Pauan / rust-dominator

Zero-cost ultra-high-performance declarative DOM library using FRP signals for Rust!
MIT License
960 stars 62 forks source link

Feat: with_element method on Dom #45

Closed dakom closed 3 years ago

dakom commented 3 years ago

Problem

I have a function that receives a Signal<Item = Option<Dom>> and wants to render it as a child, yet it needs to sprinkle in some additional attributes first (for example a slot attribute)

Since there's currently no way to do that, it means creating a wrapper, and then that can introduce awkward CSS constraints that need workarounds... for example:

.child(html!("div", {
    .style("width", "100%")
    .style("height", "100%")
    .attribute("slot", "sidebar")
    .child_signal(sidebar_signal())
}))

Solution

If Dom had a with_element() which accepts a FnOnce(&web_sys::Element), then it would be possible to completely remove the nesting:

.child_signal(sidebar_signal()
  .map(|dom| {
    if let Some(dom) = dom {
      dom.with_element(|el| {
        el.attribute("slot", "sidebar")
      })
    }
  })
)

Although probably outside the scope of Dominator itself, this would then allow for easy extensions to add children to slots.

dakom commented 3 years ago

Closing this after discussion on Discord. Basically, this would lead to all kinds of potential breakage and spaghetti code - instead, use a DomBuilder

It can still be the responsibility of the parent to set the slot by defining the child as a component which accepts a callback function before creating the Dom.

There are many ways to approach this, but here's one example from @Pauan :

fn create_my_dom<A, F>(f: F) -> Dom
    where F: FnOnce(DomBuilder<A>) -> DomBuilder<A> {
    html!("div", {
        // ... put attributes and whatever in here ...

        // This applies the closure as a mixin
        .apply(f)
    })
}

//called in parent
create_my_dom(move |dom| {
    dom.attribute("slot", "foo")
})
Pauan commented 3 years ago

Just a note that I prefer to use components (a struct + render method), so it would look like this:

pub struct MyStruct {
    ...
}

impl MyStruct {
    pub fn render<F>(this: Rc<Self>, mixin: F) -> Dom
        where F: FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> {

        html!("div", {
            ...

            .apply(mixin)
        })
    }
}

And then you would call it like this:

MyStruct::render(my_struct, move |dom| {
    dom.attribute("slot", "foo")
})

This "mixin style" is inverting the control so that the parent can manipulate the child's DomBuilder. I would be careful not to overuse it since it can make the control flow hard to understand, but it can be useful in some situations.

Note that (like everything in dominator) this is a zero cost abstraction, and in fact it has no runtime cost at all, since everything is inlined.


And this is how it would look with the original example:

.child_signal(sidebar_signal(move |dom| {
    dom.attribute("slot", "sidebar")
}))