Working with callbacks
When making shared components, we usually want to customize certain behaviours of the component. The common way to solve this, is by allowing the user of the component to provide callbacks that we trigger under specific circumstances.
With dominator, we can easily do this with regular rust closures:
pub fn my_shared_button(mut on_click: impl (FnMut() -> ()) + 'static) -> Dom {
html!("button", {
.event(move |_: events::Click| {
on_click();
})
})
}
It's important to be familiar with the clone!
macro provided by DOMINATOR.
What it does is to take a list of comma separated values as the first argument, then after the fat arrow (=>
) the code block we wish to move the clones into.
let value_a = Mutable::new(42);
let value_b = Mutable::new(666);
let my_lambda: &dyn Fn() -> () = &clone!(value_a, value_b => move || {
value_a.set(1);
value_b.set(1);
});
value_a.set(2);
We here make a lambda that captures clones of the two values by value, using the clone!
macro.
Here's an example of using our button:
pub fn use_onclick() -> Dom {
let my_local_var = Mutable::new(0);
html!("div", {
.child(html!("span", { . text_signal(my_local_var.signal().map(|v| v.to_string()))}))
.child(my_shared_button(clone!(my_local_var => move || {
my_local_var.set(my_local_var.get() + 1)
})))
})
}
Note that any callback handed over to the JS runtime must have a 'static
lifetime.
This means it must capture everything that isn't a 'static
lifetime reference by value!
Reusing closures
Sometimes, we need to provide a callback function that will be handed over to multiple callers. There are a few ways to do this, depending on what type of closure you wish to use.
Fn
closure
The simplest is to use an Fn
closure, which we can wrap in an Arc
internally, and hand over to our event handlers:
pub fn my_shared_button_factory(
val: Mutable<i32>,
on_click: impl (Fn() -> ()) + 'static,
) -> Dom {
let on_click = Arc::new(on_click);
html!("div", {
.child_signal(val.signal().map(move |v| {
Some(html!("button", {
.event(clone!(on_click => move |_: events::Click| {
on_click();
}))
}))
}))
})
}
FnMut closure
If we need to use an FnMut
closure, it's not sufficient to wrap the closure in an Arc
.
The problem is that to invoke an FnMut
, you need to own a mutable reference to it.
We can use the same approach as for Fn
, but we need to wrap the closure in a mutex as well as an Arc
.
This allows us to acquire a mut borrow for the closure when we need to invoke it!
pub fn my_shared_button_factory(
val: Mutable<i32>,
on_click: impl (FnMut() -> ()) + 'static,
) -> Dom {
let on_click = Arc::new(Mutex::new(on_click));
html!("div", {
.child_signal(val.signal().map(clone!(on_click => move |v| {
let on_click = on_click.clone();
Some(html!("button", {
.event(move |_: events::Click| {
on_click.lock().unwrap()();
})
}))
})))
})
}
Closure factory
And finally, if for some reason our FnMut
closure cannot be Clone
, we can adopt a factory pattern.
This is simply a wrapping lambda, which returns a new closure for each invocation:
pub fn my_shared_button_factory<
TFn: (FnMut() -> ()) + 'static,
TFactory: (FnMut() -> TFn) + 'static,
>(
val: Mutable<i32>,
mut on_click_factory: TFactory,
) -> Dom {
html!("div", {
.child_signal(val.signal().map(move |v| {
let mut on_click = on_click_factory();
Some(html!("button", {
.event(move |_: events::Click| {
on_click();
})
}))
}))
})
}
Dealing with 'static constraints for state management
Since all callbacks we will be dealing with are constrained to capturing by 'static
, we have to make some considerations when designing our application state management.
What this practically means is th at if we want to connect our events to the rest of our application in any meaningful way, we have two options:
- Create static references to the application state by
Box::leak()
and share&'static
references to the relevant parts - Keep state inside cloneable pointer types (usually
Arc
), and capture by move
Both of these are valid approaches, and typically a mix is good. Again, the Patterns chapter will cover more of this.