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

@thefiddler
Copy link

thefiddler commented Mar 5, 2026

Fantastic work!

I suspect this would potentially solve #123, which is currently a blocker for using hypertext in our codebase. Will check and report back.

Update: it does not fix hotpatching.

Update 2: this branch actually works fine with hotpatching! The issue is that dx serve from dioxus-cli has a special-case for the rsx! string that breaks hotpatching with hypertext. Using maud! on this branch, or renaming rsx! to html! with a simple macro works perfectly. 👍

@vidhanio
Copy link
Owner

vidhanio commented Mar 6, 2026

Hi there! Thank you so much for this work, it looks great! I would prefer that we use bon by default instead, would that be fine?

@XX
Copy link
Author

XX commented Mar 6, 2026

@vidhanio Yes, I can rewrite this to use bon. Should we leave the rest as is for now?

@vidhanio
Copy link
Owner

vidhanio commented Mar 6, 2026

@XX, yep! also, does this PR supersede your other one?

@vidhanio
Copy link
Owner

vidhanio commented Mar 6, 2026

also, please wait a sec as i will be adding in some changes to the rendering/parsing code based on #153.

@XX
Copy link
Author

XX commented Mar 6, 2026

@vidhanio Yes, this PR supersede the previous one.

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.

4 participants