0

Working forward from the last example in this page of the Yew Docs, I'd like to use a callback to update the label on an input, rather than just outputting a console message. Or in other words, having done what the example does successfully, I'd then like to change something outside the closure, to wit, a string which can be used in the html later.

#[function_component(Settings)]
pub fn settings_page() -> Html {
    let mut value_label = String::new();

    let onchange_slider = {
        Callback::from(move |e: Event| {
            let input: HtmlInputElement = e.target_unchecked_into();
            let value = input.value().parse::<i32>().unwrap_or(10);
            log::info!("value = {}", value);  //This works fine.
            value_label = format!("The value is {}", value); //Error: closure is FnMut because it mutates the variable value_label here
        })
};

html! {
    <div class="slider">
         <input type="range" class="form-range" min="-5" max="20" id="my_broken_slider" onchange={onchange_slider.clone()} />
          <label for="my_broken_slider" class="form-label">{value_label}</label>
    </div>
}
}

It seems like the docs are almost useful here. I can definitely call the onchange function, and I can read the slider value. The trick appears to be, from the docs, that Callback is "just an Fn wrapped in Rc," which is neat for cheap cloning, but less neat from the perspective of doing something useful on a web page. So it's as if, by design, the callback isn't allowed to change anything. That makes me think there's some actually correct way to do what I need to do that's unrelated to callbacks entirely, but ... I'm at a loss here.

djmcmath
  • 97
  • 5
  • Idk if yew has a builtin solution for this, but in general you can use [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). – drewtato May 12 '23 at 02:04

1 Answers1

1

Even if you would be able to change value_label (for example, by using interior mutability), this would still not cause the necessary re-render. You need to use state:

#[function_component(Settings)]
pub fn settings_page() -> Html {
    let value_label = use_state(String::new);

    let onchange_slider = {
        let value_label = value_label.clone();
        Callback::from(move |e: Event| {
            let input: HtmlInputElement = e.target_unchecked_into();
            let value = input.value().parse::<i32>().unwrap_or(10);
            log::info!("value = {}", &*value);
            value_label.set(format!("The value is {}", value));
        })
    };

    html! {
        <div class="slider">
             <input type="range" class="form-range" min="-5" max="20" id="my_broken_slider" onchange={onchange_slider.clone()} />
              <label for="my_broken_slider" class="form-label">{&*value_label}</label>
        </div>
    }
}
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • That's close, but it isn't quite working: when I go to use *value_label, the new error is that I'm borrowing after a move. I feel like I've solved this with clone() in other spots, but I'm drawing a blank on how to make that work here. – djmcmath May 12 '23 at 12:37
  • @djmcmath You just need to use `&*value_label`, updated the answer. – Chayim Friedman May 12 '23 at 13:17
  • Ok, that worked, thanks. I don't know that I understand *why*, but ... it does work. In my mind, "&*value" is the same as "value". I'm going to guess that use_state is a classy way of executing interior mutability, e.g. I'm doing something sneaky with pointers. – djmcmath May 12 '23 at 13:49
  • @djmcmath `&*value` invokes `Deref`. `use_state()` is managing state, like in React. – Chayim Friedman May 12 '23 at 14:45