Rust Wasm attach input event listener to element
Asked Answered
G

3

6

How do I add an input event listener to an HtmlInputElement/HtmlTextAreaElement?

I'm using web-sys and read this, but following that, all the elements I use inside the closure (in this case especially the input element) get moved into the closure and I can't attach the listener afterwards.

let closure = Closure::wrap(Box::new(|_: web_sys::InputEvent| {
    console_log!("{}", input.value());
}) as Box<dyn FnMut(_)>);

input.add_event_listener_with_callback("input", closure.as_ref().unchecked_ref())?;
// ^
// Doesn't work because the `input` variable was moved into the closure.

Concretely speaking, I get:

borrow of moved value: `input`
Gitlow answered 26/8, 2021 at 18:58 Comment(7)
Your code as is cannot be used to reproduce the issue, because we do not know what are those types, what receivers do their methods have, etc. So without any further information, all I can say is do not move the input variable into the closure. You can wrap it in a Rc or Rc<RefCell>> if need be, and move a clone of the Rc instead. But that might create a memory leak, because input will hold an Rc to itself.Impeccant
@SvetlinZarev what "types" are you talking about? I mean all the types here are from web-sys (and wasm-bindgen). I will have to read up on the so called "Rc".Gitlow
Well, how can anyone know they are from web-sys if there are no imports present and web-sys is never mentioned ?Impeccant
@SvetlinZarev I assumed that was the standard. I'm sorry I'm new to all of this including rust. In theory the link leads to a wiki about web-sys, but you are right. I will add the imports.Gitlow
@SvetlinZarev I added the imports and basically the rest of the code in case it helps :)Gitlow
@SvetlinZarev so I went to sleep yesterday but I thought I saw an answer from you... now it seems to be gone... it didn't work? I'm kind of stuck on this right now :(Gitlow
yeah I've deleted it because I'm not 100% sure. I've restored it, if it can be any help. In case the Weak pointer returns None you may try to use Rc in its place.Impeccant
G
12

First of all, I would like to thank Svetlin Zarev for taking the time to answer my question. I wouldn't have come to this without them. And please check out their answer.

For someone coming from javascript, all this stuff is quite a lot to take in and I wanted to present a "simple" answer.

To add an input (or really any kind) of event listener and capture the targets value, you may use the following code:

let cb = Closure::wrap(Box::new(|e: Event| {
    let input = e
        .current_target()
        .unwrap()
        .dyn_into::<web_sys::HtmlTextAreaElement>()
        .unwrap();

    console_log!("{:?}", input.value());
}) as Box<dyn FnMut(_)>);

input.add_event_listener_with_callback("input", &cb.as_ref().unchecked_ref())?;

cb.forget();

I would also recommend reading the following articles:

Gitlow answered 27/8, 2021 at 19:12 Comment(0)
I
3

It compiles now, although I cannot test it if it actually works. What I've changed:

  1. Used Rc<RefCell<INPUT>> in order to avoid the error about the moved variable

  2. Used Rc::downgrade(&input) in order to provide a Weak inside the closure in order to avoid a cyclic reference, thus avoiding a memory leak. But I'm not sure how the whole thing works in a web env, so maybe a It should be Rc instead, because the Rc will be dropped at the end of the run() method.

You should check the relevant rustdoc for more detailed explanation on what those types are doing.

use wasm_bindgen::{prelude::*, JsCast};
use web_sys::HtmlElement;
use std::cell::RefCell;
use std::rc::Rc;

#[wasm_bindgen]
pub fn run(cont: &HtmlElement) -> Result<(), JsValue> {
    let window = web_sys::window().expect("could not get window handle");
    let document = window.document().expect("could not get document handle");
    let input = document
        .create_element("textarea")?
        .dyn_into::<web_sys::HtmlTextAreaElement>()?;

    let input = Rc::new(RefCell::new(input));
    let weak_input = Rc::downgrade(&input);

    let closure: Box<dyn FnMut(_)> = Box::new(move |_: web_sys::InputEvent| {
        let input = weak_input.upgrade().unwrap();
        let _a = &input.borrow().value();
    });

    let closure = Closure::wrap(closure);

    input
        .borrow_mut()
        .add_event_listener_with_callback("input", closure.as_ref().unchecked_ref())?;
    closure.forget();

    cont.append_child(&input.borrow())?;

    Ok(())
}

Impeccant answered 26/8, 2021 at 19:36 Comment(1)
So I've read some of the links you've provided and it's quite a lot to take in for someone coming from JavaScript (I've not read all of them [yet] because I simply don't have the energy for that). I will try to make my code work with the details you have provided. What I don't understand, why such a simple use case causes such a headache. Wouldn't this be one of the first things someone tries to do? Why do you need 20 lines of rust code for 1 line of JavaScript?Gitlow
T
0

Rust function as event handler

In an attempt to make handling events of HTML elements with web-sys a bit easier, I created a function that sets a Rust function or closure (callback) as the event handler:

/// Sets the `oninput` field of `input_elem` to `callback`.
fn set_oninput<F>(input_elem: &HtmlInputElement, callback: F)
where
    // FnMut allows the closure to mutate variables from the surrounding scope, unlike Fn.
    F: IntoWasmClosure<dyn FnMut(Event)> + 'static,
{
    // https://mcmap.net/q/1004068/-how-to-convert-closure-to-js_sys-function
    let closure = Closure::new(callback);
    input_elem.set_oninput(Some(closure.as_ref().unchecked_ref()));
    closure.forget();
}

Here's an example on how to use this on an HtmlInputElement:

let input = get_elem_or_panic("textInput0").dyn_into::<HtmlInputElement>()?;
set_oninput(&input, |event: Event| {
    let target_as_input_elem = get_target_as::<HtmlInputElement>(event);
    let input_value = target_as_input_elem.value();
    let input_value_len = input_value.len();
    get_elem_or_panic("textLabel0").set_text_content(Some(&format!("Length of input: {input_value_len}")));
});

With this approach, it's not possible to reference the input inside the closure, which is why we get the input back under the name target_as_input_elem using the function get_target_as:

fn get_target_as<T: JsCast>(event: Event) -> T {
    let target = event.target().unwrap();
    target.dyn_into().unwrap()
}

Use mutable collection inside closures

I wanted to change an outside value from multiple event handlers. In my case, I tried to update a HashMap whenever an input element changed. I ran into the errors

use of moved value

and

value moved into closure here, in previous iteration of loop

To mutate the HashMap from multiple closures, I used Mutex and LazyLock:

// The lifetime of this variable needs to be static so it can be used in `set_oninput`
static INPUT_ID_AND_LENGTH: LazyLock<Mutex<HashMap<String, usize>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

for i in 1..4 {
    let input = get_elem_or_panic(&format!("textInput{i}")).dyn_into::<HtmlInputElement>()?;
    set_oninput(&input, |event: Event| {
        let target_as_input_elem = get_target_as::<HtmlInputElement>(event);
        let input_value = target_as_input_elem.value();
        let input_value_len = input_value.len();
        INPUT_ID_AND_LENGTH.lock().unwrap().insert(target_as_input_elem.id(), input_value_len);

        let sum_of_input_lengths: usize = INPUT_ID_AND_LENGTH.lock().unwrap().values().sum();
        get_elem_or_panic("textLabel1")
            .set_text_content(Some(&format!("Sum of length of these three inputs: {sum_of_input_lengths}")));
    });
}

This allows passing the mutable collection into the closure.

Here's a demo project that shows the use of these functions. It also contains an on_click function for reacting to clicks on an HtmlButtonElement.


For completeness' sake, here are the results of some experiments with mutating a collection with static lifetime from within a closure:

  • Trying LazyLock without Mutex yielded an error message saying that the value can't be borrowed as mutable.
  • Trying Mutex without LazyLock produced also an error: cannot call non-const fn HashMap::<String, usize>::new in statics
  • When using LazyCell instead of LazyLock, I got the error: UnsafeCell<State<Mutex<HashMap<String, usize>>, ...>> cannot be shared between threads safely
Tabina answered 10/8, 2024 at 22:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.