Rust Syntax Extension
Consider this variable declaration:
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let element = rsx!{ h1(){"Hello, world!"} }; }
This funny tag syntax is neither a string nor Rust code - it is a ViewBuilder
.
The macro rsx!
is using RSX, which is a "Rust Syntax Extension".
Similarly there is a html!
macro that creates ViewBuilder
from an HTML-ish
syntax:
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let element = html!{ <h1>"Hello, world!"</h1> }; }
The two definitions are synonymous.
We recommend using these macros in mogwai to describe the DOM nodes used by your components. RSX cuts down on the amount of boilerplate you have to type when writing components. RSX may remind you of a template language, but it comes with the full power of Rust.
You can always write your components without RSX - here is the same example above written out manually:
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let my_builder: ViewBuilder = ViewBuilder::element("h1") .append(ViewBuilder::text("Hello, world!")); }
Tags
You may use any html tags you wish when writing RSX with mogwai_dom
.
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let _: ViewBuilder = html! { <p>"Once upon a time in a galaxy far, far away..."</p> }; }
Attributes
Adding attributes happens the way you expect it to.
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let _: ViewBuilder = html! { <p id="starwars">"Once upon a time in a galaxy far, far away..."</p> }; }
All html attributes are supported.
Attributes that have hyphens should be written with underscores.
Special Mogwai Attributes
Additionally there are some mogwai
specific attributes that do special things.
These are all denoted by two words separated by
a colon, with an expression on the right hand side. In most cases the right hand
side is allowed to be a String
, an &str
, an impl Stream<Item = String>
or a
tuple of a stringy type and a string stream. See MogwaiValue
for more details about types that can be turned into streams.
-
style:{name} =
impl Into<MogwaiValue<String or &str, Stream<Item = String>>
Declares a single style.
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let _ = html! { <a href="#burritos" style:border="1px dashed #333">"link"</a> }; }
-
on:{event} =
impl Sink<DomEvent>
Declares that the events of a certain type (
event
) occurring on the element should be sent on the given sender. You will often see the use of SinkExt::contra_map in this position to "prefix map" the type of theSender
.#![allow(unused)] fn main() { use mogwai_dom::prelude::*; use mogwai_dom::core::channel::{broadcast, ONE}; let (tx, _rx) = broadcast::bounded::<()>(ONE); let _ = html! { <div on:click=tx.contra_map(|_:DomEvent| ())>"Click me!"</div> }; }
-
window:{event} =
impl Sink<DomEvent>
Declares that the windows's matching events should be sent on the given sender.
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; use mogwai_dom::core::channel::{broadcast, ONE}; use mogwai_dom::prelude::*; let (tx, rx) = broadcast::bounded::<()>(ONE); let _ = html! { <div window:load=tx.contra_map(|_:DomEvent| ())>{("", rx.map(|()| "Loaded!".to_string()))}</div> }; }
-
document:{event} =
impl Sink<DomEvent>
Declares that the document's matching events should be sent on the given transmitter.
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; use mogwai_dom::core::channel::{broadcast, ONE}; let (tx, rx) = broadcast::bounded::<String>(ONE); let _ = html! { <div document:keyup=tx.contra_map(|ev: DomEvent| format!("{:#?}", ev))>{("waiting for first event", rx)}</div> }; }
-
boolean:{name} =
impl Into<MogwaiValue<bool, Stream<Item = bool>>
Declares a boolean attribute with the given name.
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let _ = html! { <input boolean:checked=true /> }; }
-
patch:children =
impl Stream<ListPatch<ViewBuilder>>
Declares that this element's children will be updated with a stream of ListPatch.
Note
ViewBuilder is not
Clone
. For this reason we cannot usemogwai::channel::broadcast::{Sender, Receiver}
channels to send patches, because a broadcast channel requires its messages to beClone
. Instead usemogwai::channel::mpsc::{Sender, Receiver}
channels, which have no such requirement. Just remember that even though theReceiver
can be cloned, if ampsc::Sender
has more than onempsc::Receiver
listening, only one will receive the message and the winningReceiver
alternates in round-robin style.let (tx, rx) = mpsc::bounded(1); let my_view = SsrDom::try_from(html! { <div id="main" patch:children=rx>"Waiting for a patch message..."</div> }) .unwrap(); my_view .executor .run(async { tx.send(ListPatch::drain()).await.unwrap(); // just as a sanity check we wait until the view has removed all child // nodes repeat_times(0.1, 10, || async { my_view.html_string().await == r#"<div id="main"></div>"# }) .await .unwrap(); let other_viewbuilder = html! { <h1>"Hello!"</h1> }; tx.send(ListPatch::push(other_viewbuilder)).await.unwrap(); // now wait until the view has been patched with the new child repeat_times(0.1, 10, || async { let html_string = my_view.html_string().await; html_string == r#"<div id="main"><h1>Hello!</h1></div>"# }) .await .unwrap(); }) .await;
-
post:build =
FnOnce(&mut T)
Used to apply one-off changes to the domain specific view
T
after it has been built. -
capture:view =
impl Sink<T>
Used to capture a clone of the view after it has been built. The view type
T
must beClone
. For more info see Capturing Views -
cast:type = Any domain specific inner view type, eg
Dom
Declares the inner type of the resulting ViewBuilder. By default this is Dom.
use mogwai_dom::prelude::*; let my_input: ViewBuilder<MyCustomInnerView> = html! { <input cast:type=MyCustomInnerView /> };
Expressions
Rust expressions can be used as the values of attributes and as child nodes.
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; let is_cool = true; let _ = html! { <div> { if !is_cool { "This is hot." } else { "This is cool." } } </div> }; }
Conditionally include DOM
Within a tag or at the top level of an RSX macro, anything inside literal brackets is interpreted and used
as Into<ViewBuilder<T>>
. Any type that can be converted into a ViewBuilder
can be used to construct a node including Option<impl Into<ViewBuilder<T>>
. When the value is None
,
an empty node is created.
Below we display a user's image if they have one:
let o_image: Option<ViewBuilder> = user
.o_image
.as_ref()
.map(|image| {
if image.is_empty() {
None
} else {
Some(html! { <img class="user-pic" src=image /> })
}
})
.flatten();
rsx! {
ul(class="nav navbar-nav pull-xs-right") {
li(class="nav-item") {
a(class=home_class, href="#/"){ " Home" }
}
li(class="nav-item") {
a(class=editor_class, href="#/editor") {
i(class="ion-compose") {
" New Post"
}
}
}
li(class="nav-item") {
a(class=settings_class, href="#/settings") {
i(class="ion-gear-a"){
" Settings"
}
}
}
li(class="nav-item") {
a(class=profile_class, href=format!("#/profile/{}", user.username)) {
{o_image}
{format!(" {}", user.username)}
}
}
}
}
Including fragments
You can use RSX to build more than one view at a time:
#![allow(unused)] fn main() { use mogwai_dom::prelude::*; // Create a vector with three builders in it. let builders: Vec<ViewBuilder> = html! { <div>"hello"</div> <div>"hola"</div> <div>"kia ora"</div> }; // Then add them all into a parent tag just like any component let parent: ViewBuilder = html! { <section>{builders}</section> }; }
Without RSX
It is possible and easy to create mogwai views without RSX by using the API provided by ViewBuilder.