Skip to content

Mox builder API RFC #87

Closed
Closed
@tiffany352

Description

@tiffany352

I want to propose a more explicitly defined interface that the mox macro
consumes. Some of this is based on my own local fork I'm using for
moxie-native.

Everything I'm talking about here is based on my own requirements for
moxie-native, and I'd like to know if any of these requirements are too
strict for other implementations, or if there are other ways of
improving this.

I'll start off with suggesting that as much as possible be moved into a
module named mox_impl, so that we don't pollute the namespace of
programs and require a blob import like use moxie_native::prelude::*;.

Inside of this module, there will be another namespace for host elements
(which we will distinguish from user components, as I'll detail below).
There will also be one for attribute names, and for event names.

API Sketch

None of the types used here are literal, but are just to name the return
values so you can match them up to where they get plumbed into.

mod mox_impl {
    mod elements {
        // Note that this is not a macro.
        fn element_name_here(
            with_builder: impl FnOnce(builder: Builder) -> Builder
        ) -> Node;
    }

    mod attributes {
        fn attr_name_here() -> Attribute;
    }

    mod events {
        fn on_event_name_here() -> Event;
    }
}

The builder API has these methods on it (again, the types are not actual types):

trait Builder {
    fn set_attr(self, key: Attribute, value: Attribute::Value) -> Self;
    fn on_event(self, event: Event, callback: impl FnMut(event)) -> Self;
    fn set_data(self, key: &'static str, value: DataValue) -> Self;
    fn add_child(self, child: Node) -> Self;
    fn enter_fragment(self, with_builder: impl FnOnce(builder: Self) -> Self) -> Self;
}

Changes to syntax

  1. User components need to be written in a way that lets them be
    differentiated from host components. This is already true of
    components that take arguments, since you have to use the <element _=(...)> syntax. Zero argument components need to be written
    differently from <element /> though. <element _=() /> would work
    if it didn't fail to parse.
  2. Instead of <element on={func} />, events actually have names, so
    <element on_foo={func} /> turns into
    .on(mox_impl::events::on_foo(), func).
  3. Add a concept of data attributes (not currently present) which are
    not statically declared. <element data_foo=4 /> -> .data("foo", 4)

Implementation

Implementation here is pretty straightforward, the biggest unknown is if
this new scheme will be difficult for other users of mox.

Simple implementations can define attribute functions as just a function
that returns a string, but this might be more manual than desired for
something like moxie-dom.

Some implementation hints from moxie-native that can come in handy here:

  • The builder trait can be implemented per element with a Builder<T>
    generic. The element functions can create an instance of this builder
    and pass it to the input function.
  • set_attr can be implemented using a trait like this:
    trait HasAttribute<Attr> where Attr: Attribute {
        fn set_attribute(&mut self, value: Attr::Value);
    }
  • on_event can be implemented the same way using a HasEvent trait.
  • The element function and the enter_fragment method are both good
    places to insert topo calls.

Examples

Simple with attributes

<foo some_attr=4 />
mox_impl::elements::foo(|builder| {
    builder
    .set_attr(mox_impl::attributes::some_attr(), 4)
})

User components

<bar>
    <user_component _=(1, "foo") />
</bar>
mox_impl::elements::bar(|builder| {
    builder
    .add_child(user_component!(1, "foo"))
})

Events and data attributes

<button on_click={func} data_foo=4 />
mox_impl::elements::button(|builder| {
    builder
    .on_event(mox_impl::events::on_click(), func)
    .data("foo", 4)
})

Children

<foo>
    <bar />
    "text"
    {children}
</foo>
mox_impl::elements::foo(|builder| {
    builder
    .add_child(mox_impl::elements::bar(|builder| builder))
    .add_child("text")
    .add_child(children)
})

Fragments

<foo>
    <>
        <fragment_child />
    <>
</foo>
mox_impl::elements::foo(|builder| {
    builder
    .enter_fragment(|builder| {
        builder
        .add_child(mox_impl::elements::fragment_child(|builder| builder))
    })
})

Bare fragments

Behavior here isn't super clear, but adding a create_fragment function
to mox_impl to handle this case seems flexible enough.

<>
    <fragment_child />
</>
mox_impl::create_fragment(|builder|
    builder
    .add_child(mox_impl::elements::fragment_child(|builder| builder))
)

Iterators

This one is tricky to support, and I currently can't handle it correctly
in moxie-native without trait specialization. I haven't thought of any
workarounds that don't require modifying the syntax to be like
..{iterator} or something like that. Any ideas here would be nice.

<foo>
    {[1, 2, 3].iter().map(|index| mox!(
        <number value={index} />
    ))}
</foo>
mox_impl::elements::foo(|builder| {
    builder
    .add_child(
        [1, 2, 3].iter().map(|index| 
            mox_impl::elements::number(|builder| {
                builder
                .set_attr(mox_impl::attributes::value(), index)
            })
        )
    )
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions