Open dakom opened 5 years ago
Document
can be stored somewhere, or pulled at any time from web_sys
. Two approaches for interop are what you described: passing web_sys
node/element trees directly, (straightfwd), and interfacing generically through an API, ie for virtual DOMs. The latter approach requires a yet-undeveloped standard interface.
The nature of any specific element-creation API will be opinionated: I think the way around this is for Gloo
to provide multiple ones. There are three broad styles I've encountered:
1: Html or JSX-like: Resembles HTML through heavy use of macros. Seems to be the most popular.
2: Pure Rust: eg Sauron
. Easy to reason about from looking at the syntax, and for IDEs etc to assist with, but may be verbose.
3: Hybrid: Minimally wrap Rust functions. Your suggestion fits here, as does the one I designed for Seed
. I set up macros that are similar to functions, but take an arbitrary number of parameters, and differentiate them based on type. Types can be children, Vecs of children, events, Vecs of events, thinly-wrapped style or attributes HashMap
s, text, or lifecycle hooks.
Nice! Wasn't aware of Seed either, added it to "Prior Art" :)
Anyone have thoughts on how to implement a generic API these element-creation APIs could tie into? Maybe with a trait? I have a non-working Element trait I can post for inspiration if anyone would like.
I'm also curious if anyone has thoughts on an API they'd like... no limits. Something new, or a modification to something existing.
Examples, from todomvc:
Yew (HTML-like, powerful use of macros to create arbitrary syntax):
html! {
<header class="header",>
<h1>{ "todos" }</h1>
<input class="new-todo",
placeholder="What needs to be done?",
value=&self.state.value,
oninput=|e| Msg::Update(e.value),
onkeypress=|e| {
if e.key() == "Enter" { Msg::Add } else { Msg::Nope }
}, />
</header>
}
Sauron (pure Rust):
header(
[class("header")],
[
h1([], [text("todos")]),
input(
[
class("new-todo"),
placeholder("What needs to be done?"),
value(self.value.to_string()),
oninput(|v: InputEvent| Msg::Update(v.value)),
onkeypress(|event: KeyEvent| {
if event.key == "Enter" { Msg::Add } else { Msg::Nope }
}),
],
[],
)),
],
)
Seed (hybrid - arguments inside macros are normal Rust, but optional and in arbitrary order, using Rust's type system):
header![ class!["header"],
h1!["todos"],
input![
attrs! {
At::Class => "new-todo";
At::PlaceHolder => "What needs to be done?";
At::Value => model.entry_text;
},
raw_ev(Ev::KeyDown, Msg::New),
input_ev(Ev::Input, Msg::Update),
]
],
Sorry for the delay on this. I tend to agree with @David-OConnor that some sort of unified interface seems more appropriate right now.
The reason I say that is because there's so many different syntaxes for an HTML macro, with different trade-offs. For example, the above would look like this with dominator:
html!("header", {
.class("header")
.children(&mut [
html!("h1", {
.text("todos")
}),
html!("input", {
.class("new-todo")
.attribute("placeholder", "What needs to be done?")
.property_signal("value", this.value.signal_cloned())
.event(clone!(this => move |e: InputEvent| this.on_input(e.value())))
.event(clone!(this => move |e: KeyPressEvent| {
if e.key() == "Enter" { this.add(); } else { this.nope(); }
}))
}),
])
})
There's many good reasons for why I chose this style for dominator, but I don't think it generalizes to other DOM frameworks (and vice versa).
Even if we ignore vdom and just focus on creating raw HTML, there's still a lot of different syntax choices.
But if we had a unified interface which all of the syntaxes expanded to, then people could pick and choose which syntax they like. That's why I think we should be focusing on the interface first.
Anyone have thoughts on how to implement a generic API these element-creation APIs could tie into?
This is pretty much just dumle or typed-html. For instance, dumle aims to make VDOM instantiation zero-cost, so with a trait that converts the types of dumle into the set of types of some other library, or renders DOM nodes, one could use dumle's Yew inspired html!
macro and it would turn into the correct thing.
One thing I've come to dislike about macros is the lack of rustfmt support. Usually this isn't such a big deal, but I've been using typed-html
for a while, and when doing UI work, a lot of HTML gets moved around, inside divs, outside of divs, etc, and the formatting just ends up all over the place.
What would help here is there's some kind of way to define the html outside of the rust files, into template files which an editor can format using any popular formatting tools such as prettier
, and then load in those files at compile time.
This still isn't great though, as you want to be able to mix the HTML with Rust-specific branching logic (keeping it to a minimum of course, but there'll always be some logic involved).
I guess one solution could be to have some kind of compile-time transformation of Handlebars-like templating in your external html files, that get translated to regular Rust code, and inserted into your code.
But, that's getting a bit off-topic. The main point I wanted to make is that I started out really liking typed-html
, and I still do like it, but I dislike the lack of formatting within macros.
Separate template files would be a nice addition as well. Some web devs (eg Vue users) prefer it over integration with code.
What's the status of this discussion? Is there an implementation of any of these ideas? I was super excited to find gloo just now as I was hoping that someone had implemented a html!() like macro ... but it seems like it's not here yet? I implemented my own ... which is fairly fugly but had dramatically reduced my typing. I assumed it was so crap that it should be thrown away once I found a better implementation, but a quick look through gloo didn't show one. Thoughts?
FYI:
/**
* Modestly helpful HTML macro that will create an element and set all of the attributes
*
* /// let div = html!("div", {
* /// "name" => "root_div",
* /// "id" => "container",
* /// /* .... more attributes*/
* /// });
* /// assert_eq!(div.get_attribute("name"), Some("root_div");
* ///
*
* Alternatively, can also append children at the end
* /// let list = html!("ui", {"id = "mylist"},
* /// html!("li", {"inner_html" => "item1"}),
* /// html!("li", {"inner_html" => "item2"}),
* /// html!("li", {"inner_html" => "item3"}),
* /// );
* /// assert_eq!(list.children().lenght(), 3);
*
* A smarter person than I would have been able to simplify this into a single case...
*/
#[macro_export]
macro_rules! html {
// html!("div", {"key"=>"value", ...})
($e:expr) => {
{
(|| -> Result<web_sys::Element, wasm_bindgen::JsValue> {
let d = web_sys::window().expect("window").document().expect("document");
let element = d.create_element($e)?;
Ok(element)
})()
}
};
($e:expr, {$( $k:expr => $v:expr ),* $(,)?}) => {
{
(|| -> Result<web_sys::Element, wasm_bindgen::JsValue> {
let d = web_sys::window().expect("window").document().expect("document");
let element = d.create_element($e)?;
$(
element.set_attribute($k,$v)?;
)*
Ok(element)
})()
}
};
// html!("div", {"key"=>"value", ...}, child1, child2, ...)
($e:expr, {$( $k:expr => $v:expr ),* $(,)?}, $($c:expr),*) => {
{
(|| -> Result<web_sys::Element, wasm_bindgen::JsValue> {
let d = web_sys::window().expect("window").document().expect("document");
let element = d.create_element($e)?;
$(
element.set_attribute($k,$v)?;
)*
$(
element.append_child($c)?;
)*
Ok(element)
})()
}
};
}
I don't believe gloo is the right place for such a macro (open to be convinced otherwise though). It feels a lot like a web framework/library job to do it. There are crates that can build html trees. I believe any of them can be modified to convert to HtmlElement
instead of their own virtual DOM.
Yew's html!
macro can generate HTML for you. It should be fairly simple to build wrapper around yew's VNode
that ignores everything but VText and VTag and converts those two to web_sys::HtmlElement
I have to say, that answer really confuses me. From the motivation in the main README.md :
In particular I’d love to see a modular effort towards implementing a virtual DOM library with JSX like syntax. There have been several efforts on this front but all have seemed relatively monolithic and “batteries included”. I hope this will change in 2019.
— Ryan Levick in [Rust WebAssembly 2019](https://blog.ryanlevick.com/posts/rust-wasm-2019/)
Part of the reason why I got excited about gloo is it is less opinionated than Yew/Leptos/etc. about how to run your application. All of those other projects make fairly heavy assumptions about what you're doing and how you should do them. Maybe they're good assumptions, maybe they're not, but I do think there's room for a light-weight no-hassle JSX macro that just automates away the document.create_element(...), .set_attribute(), .set_inner_html(), .append_child() dance that's inherent to writing front-ends in Rust now.
But I guess implicit in your answer is two things:
1) This doesn't exist in gloo now 2) To get there would require at least some amount of code changes/code copying from another project that already does this "and more", if someone (like me) didn't want the "and more".
Does that sound correct? Thanks for the reply!
Thoughts?
More so than that, there's no agreeable public API: https://github.com/yewstack/yew/discussions/2941
Gloo, as you mentioned, is not opinionated. I'm not sure about providing a specific DSL (even with if it's HTML) where there are multiple ways to do the same thing, each with their own pros and cons. There have been cases of merging code from external crates to gloo, so perhaps this could be one such case? See https://github.com/yewstack/yew/issues/1841 and https://github.com/rustwasm/gloo/pull/180
To be clear, I'm not entirely against it. I just would like to avoid adding an API and then changing it entirely, forcing users to completely rewrite their code.
I hear you about lots of options - but right now, AFAICT, there's none that only write to the DOM via web_sys, so my inclination is to write it, get comments/see if people like it, and iterate.
How about this for next steps: I'll let more people comment and I'll try to put up a pull request and we'll see which one happens first :-)
If you want to work on it, I suggest looking at the syn-rsx crate. It implements a lot of the parsing for you
@capveg-netdebug AFAICT, there's none that only write to the DOM via web_sys, so my inclination is to write it, get comments/see if people like it, and iterate.
dominator compiles down to raw web-sys calls, it doesn't use a vdom, so it's currently the closest framework to what you want.
dominator is an FRP framework, so it does a lot more than just generate DOM nodes, but you can use it to just generate static DOM nodes if you want to.
If you just need to generate static DOM nodes, then it's pretty easy to create your own html!
macro, or you can use the typed-html
crate, which can output either a String or VDom.
But as @hamza1311 said, you will have to make design choices and tradeoffs. There are many different ways to create an html!
macro, there isn't a single obviously-correct design.
Also, personally, I don't think it's particularly useful to have an html!
macro that just generates static DOM nodes. Generating static DOM is easy, it's the dynamic changing DOM that is hard. And that's where frameworks like Yew / MoonZoon / dominator are useful, because they handle dynamic DOM.
Summary
Macro to simplify creating DOM elements
Motivation
web-sys often requires multiple steps and type conversions to accomplish the same results as typical html.
For example:
<a href="/foo"><div>Foo</div></a>
currently requires something like this:Detailed Explanation
From a consumers point of view, the above could be simplified as:
The concept is following the same idea as React's createElement() wherein the arguments are:
Children must be another element, or raw text, or slices of those.
Macros can be used to allow for shorthand and remove the need for wrapping things in
Some()
, in other words all of these are legal and mean the same thing:Same with these:
And multiple children could be wrapped in brackets:
Prior Art
The concept of the above is exactly the same as React createElement, only instead of creating proprietary elements it creates real elements
There are a few known projects that use a macro to ultimately create html elements as well - either through a vDom or directly:
Drawbacks, Rationale, and Alternatives
Not sure - I'm opening this issue more for discussion than a direct proposal :) Feel free to shoot it down!
Unresolved Questions
Where does
Document
come from?How to interop with other crates?
For interop, I think as long as the macro returns native HtmlElements I'd imagine that will flow organically?
Specifically - how do we attach an event listener to nested children? On one foot it seems that this is doable if we break it apart: