Mixins

Mixins is a pattern that can be used to allow the user access to a components builder. For instance, we may want to give the user a way of setting the ID of our component, or to attach some custom event handler to it.

Here's a very basic example. The component_with_mixin component allows its user to attach to its top level elements' apply function:

fn component_with_mixin(
    mixin: impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement>,
) -> Dom {
    html!("div", {
        .apply(mixin)
        .text("I support a mixin")
    })
}

The user can now customize essentially anything it wants on the component. Here's an example setting a CSS class:

fn using_a_mixin() -> Dom {
    html!("div", {
        .child(component_with_mixin(|builder| builder.class("added-from-user")))
        .text("I applied a mixin to my child!")
    })
}
By allowing access to your components builder, it's possible for the user to create conflicts with the invariants specified for your components.

Making generic mixin implementations

Allowing mixins to be applied to components that we write is good, but we can also implement generic functionality as mixin factories.

Let's say we wish to get a signal for the hovered state of any element. We could of course copy and paste the code around, but it seems like a bad practice.

We can instead create a higher order function, which produces a mixin that we can pass to any component that we wish to support this hover functionality for!

#[derive(Copy, Clone, Debug)]
enum HoverState {
    Hovered,
    NotHovered,
}

fn hover_signal_mixin(
    callback: impl FnOnce(DomBuilder<HtmlElement>, MutableSignal<HoverState>) -> DomBuilder<HtmlElement>,
) -> impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> {
    let is_hovered = Mutable::new(HoverState::NotHovered);

    move |builder| {
        builder
            .apply(clone!(is_hovered =>  move |builder|
                callback(builder, is_hovered.signal())))
            .event(clone!(is_hovered => move |_: MouseEnter|
                is_hovered.set(HoverState::Hovered)))
            .event(clone!(is_hovered => move |_: MouseLeave|
                is_hovered.set(HoverState::NotHovered)
            ))
    }
}
fn using_hover_signal_mixin() -> Dom {
    component_with_mixin(hover_signal_mixin(|builder, hover_signal| {
        builder.class_signal(
            "hovered",
            hover_signal.map(|hover_state| match hover_state {
                HoverState::Hovered => true,
                _ => false,
            }),
        )
    }))
}

Avoiding footgunnyness

As the warning above states, it can be risky to provide full access to your internal builder. This can allow the user to break invariants assumed to be in place for your internal implementation, so use mixins a bit carefully.

Use fragments

If what we wish to do is to let the user inject one or more children into a section of our components DOM tree, we may wish to use fragments instead.

More precise arguments

It's a good idea to consider more targeted callback functions and signals. If you want the user to be allowed to provide a class signal, then simply accept the class signal:

fn component_with_class_signal(active_class_signal: impl Signal<Item = bool> + 'static) -> Dom {
    html!("div", {
        .class_signal("active", active_class_signal)
    })
}

This lets the consumer do a more controlled customization of your component, and doesn't risk the issues associated with mixins.

Builder wrapper

Alternatively, we can wrap the builder in a struct to expose only certain operations:

pub struct MyBuilderWrapper {
    inner: DomBuilder<HtmlElement>,
}

impl MyBuilderWrapper {
    pub fn class(self, class: impl AsRef<str>) -> Self {
        Self {
            inner: self.inner.class(class.as_ref()),
        }
    }

    fn new(inner: DomBuilder<HtmlElement>) -> Self {
        Self { inner }
    }
}

pub fn my_wrapped_mixin(wrapped_mixin: impl FnOnce(MyBuilderWrapper) -> MyBuilderWrapper) -> Dom {
    html!("div", {
        .apply(|builder| {
            wrapped_mixin(MyBuilderWrapper::new(builder)).inner
        })
    })
}