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.