Attribute Passthrough

Say you’re working on a reusable component library for Sycamore. You provide components such as Button, Container, and Grid etc. which wraps native HTML elements with extra behavior.

However, a downstream user of your library uses the Button component and wants to set a custom id attribute on the wrapped <button> element. How do you solve this?

One way to do this is to add an additional id prop to your component and forward it to the <button> element. However, this approach is not scalable. If we can support setting a custom id attribute, why not also support class, disabled, etc. as well as all the different possible events.

For such scenarios, attribute passthrough comes to the rescue!

The attributes prop

You can add a special attributes prop with the Attributes type to your component. You must then specify what kind of element these attributes are expected to be applied on using the #[prop(attributes(...))] tag. Finally, you can spread the attributes on the HTML element using the attribute spread syntax.

#[component(inline_props)]
fn Button(
    // `html` means that we are spreading onto an HTML element.
    // The other possible value is `svg`.
    //
    // `button` means that we are spreading onto a `<button>` element.
    #[prop(attributes(html, button))]
    attributes: Attributes,
    // We can still accept children.
    children: Children,
    // We can still accept other props besides `attributes`.
    other_prop: i32,
) -> View {
    view! {
        // The spread (`..xyz`) syntax applies all the attributes onto the element.
        button(..attributes)
    }
}

Passing attributes to components

Downstream users of your component can set HTML attributes (and events, props, etc.) in the same way as they would on an element.

view! {
    Button(
        // Pass as many HTML attributes/events as you want.
        id="my-button",
        class="btn btn-primary",
        on:click=|_| {}
        // You can still pass in regular props as well.
        other_prop=123,
    ) {
        // Children still gets passed into the `children` prop.
        "Click me"
    }
}

Attributes are still fully type checked and will cause compile errors for unknown attributes or wrong types.

Note: There is actually a subtle different between how attributes are set on a component versus on an element. The view! macro will automatically wrap attributes in a closure if it is dynamic. For components, however, this will never happen and you will need to provide a closure manually.

We are currently finding a way to resolve this discrepancy. Of course, if you use the builder API, this isn’t a problem to begin with!

Intercepting attributes

To intercept an attribute, for instance, to modify it, simply add the attribute as a regular prop to your component. Say we wanted to add some classes to our button in addition to any extra classes we get from attribute passthrough. We could then write something like this.

use sycamore::web::StringAttribute;

#[component(inline_props)]
fn Button(
    #[prop(attributes(html, button))]
    attributes: Attributes,
    // StringAttribute is a type alias for MaybeDyn containing a string.
    class: StringAttribute,
) -> View {
    // We still want the attribute to be reactive.
    let class = move || format!("{class} custom-button");
    view! {
        button(class=class, ..attributes)
    }
}

Because of how Rust’s method resolution works, setting class on our Button component will reference the prop rather then the attribute, which lets us intercept it.