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
input
variable into the closure. You can wrap it in aRc
orRc<RefCell>>
if need be, and move a clone of theRc
instead. But that might create a memory leak, becauseinput
will hold anRc
to itself. – Impeccantweb-sys
(andwasm-bindgen
). I will have to read up on the so called "Rc". – Gitlowweb-sys
if there are no imports present andweb-sys
is never mentioned ? – Impeccantweb-sys
, but you are right. I will add the imports. – GitlowWeak
pointer returnsNone
you may try to useRc
in its place. – Impeccant