Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend examples/counters_stable with <select> and interactive add_x_counters <button> #1937

Closed
qknight opened this issue Oct 24, 2023 · 2 comments

Comments

@qknight
Copy link

qknight commented Oct 24, 2023

While reading the documentation on signals I was using https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable and extended the example a little.

I think that my changes are valuable for others so could we add it to the example? In the code below I introduce two new things:

  • add x counters where the label of the button is updated based on a signal
  • a select example where the options texts are updated

It also has a bug where the print button does not work anymore after the first item has been selected and the other button is pressed. Probably some data race of some sort. The resulting stack trace in the F12 console is hard to understand.

But maybe this helps someone else already.

use leptos::*;
use leptos_meta::*;
use log::info;
use log::error;

const MANY_COUNTERS: usize = 1000;

type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;

#[derive(Copy, Clone)]
struct CounterUpdater {
    set_counters: WriteSignal<CounterHolder>,
}

#[component]
pub fn Counters() -> impl IntoView {
    let (next_counter_id, set_next_counter_id) = create_signal(0);
    let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
    provide_context(CounterUpdater { set_counters });

    let add_counter = move |_| {
        let id = next_counter_id.get();
        let sig = create_signal(0);
        set_counters.update(move |counters| counters.push((id, sig)));
        set_next_counter_id.update(|id| *id += 1);
    };

    let add_many_counters = move |_| {
        let next_id = next_counter_id.get();
        let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
            let signal = create_signal(0);
            (id, signal)
        });
        set_counters.update(move |counters| counters.extend(new_counters));
        set_next_counter_id.update(|id| *id += MANY_COUNTERS);
    };

    let clear_counters = move |_| {
        set_counters.update(|counters| counters.clear());
    };

    ///////////////////// x counters input ///////////////////////////////
    let (x_counters_input, set_x_counters_input) = create_signal(2);
    let add_x_counters = move |_| {
        let v = x_counters_input.get() as usize;

        info!("add_x_counters: {}", v);
        let next_id = next_counter_id.get();
        let new_counters = (next_id..next_id + v).map(|id| {
            let signal = create_signal(0);
            (id, signal)
        });
        set_counters.update(move |counters| counters.extend(new_counters));
        set_next_counter_id.update(|id| *id += v);
    };

    view! {
        <Title text="Counters (Stable)" />
        <div style="background:#e5ffe5">
            <button on:click=add_counter>
                "Add Counter"
            </button>
            <button on:click=add_many_counters>
                {format!("Add {MANY_COUNTERS} Counters")}
            </button>
            <button on:click=clear_counters>
                "Clear Counters"
            </button>
            <p>
            <input data-testid="add_x_counters_input" type="text" on:input=move |ev| {
                let x = event_target_value(&ev).parse::<i32>().unwrap_or_default();
                set_x_counters_input.set(x)
              }
              prop:value=x_counters_input/>
            <button on:click=add_x_counters>
                "Add " {move || x_counters_input.get()} " counters"
            </button>
            </p>

            <p>
                "Total: "
                <span data-testid="total">{move ||
                    counters.get()
                        .iter()
                        .map(|(_, (count, _))| count.get())
                        .sum::<i32>()
                        .to_string()
                }</span>
                " from "
                <span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
                " counters."
            </p>
            <ul>
                <For
                    each={move || counters.get()}
                    key={|counter| counter.0}
                    children=move |(id, (value, set_value))| {
                        view! {
                            <Counter id value set_value/>
                        }
                    }
                />
            </ul>
        </div>
    }
}

#[component]
pub fn Selector() -> impl IntoView {
    ////////////////////// SelectOption //////////////////////////////
    #[derive(Debug, Clone, Copy)]
    pub struct SelectOption {
        pub label: &'static str,
        pub pos: usize,
    }
    let options: Vec<SelectOption> =  vec![
        SelectOption{ label: "h0rses", pos: 0},
        SelectOption{ label: "b1rds", pos: 1},
        SelectOption{ label: "2nfish", pos: 2},
    ];

    let optionsClone = options.clone();
    let selection: RwSignal<Option<usize>> = create_rw_signal(Some(1)); // initial selection

    view! {
      <div style="background:#ffffbf">
      <select
        id = "myselect"
        on:change = move |ev| {
          let new_selection = event_target_value(&ev);
          if new_selection.is_empty() {
            selection.set(None);
          } else {
            match new_selection.parse() {
              Ok(v) => {
                info!("you selected {}", v);
                selection.set(Some(v))
              },
              Err(_) => {
                error!("Error: Unexpected option value {new_selection}");
              },
            }
          }
        }
      >
      <For
        each = move || optionsClone.clone()
        key = |option| option.pos
        let:option
          >
          <option
            value = option.pos
            selected = (selection.get() == Some(option.pos))
            >
            { option.label }
          </option>
      </For>
      </select>
        <p>
        "You selected: "
        <span data-testid="myselection">{move || {
          match selection.get() {
            Some(v) => {
              options[v].label
            },
            None => "no idea..."
          }
        }
        }</span>
        </p>
      </div>
    }
}

#[component]
fn Counter(
    id: usize,
    value: ReadSignal<i32>,
    set_value: WriteSignal<i32>,
) -> impl IntoView {
    let CounterUpdater { set_counters } = use_context().unwrap();
    let input = move |ev| {
        set_value
            .set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
    };

    // this will run when the scope is disposed, i.e., when this row is deleted
    // because the signal was created in the parent scope, it won't be disposed
    // of until the parent scope is. but we no longer need it, so we'll dispose of
    // it when this row is deleted, instead. if we don't dispose of it here,
    // this memory will "leak," i.e., the signal will continue to exist until the
    // parent component is removed. in the case of this component, where it's the
    // root, that's the lifetime of the program.
    on_cleanup(move || {
        log::debug!("deleted a row");
        value.dispose();
    });

    view! {
        <li>
            <button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
            <input data-testid="counter_input" type="text"
                prop:value={move || value.get().to_string()}
                on:input=input
            />
            <span>{value}</span>
            <button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
            <button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
        </li>
    }
}

#[component]
pub fn DynSelector() -> impl IntoView {
    // type SelectionHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;

    // #[derive(Copy, Clone)]
    // struct SelectionUpdater {
    //     set_selectionOptions: WriteSignal<SelectionHolder>,
    // }
    #[derive(Debug, Clone, Copy)]
    pub struct SelectionOption {
        pub label: &'static str,
        pub id: u32,
        pub amount: RwSignal<u32>,
    }
    let selectionOptions1: Vec<SelectionOption> = vec![
        SelectionOption{ label: "h0rses", id: 0, amount: create_rw_signal::<u32>(0)},
        SelectionOption{ label: "b1rds", id: 1, amount: create_rw_signal::<u32>(10)},
        SelectionOption{ label: "2nfish", id: 2, amount: create_rw_signal::<u32>(20)},
        SelectionOption{ label: "3lk", id: 3, amount: create_rw_signal::<u32>(30)},
    ];
    let default_selection: RwSignal<Option<u32>> = create_rw_signal(Some(0));
    let selectionOptions = create_rw_signal::<Vec<SelectionOption>>(selectionOptions1);
    let print = move |_| {
        let s = selectionOptions.get();
        info!("{} {}", s[0].label, s[0].amount.get());
    };
    let add_option = move |_| {
        let a = SelectionOption{ label: "4eel", id: 4, amount: create_rw_signal::<u32>(20)};
        selectionOptions.update(|n| {
            if n.len() > 0 {
                n[0].amount.set(200);
                info!("n[0]: {} {}", n[0].label, n[0].amount.get())
            }
            n.push(a);
        });
    };
    view! {
      <div style="background:#eae3ff">
        <select
          id = "mymultiselect"
          on:change = move |ev| {
            let target_value = event_target_value(&ev);
            if target_value.is_empty() {
              default_selection.set(None);
            } else {
               match target_value.parse() {
                 Ok(v) => {
                   info!("you selected {}", v);
                   default_selection.set(Some(v))
                 },
                 Err(_) => {
                   error!("Error: Unexpected option value {target_value}");
                 },
              }
            }
          }
        >
          <For
            each = {move || selectionOptions.clone().get()}
            key = |option| option.id
            let:option
          >
            <option
              value = move || option.id
              default_selection = (default_selection.get() == Some(option.id))
            > { move || {
               let z = option.amount.get();
               let v = format!("{} - {}", option.label, z);
               v
               }
              }
            </option>
          </For>
        </select>
        <p>
        "You selected: "
        <span data-testid="mymultiselection">{move || {
          let selected_option = default_selection.get();
            match selected_option {
                Some(v) => {
                    let l = selectionOptions.get()[v as usize].label;
                    let a = selectionOptions.get()[v as usize].amount.get();
                    format!("{} - {}", l, a.to_string())
                },
                None => "no idea...".to_string()
            }
        }
        }</span>
         <button on:click=add_option>
           "Add another option to selector"
         </button>
         <button on:click=print>
           "Print"
         </button>
        </p>
      </div>
    }
}

@gbj
Copy link
Collaborator

gbj commented Oct 24, 2023

Just eyeballing it, the selector codes ends up being significantly longer than the rest of the example -- I'd be very happy to have a simple example of how to use <select>, as I don't think we actually have that anywhere in the codebase, but wouldn't want to add so much to this relatively simple example.

Note also that we tend to follow clippy for idiomatic Rust style -- you have things like camelCase variable names that would be rejected.

I would welcome a PR adding the dynamic "add X counters" and an example of <select> to the counters example.

Are you looking for help debugging the broken part? If so let me know, and maybe we could convert this to a GitHub Discussion instead of an issue.

@qknight
Copy link
Author

qknight commented Oct 24, 2023

Cool! Then I'll prepare a PR which we then might discuss in the PR comments section!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants