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
- 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.
- Instead of
<element on={func} />
, events actually have names, so
<element on_foo={func} />
turns into
.on(mox_impl::events::on_foo(), func)
.
- 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
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)
})
)
)
})