Skip to content

Advanced macros for cleaner component property syntax #180

@XX

Description

@XX

The Problem

Currently, when using components, it is necessary to explicitly specify .. to fill in omitted properties with their default values:

rsx! {
    <Element ../>
    <Element id="element" ../>
}

This is because the generated code instantiates the component using struct literal syntax, where .. expands to ..Default::default():

Element {
    id: "element",
    ..Default::default()
}

This approach is simple and clear. However, for large components with many properties, it results in the almost constant use of .., which clutters the code:

rsx! {
    <CodeExample ..>
        <CodeExamplePreview resize=true ..>
            <Element ..>"Hello"</Element>
        </CodeExamplePreview>
        <CodeExampleSource ..>
            <code class="language-html">
                r#"<Element ..>"Hello"</Element>"#
            </code>
        </CodeExampleSource>
        <CodeExampleButton ..>"Code"</CodeExampleButton>
    </CodeExample>
}

Additionally, this makes it harder to use wrapper or aggregating structs, since all component attributes must match struct field names exactly. This leads to less clean syntax like the following:

#[derive(Default)]
pub struct CodeExample {
    pub open: bool,
    pub attrs: CommonAttrs,
    pub children: Lazy<fn(&mut Buffer)>,
}

rsx! {
    <CodeExample open=true attrs=(CommonAttrs::new().id("example").class("toggle").style("color: red"))>
        ...
    </CodeExample>
}

Proposed Solution

I propose extending the component instantiation code generator with an additional mode that relies on:

  • The component struct implementing Default
  • Builder-style setter methods for properties

For example:

Element::default()
    .id("element")
    .tabindex(2)

With this approach, separate macro variants such as maud_cb! and rsx_cb! (CB = Component Builder) could significantly simplify component property syntax:

rsx_cb! {
    <Element />
    <Element id="element" />
    <Element id="element" tabindex=2 />
}
rsx_cb! {
    <CodeExample />
    <CodeExample open=true id="example" class="toggle" style="color: red" />
}

The .attr(...) methods could be implemented either manually or automatically using a #[derive(Builder)] macro based on the struct’s fields:

#[derive(Default, Builder)]
struct Element<'a> {
    id: &'a str,
    tabindex: u32,
    children: Lazy<fn(&mut Buffer)>,

    #[builder(skip)]
    _skipped: (),
}

Implementation

I have implemented the functionality described above so it can be experimented with and potentially used as a foundation if there is interest in adding it to hypertext.

PR: #179
Working example in tests: https://github.com/vidhanio/hypertext/pull/179/changes#diff-dc0fadfd8cc5b92a1297488453c2a317f203183fd3b3a27f0c4870b7aa9cb47cR808

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions