Conversation
|
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 I hope this gets merged |
|
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 2: this branch actually works fine with hotpatching! The issue is that |
|
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? |
|
@vidhanio Yes, I can rewrite this to use |
|
@XX, yep! also, does this PR supersede your other one? |
|
also, please wait a sec as i will be adding in some changes to the rendering/parsing code based on #153. |
|
@vidhanio Yes, this PR supersede the previous one. |
|
@vidhanio @XX This is just a thought I'm throwing out there. It might not make sense (particularly if it's just bon-by-default, and not bon-only) If this is going to be bon-only, what do you think about using bon's function signature instead? I guess that would mean typed-builder would become incompatible, so that might not be acceptable. The signature is this as opposed to You can of course change For example @XX, I re-built your CommonAttrs, implementing it on the Builder. I wasn't able to do it via as_ref, but it's fairly close: #[derive(Default)]
struct CommonAttrs {
id: Cow<'static, str>,
class: Cow<'static, str>,
}
trait CommonAttrsExt: Sized {
fn attrs(&mut self) -> &mut CommonAttrs;
fn id(mut self, id: impl Into<Cow<'static, str>>) -> Self {
self.attrs().id = id.into();
self
}
fn class(mut self, id: &str) -> Self {
let mut old = self.attrs().class.to_string();
old.push_str(id);
self.attrs().class = Cow::Owned(old);
self
}
}
struct Component;
#[bon]
impl Component {
#[builder]
fn new(
#[builder(field)] attrs: CommonAttrs,
test: &str,
children: impl Renderable,
) -> impl Renderable {
maud! {
div #(attrs.id) .(attrs.class) { (test) (children) }
}
}
}
impl<'a, R: Renderable, S: component_builder::State> CommonAttrsExt for ComponentBuilder<'a, R, S> {
fn attrs(&mut self) -> &mut CommonAttrs {
&mut self.attrs
}
}
fn main() {
let res = maud! {
Component
test="2"
id="1"
class="btn"
{ "hello" }
}
.render();
println!("{res:?}"); // Rendered("<div id=\"1\" class=\"btn\">2hello</div>")
}The function version would be basically the same, just a function instead of the unit struct of course. So you retain all the functionality of bon, but with one less struct def. And if you still want to do fully custom stuff, I imagine you can just do this: struct MyBuilder {
attr1, attr2
}
impl MyBuilder {
fn call(self) -> impl Renderable { maud! { ... } }
}
fn CustomComponent() -> MyBuilder {
MyBuilder::default()
}So really, I don't think you lose anything other than support for typed-builder and other non-bon builders. |
|
@vidhanio I have finally updated this PR, please take a look. |
|
@XX thanks! taking a look |
| } | ||
| } | ||
|
|
||
| #[renderable(builder = Builder, attrs = [builder])] |
There was a problem hiding this comment.
attrs is a bit ambiguous, and i don't think it needs to be an array. i think builder_attr = builder makes more sense. basically, i'd rather it be a single-value field named builder_attr.
There was a problem hiding this comment.
The use of an array here is intentional: when implementing a custom builder or forwarding additional derives, a user may need to define a set of attributes that should be transferred to the struct fields (while the rest remain on the function arguments). For example:
#[renderable(attrs = [builder, serde])]
#[derive(Serialize)]
fn component(
#[builder(default = 1)]
tabindex: u32,
#[serde(skip)]
#[builder(default)]
children: Lazy<fn(&mut Buffer)>,
) -> impl Renderable {
rsx! {
<div tabindex=(tabindex)>
(children)
</div>
}
}There was a problem hiding this comment.
It could be implemented so that the attr parameter accepts either a single value or an array of values.
There was a problem hiding this comment.
ah, i see. how about an interface like #[renderable(builder = DefaultBuilder, forwarded_attrs(serde, builder))]?
There was a problem hiding this comment.
Alternatively, the condition could be inverted: all attributes of the function would be forwarded to the generated struct, while only those specified in an additional parameter #[renderable(fn_attrs = [..])] would remain on the function itself. This might be somewhat confusing, but it would likely be used less often.
There was a problem hiding this comment.
yeah i think that's a good idea. do that, but use the fn_attrs(...) syntax.
There was a problem hiding this comment.
add tests for a component containing children: Lazy<F> where F: Fn(&mut Buffer). These tests all only would work for components where children do not capture any variables from the environment, which will rarely be the case in actual use. in #167 we already found that this wouldn't be possible when children must be Default, but that's okay we can just test for when it is required.
This pull request changes the way user components are instantiated during the expansion of the
rsx!andmaud!macros. Previously, a struct-literal approach was used:Now a builder-based approach is used:
The builder methods can be:
#[derive(Builder)]from theboncrate);#[component]macro arguments (for example,#[renderable(builder = hypertext::DefaultBuilder)]or#[renderable(builder = bon::Builder)]);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:
..at the end if the component implementsDefault. This syntax has been removed from the component parser.Default, it is now necessary to explicitly specify the use ofDefaultBuilderinstead of the defaultTypedBuilder(i.e.,#[component(builder = hypertext::DefaultBuilder)]) if omitting some component properties at the call site should be allowed.builder,build, and field setters, which may cause conflicts with similarly named methods already present in older components.#[component]macro. By default, this list includes thebuilderattribute to allow passing configuration options toTypedBuilder.Closes issue #180
Closes issue #128