Conversation
|
This looks great, and I agree that the macro should exist. However, could this not also be solved by making the macro simply apply |
|
@vidhanio Thank you! Using methods instead of directly assigning values to struct fields provides significantly more flexibility. For example, here is how one of my components is structured: #[derive(Default, AsRef, AsMut)]
pub struct CodeExample {
pub open: bool,
#[as_ref]
#[as_mut]
pub attrs: CommonAttrs,
pub children: Lazy<fn(&mut Buffer)>,
}The #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CommonAttrs {
pub id: Cow<'static, str>,
pub classes: Vec<Cow<'static, str>>,
pub styles: Vec<Cow<'static, str>>,
}To avoid explicitly declaring fields for these attributes in every component or manually implementing builder methods, a trait is used: pub trait CommonAttributeSetters {
fn id(mut self, id: impl Into<Cow<'static, str>>) -> Self
where
Self: Sized,
{
self.set_id(id);
self
}
fn class(mut self, class: impl Into<Cow<'static, str>>) -> Self
where
Self: Sized,
{
self.add_class(class);
self
}
fn style(mut self, style: impl Into<Cow<'static, str>>) -> Self
where
Self: Sized,
{
self.add_style(style);
self
}
fn set_id(&mut self, id: impl Into<Cow<'static, str>>);
fn set_classes(&mut self, classes: Vec<Cow<'static, str>>);
fn set_styles(&mut self, styles: Vec<Cow<'static, str>>);
fn add_class(&mut self, class: impl Into<Cow<'static, str>>);
fn add_style(&mut self, style: impl Into<Cow<'static, str>>);
}
impl CommonAttributeSetters for CommonAttrs {
fn set_id(&mut self, id: impl Into<Cow<'static, str>>) {
self.id = id.into();
}
fn set_classes(&mut self, classes: Vec<Cow<'static, str>>) {
self.classes = classes;
}
fn set_styles(&mut self, styles: Vec<Cow<'static, str>>) {
self.styles = styles;
}
fn add_class(&mut self, class: impl Into<Cow<'static, str>>) {
self.classes.push(class.into());
}
fn add_style(&mut self, style: impl Into<Cow<'static, str>>) {
self.styles.push(style.into());
}
}With a blanket implementation for all types that implement impl<T: AsMut<CommonAttrs>> CommonAttributeSetters for T {
fn set_id(&mut self, id: impl Into<Cow<'static, str>>) {
self.as_mut().set_id(id);
}
fn set_classes(&mut self, classes: Vec<Cow<'static, str>>) {
self.as_mut().set_classes(classes);
}
fn set_styles(&mut self, styles: Vec<Cow<'static, str>>) {
self.as_mut().set_styles(styles);
}
fn add_class(&mut self, class: impl Into<Cow<'static, str>>) {
self.as_mut().add_class(class);
}
fn add_style(&mut self, style: impl Into<Cow<'static, str>>) {
self.as_mut().add_style(style);
}
}Now, for a component to support common attributes, it is sufficient to implement #[derive(Default, AsRef, AsMut, Builder)]
pub struct CodeExample {
pub open: bool,
#[as_ref]
#[as_mut]
#[builder(skip)]
pub attrs: CommonAttrs,
pub children: Lazy<fn(&mut Buffer)>,
}rsx_cb! {
<CodeExample open=true id="the-example" class="code-example" />
}Additionally, using methods allows performing arbitrary transformations when setting values. For example, taking a |
|
I see, that makes a lot of sense. Instead of having it be its own macro (which disallows mixing builder/normal components), how would a separate syntax for these sound? something like |
|
@vidhanio For example, a general rule for generating instantiation code could be based on builder-style calls: Component::builder()
.foo(value_a.into())
.bar(value_b.into())
.build()Then the 1. No additional arguments or
|
|
Commenting as someone who doesn't understand the underlying code here, and so I make speak nonsense. Would it be possible to change all components to use the same builder syntax regardless of whether it's "needed"? This might be a breaking change, but it could be worthwhile. I have previously come across this very cool builder system that uses typestates to keep correctness: https://docs.rs/bon/latest/bon/ It supports both optional and required arguments and maintains static type-safety in all cases. They also claim equivalent performance to normal function calls in their benchmarks (and they also do ASM comparisons). The way I think it would look would be something like this (semi-pseudo code): To yank out from bon's readme: use bon::builder;
#[builder]
fn greet(name: &str, level: Option<u32>) -> String {
let level = level.unwrap_or(0);
format!("Hello {name}! Your level is {level}")
}
let greeting = greet()
.name("Bon")
.level(24) // <- setting `level` is optional, we could omit it
.call();
assert_eq!(greeting, "Hello Bon! Your level is 24");Then the macros would translate the existing syntax to call the builder: // this
maud! { Greet name="hello" level=0 }
// to this
greet().name("hello").level(0).call()
// and this
maud! { Greet name="hello" }
// to this
greet().name("hello").call()The cool thing is you automatically get relatively decent errors. Again, because of bon's typestate approach: // this
maud! { Greet level=0 }
// becomes this
greet().level(0).call()Which gives this (admittedly not perfect) error: And of course, you swap out String for whatever return is used by hypertext (I forget), and maybe you still need the wrapping #[component] macro, maybe not Edit: I see that this PR is using builders. Hopefully my above comment is still useful I feel like it would be worth maintaining only one way to do it, rather than having two different types of components and two different macros to use them (if my understanding of what's going on here is right). If the new macros here work with both the original components and the builder-style, why not just keep those? And if they're incompatible (i.e. maud_cb can only use builders, maud! can only use normal), that means one maud(_cb)! call can't use both types together. So either you have only use one kind of component, or you end up with ugly nesting of macros. |
|
@circuitsacul Thank you for your comment! I would also prefer to have only one way to create a component, but still retain some flexibility in how the user defines it. The |
|
Ah cool, so it's similar to I'm curious if you did any comparison of the two? from a quick glance, https://bon-rs.com/guide/overview |
|
Please look at the new pull request ) It uses the #[component]
fn element<'a>(
#[builder(default)] id: &'a str,
#[builder(default = 1)] tabindex: u32,
#[builder(default)] children: Lazy<fn(&mut Buffer)>,
) -> impl Renderable {
rsx! {
<div id=(id) tabindex=(tabindex)>
(children)
</div>
}
} |
The
rsx_cb!,maud_cb!andBuilderderive macros have been implemented.Closes issue #180