Skip to content

Using builder for component instantiation#183

Open
XX wants to merge 1 commit intovidhanio:mainfrom
XX:use_builder
Open

Using builder for component instantiation#183
XX wants to merge 1 commit intovidhanio:mainfrom
XX:use_builder

Conversation

@XX
Copy link

@XX XX commented Mar 2, 2026

This pull request changes the way user components are instantiated during the expansion of the rsx! and maud! macros. Previously, a struct-literal approach was used:

rsx! {
    <Component foo="foo" bar="bar" .. />
}
...
Component {
    foo: "foo",
    bar: "bar",
    ..Default::default()
}

Now a builder-based approach is used:

rsx! {
    <Component foo="foo" bar="bar" />
}
...
Component::builder()
    .foo("foo")
    .bar("bar")
    .build()

The builder methods can be:

  • Automatically derived with compile-time checks ensuring that all fields are initialized (by default using #[derive(TypedBuilder)] from the typed-builder crate);
  • Generated according to the builder specified in the #[component] macro arguments (for example, #[component(builder = hypertext::DefaultBuilder)] or #[component(builder = bon::Builder)]);
  • Implemented manually by the user for a component type, with any custom behavior required.

It is also now possible to propagate attributes defined on the component function parameters to the fields of the generated struct. This allows specifying builder-specific field attributes, including default argument values.

Some usage examples are provided in two new tests: https://github.com/vidhanio/hypertext/pull/183/changes#diff-8c79c3d623f866c80fb5e4093f5e2b774c3d590025116a0352d4a240e1ed6817

Backward Compatibility

The changes preserve API backward compatibility as much as possible. However, in some aspects the new behavior is incompatible with the previous one:

  • It is no longer necessary to specify .. at the end if the component implements Default. This syntax has been removed from the component parser.
  • For components that implement Default, it is now necessary to explicitly specify the use of DefaultBuilder instead of the default TypedBuilder (i.e., #[component(builder = hypertext::DefaultBuilder)]) if omitting some component properties at the call site should be allowed.
  • The generated builders define the methods builder, build, and field setters, which may cause conflicts with similarly named methods already present in older components.
  • Attributes from the component constructor function parameters may be forwarded to the fields of the generated struct. The list of such attributes is expected in the attrs argument of the #[component] macro. By default, this list includes the builder attribute to allow passing configuration options to TypedBuilder.

Closes issue #180
Closes issue #128

@circuitsacul
Copy link

circuitsacul commented Mar 3, 2026

Nice, this works

use bon::bon;
use hypertext::prelude::*;

struct Component;

#[bon]
impl Component {
    #[builder]
    fn new(id: u64, optional: Option<String>, children: impl Renderable) -> impl Renderable {
        maud! { div #(id) { (optional) (children) } }
    }
}

fn main() {
    let res = maud! {
        Component id=1 { "hello" }
    }
    .render();

    println!("{res:?}"); // Rendered("<div id=\"1\">hello</div>")
}

I only wish there was a way to do it with bare functions, but unfortunately the generated API is function().id().call(), and while I can change .call to .build, I can't add in ::builder(). I can't think of a clean solution to this, at least not without making the maud/rsx macros aware of the actual type of the components. But that's alright, this already makes me very happy lol

I hope this gets merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants