Cooking with Mogwai

This book contains explanations and examples of solving common user interface problems using the mogwai rust library. It is a work in progress.

Any requests, questions, or improvements may be logged in the issue tracker.

See the library documentation for an introduction to the library in general.

Visit us at the support channel.

Happy hacking!

🦀

Starting a new project

First you'll need a new(ish) version of the rust toolchain. For that you can visit https://rustup.rs/ and follow the installation instructions.

Then you'll need wasm-pack.

The easiest way to get hacking is to use the wonderful cargo-generate, which can be installed using cargo install cargo-generate.

Then run

cargo generate --git https://github.com/schell/mogwai-template.git

and give the command line a project name. Then cd into your sparkling new project and

wasm-pack build --target web

Then, if you don't already have it, cargo install basic-http-server or use your favorite alternative to serve your app:

basic-http-server -a 127.0.0.1:8888

Rust Syntax Extension

Consider this variable declaration:


#![allow(unused)]
fn main() {
use mogwai::prelude::*;
let element = builder!{ <h1>"Hello, world!"</h1> };
}

This funny tag syntax is neither a string nor HTML - it is a ViewBuilder<HtmlElement>.

The macro builder! is using RSX, which is a "Rust Syntax Extension". Similarly there is a view! macro that creates View<HtmlElement>.


#![allow(unused)]
fn main() {
use mogwai::prelude::*;
let my_builder: ViewBuilder<HtmlElement> = builder!{ <h1>"Hello, world!"</h1> };
let my_view: View<HtmlElement> = view!{ <h1>"Hello, world!"</h1> };

let my_identical_view: View<HtmlElement> = View::from(my_builder);
}

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.

Tags

You may use any tags you wish when writing RSX.

# use mogwai::prelude::*;
builder! {
    <p>"Once upon a time in a galaxy far, far away..."</p>
}

Attributes

Adding attributes happens the way you expect it to.

# use mogwai::prelude::*;
builder! {
    <p id="starwars">"Once upon a time in a galaxy far, far away..."</p>
}

All html attributes are supported.

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.

  • style:{name} = {expr: Into<Effect<String>>}

    Declares a single style.

    
    #![allow(unused)]
    fn main() {
    use mogwai::prelude::*;
    let _ = builder! {
        <a href="#burritos" style:border="1px dashed #333">"link"</a>
    };
    }
    
  • on:{event} = {tx: &Transmitter<Event>}

    Declares that the element's matching events should be sent on the given transmitter.

    
    #![allow(unused)]
    fn main() {
    use mogwai::prelude::*;
    let (tx, _rx) = txrx::<()>();
    let _ = builder! {
        <div on:click=tx.contra_map(|_:&Event| ())>"Click me!"</div>
    };
    }
    
  • window:{event} = = {tx: &Transmitter<Event>}

    Declares that the windows's matching events should be sent on the given transmitter.

    
    #![allow(unused)]
    fn main() {
    use mogwai::prelude::*;
    let (tx, rx) = txrx::<()>();
    let _ = builder! {
        <div window:load=tx.contra_map(|_:&Event| ())>{rx.branch_map(|_:&()| "Loaded!".to_string())}</div>
    };
    }
    
  • document:{event} = = {tx: &Transmitter<Event>}

    Declares that the document's matching events should be sent on the given transmitter.

    
    #![allow(unused)]
    fn main() {
    use mogwai::prelude::*;
    let (tx, rx) = txrx::<Event>();
    let _ = builder! {
        <div document:keyup=tx>{rx.branch_map(|ev| format!("{:#?}", ev))}</div>
    };
    }
    
  • boolean:{name} = {expr: Into<Effect<bool>}

    Declares a boolean attribute with the given name.

    
    #![allow(unused)]
    fn main() {
    use mogwai::prelude::*;
    let _ = builder! {
        <input boolean:checked=true />
    };
    }
    
  • patch:children = {expr: Receiver<Patch<View<_>>>}

    Declares that this element's children will be updated with Patch messages received on the given Receiver.

    
    #![allow(unused)]
    fn main() {
    use mogwai::prelude::*;
    let (tx, rx) = txrx();
    let my_view = view! {
        <div id="main" patch:children=rx>"Waiting for a patch message..."</div>
    };
    tx.send(&Patch::RemoveAll);
    
    let other_view = view! {
        <h1>"Hello!"</h1>
    };
    tx.send(&Patch::PushBack { value: other_view });
    
    assert_eq!(my_view.html_string(), r#"<div id="main"><h1>Hello!</h1></div>"#);
    }
    
  • cast:type = web_sys::{type}

    Declares that this element's underlying DomNode is the given type.

    
    #![allow(unused)]
    fn main() {
    use mogwai::prelude::*;
    let my_input: ViewBuilder<web_sys::HtmlInputElement> = builder! {
          <input cast:type=web_sys::HtmlInputElement />
    };
    }
    

Transmitters, Receivers and Effects

Transmitters and Receivers are the key to reactivity in mogwai.

Transmitters

Transmitters are given to a View so that View can transmit DOM events out to any Receivers. In most cases the Receiver will be wired to the Component::update function. In turn, this function can send messages back out to that same View's Receivers.

Receivers

Receivers are responsible for the majority of DOM updates. It's possible to patch the DOM by hand in the Component::update function but in most cases it's not necessary. Receivers can be used as attribute values and child nodes. When a message is received, that attribute will update its value, or that child node will be replaced with the new message value.

Effects

An Effect is either a value now or some values later, or both. We can declare text nodes and attributes to have a value now and some values later using anything that can be converted into an Effect. In most cases we'll use a tuple:


#![allow(unused)]
fn main() {
use mogwai::prelude::*;
let (tx, rx_later) = txrx();
let _ = builder! {
    <div>
      <p>{("Value now!", rx_later.clone())}</p>
      <p>{"Only a value now!"}</p>
      <p>{rx_later}</p>
    </div>
};
tx.send(&"Value later".to_string());
}

Expressions

Rust expressions can be used as the values of attributes and as child nodes.


#![allow(unused)]
fn main() {
use mogwai::prelude::*;
let is_cool = true;
let _ = builder! {
    <div>
        {
            if !is_cool {
                "This is hot."
            } else {
                "This is cool."
            }
        }
    </div>
};
}

Casting the inner DOM element

You can cast the inner DOM element of a View or ViewBuilder using the special attribute cast:type:


#![allow(unused)]
fn main() {
use mogwai::prelude::*;
use web_sys::HtmlInputElement;

let name_input: View<HtmlInputElement> = view! {
    <input type="text" placeholder="Your Name" cast:type=web_sys::HtmlInputElement />
};
}

Without this explicit casting all DOM nodes assume the type HtmlElement;

Conditionally include DOM


#![allow(unused)]
fn main() {
use mogwai::prelude::*;

struct User {
    username: String,
    o_image: Option<String>
}

fn signed_in_view_builder(
    user: &User,
    home_class: Effect<String>,
    editor_class: Effect<String>,
    settings_class: Effect<String>,
    profile_class: Effect<String>,
) -> ViewBuilder<HtmlElement> {
    let o_image: Option<ViewBuilder<HtmlElement>> = user
        .o_image
        .as_ref()
        .map(|image| {
            if image.is_empty() {
                None
            } else {
                Some(builder! { <img class="user-pic" src=image /> })
            }
        })
        .flatten();

    builder! {
        <ul class="nav navbar-nav pull-xs-right">
            <li class="nav-item">
                <a class=home_class href="#/">" Home"</a>
            </li>
            <li class="nav-item">
            <a class=editor_class href="#/editor">
                <i class="ion-compose"></i>
                " New Post"
                </a>
            </li>
            <li class="nav-item">
            <a class=settings_class href="#/settings">
                <i class="ion-gear-a"></i>
                " Settings"
                </a>
            </li>
            <li class="nav-item">
                <a class=profile_class href=format!("#/profile/{}", user.username)>
                    {o_image}
                    {format!(" {}", user.username)}
                </a>
            </li>
        </ul>
    }
}
}

Without RSX

Here is the definition of signed_in_user above, written without RSX:


#![allow(unused)]
fn main() {
use mogwai::prelude::*;

struct User {
    username: String,
    o_image: Option<String>
}

fn signed_in_view_builder(
    user: &User,
    home_class: Effect<String>,
    editor_class: Effect<String>,
    settings_class: Effect<String>,
    profile_class: Effect<String>,
) -> ViewBuilder<HtmlElement> {
    let o_image: Option<ViewBuilder<HtmlElement>> = user
        .o_image
        .as_ref()
        .map(|image| {
            if image.is_empty() {
                None
            } else {
                Some({
                    let mut __mogwai_node = (ViewBuilder::element("img")
                        as ViewBuilder<web_sys::HtmlElement>);
                    __mogwai_node.attribute("class", "user-pic");
                    __mogwai_node.attribute("src", image);
                    __mogwai_node
                })
            }
        })
        .flatten();
    {
        let mut __mogwai_node = (ViewBuilder::element("ul")
            as ViewBuilder<web_sys::HtmlElement>);
        __mogwai_node.attribute("class", "nav navbar-nav pull-xs-right");
        __mogwai_node.with({
            let mut __mogwai_node = (ViewBuilder::element("li")
                as ViewBuilder<web_sys::HtmlElement>);
            __mogwai_node.attribute("class", "nav-item");
            __mogwai_node.with({
                let mut __mogwai_node = (ViewBuilder::element("a")
                    as ViewBuilder<web_sys::HtmlElement>);
                __mogwai_node.attribute("class", home_class);
                __mogwai_node.attribute("href", "#/");
                __mogwai_node.with(ViewBuilder::from(" Home"));
                __mogwai_node
            });
            __mogwai_node
        });
        __mogwai_node.with({
            let mut __mogwai_node = (ViewBuilder::element("li")
                as ViewBuilder<web_sys::HtmlElement>);
            __mogwai_node.attribute("class", "nav-item");
            __mogwai_node.with({
                let mut __mogwai_node = (ViewBuilder::element("a")
                    as ViewBuilder<web_sys::HtmlElement>);
                __mogwai_node.attribute("class", editor_class);
                __mogwai_node.attribute("href", "#/editor");
                __mogwai_node.with({
                    let mut __mogwai_node = (ViewBuilder::element("i")
                        as ViewBuilder<web_sys::HtmlElement>);
                    __mogwai_node.attribute("class", "ion-compose");
                    __mogwai_node
                });
                __mogwai_node.with(ViewBuilder::from(" New Post"));
                __mogwai_node
            });
            __mogwai_node
        });
        __mogwai_node.with({
            let mut __mogwai_node = (ViewBuilder::element("li")
                as ViewBuilder<web_sys::HtmlElement>);
            __mogwai_node.attribute("class", "nav-item");
            __mogwai_node.with({
                let mut __mogwai_node = (ViewBuilder::element("a")
                    as ViewBuilder<web_sys::HtmlElement>);
                __mogwai_node.attribute("class", settings_class);
                __mogwai_node.attribute("href", "#/settings");
                __mogwai_node.with({
                    let mut __mogwai_node = (ViewBuilder::element("i")
                        as ViewBuilder<web_sys::HtmlElement>);
                    __mogwai_node.attribute("class", "ion-gear-a");
                    __mogwai_node
                });
                __mogwai_node.with(ViewBuilder::from(" Settings"));
                __mogwai_node
            });
            __mogwai_node
        });
        __mogwai_node.with({
            let mut __mogwai_node = (ViewBuilder::element("li")
                as ViewBuilder<web_sys::HtmlElement>);
            __mogwai_node.attribute("class", "nav-item");
            __mogwai_node.with({
                let mut __mogwai_node = (ViewBuilder::element("a")
                    as ViewBuilder<web_sys::HtmlElement>);
                __mogwai_node.attribute("class", profile_class);
                __mogwai_node.attribute("href", format!("#/profile/{}", user.username));
                __mogwai_node.with(ViewBuilder::try_from({ o_image }).ok());
                __mogwai_node.with(
                    ViewBuilder::try_from(format!(" {}", user.username))
                    .ok(),
                );
                __mogwai_node
            });
            __mogwai_node
        });
        __mogwai_node
    }
}
}

Components

The Component trait allows rust types to be used as a node in the DOM. These nodes are loosely referred to as components.

Discussion

A type that implements Component is the state of a user interface element - for example we might use the following struct to express a component that keeps track of how many clicks a user has performed on a button.


#![allow(unused)]
fn main() {
struct ButtonCounter {
    clicks: u32
}
}

or current_contents: String to store what a user has entered into an input field. Another common pattern is to use a field like items: Gizmo<String> to be able to communicate with a list of subcopmonents (we'll go into more detail on that in the subcomponents chapter).

There are two basic concepts you must understand about components:

  1. State As described above, a component probably contains some stateful information which determines how that component reacts to input messages (also known as your component's Self::ModelMsg type). In turn this determines when and if the component sends output messages (also known as your component's Self::ViewMsg) to its view.
  2. View A component describes the structure of its DOM, most likely using RSX. It also describes how messages received may change that view and when or if the view sends input messages to update the component's state.

To these ends there are two Component methods that you must implement:

  1. Component::update This function is your component's logic. It receives an input message msg: &Self::ModelMsg from the view or another component and can update the component state using &mut self. Within this function you can send messages out to the view using the provided tx: &Transmitter<Self::ViewMsg>. When the view receives these messages it will patch the DOM according to the Component::view function.

    Also provided is a subscriber sub: &Subscriber which we'll talk more about in the subcomponents chapter. At this point it is good to note that with a Subscriber you can send async messages to self - essentially calling self.update when the async block yields. This is great for sending off requests, for example, and then triggering a state update with the response.

  2. Component::view This function uses a reference to the current state (in the form of &self) to return its DOM representation: a ViewBuilder. With the tx: &Transmitter<Self::ModelMsg> the returned ViewBuilder can send DOM events as messages to update the component state. With the rx: &Receiver<Self::ViewMsg> the return ViewBuilder can receive messages from the component and patch the DOM accordingly.

    It should be noted that this function is typically run only once during view construction.

    For folks coming from other libraries (ahem, React) this looks very much like the render function but that's not how mogwai works. The Component::view function is like a "setup" function that describes initial state (the RSX) and how it will update (via Receivers). -- bryanjswift

Example

extern crate mogwai;

use mogwai::prelude::*;

#[derive(Clone)]
enum In {
  Click
}

#[derive(Clone)]
enum Out {
  DrawClicks(i32)
}

struct App {
  num_clicks: i32
}

impl Component for App {
  type ModelMsg = In;
  type ViewMsg = Out;
  type DomNode = HtmlElement;

  fn update(&mut self, msg: &In, tx_view: &Transmitter<Out>, _sub: &Subscriber<In>) {
    match msg {
      In::Click => {
        self.num_clicks += 1;
        tx_view.send(&Out::DrawClicks(self.num_clicks));
      }
    }
  }

  fn view(&self, tx: &Transmitter<In>, rx: &Receiver<Out>) -> ViewBuilder<HtmlElement> {
      builder!{
          <button on:click=tx.contra_map(|_| In::Click)>
          {(
              "clicks = 0",
              rx.branch_map(|msg| {
                  match msg {
                      Out::DrawClicks(n) => {
                          format!("clicks = {}", n)
                      }
                  }
              })
          )}
          </button>
      }
  }
}


fn main() {
    let gizmo: Gizmo<App> = Gizmo::from(App{ num_clicks: 0 });
    let view = View::from(gizmo.view_builder());

    gizmo.send(&In::Click);
    gizmo.send(&In::Click);
    gizmo.send(&In::Click);

    println!("{}", view.html_string());

    // In wasm32 we can add the view to the window.body
    if cfg!(target_arch = "wasm32") {
        view.run().unwrap()
    }
}

Nesting components

A type implementing Component is a node in a user interface graph. This type will naturally contain other types that represent other nodes in the graph. maintaining a Gizmo in your component. Then spawn a builder from that sub-component field in your component's Component::view function to add the sub-component's view to your component's DOM.

extern crate mogwai;
extern crate web_sys;

use mogwai::prelude::*;

#[derive(Clone)]
enum CounterIn {
    Click,
    Reset
}

#[derive(Clone)]
enum CounterOut {
    DrawClicks(i32)
}

struct Counter {
    num_clicks: i32,
}

impl Component for Counter {
    type ModelMsg = CounterIn;
    type ViewMsg = CounterOut;
    type DomNode = HtmlElement;

    fn view(&self, tx: &Transmitter<CounterIn>, rx: &Receiver<CounterOut>) -> ViewBuilder<HtmlElement> {
        builder!{
            <button on:click=tx.contra_map(|_| CounterIn::Click)>
            {(
                "clicks = 0",
                rx.branch_map(|msg| {
                    match msg {
                        CounterOut::DrawClicks(n) => {
                            format!("clicks = {}", n)
                        }
                    }
                })
            )}
            </button>
        }
    }

    fn update(&mut self, msg: &CounterIn, tx_view: &Transmitter<CounterOut>, _sub: &Subscriber<CounterIn>) {
        match msg {
            CounterIn::Click => {
                self.num_clicks += 1;
                tx_view.send(&CounterOut::DrawClicks(self.num_clicks));
            }
            CounterIn::Reset => {
                self.num_clicks = 0;
                tx_view.send(&CounterOut::DrawClicks(0));
            }
        }
    }
}


#[derive(Clone)]
enum In {
    Click
}


#[derive(Clone)]
enum Out {}


struct App {
    counter: Gizmo<Counter>
}


impl Default for App {
    fn default() -> Self {
        let counter: Gizmo<Counter> = Gizmo::from(Counter { num_clicks: 0 });
        App{ counter }
    }
}


impl Component for App {
    type ModelMsg = In;
    type ViewMsg = Out;
    type DomNode = HtmlElement;

    fn view(&self, tx: &Transmitter<In>, rx: &Receiver<Out>) -> ViewBuilder<HtmlElement> {
        builder!{
            <div>
                {self.counter.view_builder()}
                <button on:click=tx.contra_map(|_| In::Click)>"Click to reset"</button>
            </div>
        }
    }

    fn update(&mut self, msg: &In, tx_view: &Transmitter<Out>, _sub: &Subscriber<In>) {
        match msg {
            In::Click => {
                self.counter.send(&CounterIn::Reset);
            }
        }
    }
}


pub fn main() -> Result<(), JsValue> {
    let view = View::from(Gizmo::from(App::default()));
    if cfg!(target_arch = "wasm32") {
        view.run()
    } else {
        Ok(())
    }
}

A List of Components

A list of components is a common interface pattern. In mogwai we can express this using two component definitions. In this example we'll use a List component and an Item component.

Contents

Explanation

The List defines its view to include a button to create new items and a ul to hold each item. Each item will have a unique id that will help us determine which items to remove. This id isn't strictly neccessary but it my experience it's a foolproof way to maintain a list of items with frequent splices.

Each item contains a button to remove the item from the list. Click events on this button must be bound to the parent since it is the job of the parent to patch its child views. Each item will also maintain a count of clicks - just for fun.

Both List and Item must define their own model and view messages - ListIn, ListOut, ItemIn and ItemOut, respectively. These messages encode all the interaction between the user/operator, the List and each Item.

When the operator clicks an item's remove button the item's view sends an ItemIn::Remove message. The item's Component::update function then sends an ItemOut::Remove(item.id) message - which is bound to its parent List and mapped to ListIn::Remove(id). This triggers the parent's Component::update function, which will search for the item that triggered the event, remove it from its items and also send an ItemOut::PatchItem(...) patch message to remove the item's view from the list's view.

This is a good example of how mogwai separates component state from component views. The item gizmos don't own the view - the window does! Using the item gizmos we can communicate to the item views. Conversly the item views will communicate with our item gizmos, which will trickle up into the parent.

Notes

In this example you can see that unlike other vdom based libraries, mogwai's state is completely separate from its views. Indeed they even require separate handling to "keep them in sync". This is by design. In general a mogwai app tends to be more explicit and less magical than its vdom counterparts.

Code

#![allow(unused_braces)]
use log::Level;
use mogwai::prelude::*;
use std::panic;
use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

/// One item - keeps track of clicks.
struct Item {
    id: usize,
    clicks: u32,
}

/// An item's model messages.
#[derive(Clone)]
enum ItemIn {
    /// The user clicked
    Click,
    /// The user requested this item be removed
    Remove,
}

/// An item's view messages.
#[derive(Clone)]
enum ItemOut {
    /// Change the number of clicks displayed in the view
    Clicked(u32),
    /// Remove the item from the parent view
    Remove(usize)
}

impl Component for Item {
    type ModelMsg = ItemIn;
    type ViewMsg = ItemOut;
    type DomNode = HtmlElement;

    fn update(
        &mut self,
        msg: &Self::ModelMsg,
        tx: &Transmitter<Self::ViewMsg>,
        _sub: &Subscriber<Self::ModelMsg>,
    ) {
        match msg {
            ItemIn::Click => {
                self.clicks += 1;
                tx.send(&ItemOut::Clicked(self.clicks))
            }
            ItemIn::Remove => {
                tx.send(&ItemOut::Remove(self.id));
            }
        }
    }

    fn view(
        &self,
        tx: &Transmitter<Self::ModelMsg>,
        rx: &Receiver<Self::ViewMsg>,
    ) -> ViewBuilder<Self::DomNode> {
        let clicks_to_string = |clicks| match clicks {
            1 => "1 click".to_string(),
            n => format!("{} clicks", n),
        };
        builder! {
            <li>
                <button style:cursor="pointer" on:click=tx.contra_map(|_| ItemIn::Click)>"Increment"</button>
                <button style:cursor="pointer" on:click=tx.contra_map(|_| ItemIn::Remove)>"Remove"</button>
                " "
                <span>
                {(
                    clicks_to_string(self.clicks),
                    rx.branch_filter_map(move |msg| match msg {
                        ItemOut::Clicked(clicks) => Some(clicks_to_string(*clicks)),
                        _ => None
                    })
                )}
                </span>
            </li>
        }
    }
}

struct List {
    next_id: usize,
    items: Vec<Gizmo<Item>>,
}

#[derive(Clone)]
enum ListIn {
    /// Create a new item
    NewItem,
    /// Remove the item at the given index
    RemoveItem(usize),
}

#[derive(Clone)]
enum ListOut {
    /// Patch the list of items
    PatchItem(Patch<View<HtmlElement>>),
}

impl Component for List {
    type ModelMsg = ListIn;
    type ViewMsg = ListOut;
    type DomNode = HtmlElement;

    fn update(&mut self, msg: &ListIn, tx: &Transmitter<ListOut>, sub: &Subscriber<ListIn>) {
        match msg {
            ListIn::NewItem => {
                let item: Item = Item { id: self.next_id, clicks: 0 };
                self.next_id += 1;

                let gizmo: Gizmo<Item> = Gizmo::from(item);
                sub.subscribe_filter_map(&gizmo.recv, |child_msg: &ItemOut| match child_msg {
                    ItemOut::Remove(index) => Some(ListIn::RemoveItem(*index)),
                    _ => None
                });

                let view: View<HtmlElement> = View::from(gizmo.view_builder());
                tx.send(&ListOut::PatchItem(Patch::PushBack { value: view }));
                self.items.push(gizmo);
            }
            ListIn::RemoveItem(id) => {
                let mut may_index = None;
                'find_item_by_id: for (item, index) in self.items.iter().zip(0..) {
                    if &item.state_ref().id == id {
                        may_index = Some(index);
                        tx.send(&ListOut::PatchItem(Patch::Remove{ index }));
                        break 'find_item_by_id;
                    }
                }
                if let Some(index) = may_index {
                    self.items.remove(index);
                }
            }
        }
    }

    fn view(&self, tx: &Transmitter<ListIn>, rx: &Receiver<ListOut>) -> ViewBuilder<HtmlElement> {
        builder! {
            <fieldset>
                <legend>"A List of Gizmos"</legend>
                <button style:cursor="pointer" on:click=tx.contra_map(|_| ListIn::NewItem)>
                    "Create a new item"
                </button>
                <fieldset>
                    <legend>"Items"</legend>
                    <ol patch:children=rx.branch_map(|ListOut::PatchItem(patch)| patch.clone())>
                    </ol>
                </fieldset>
            </fieldset>
        }
    }
}

#[wasm_bindgen]
pub fn main(parent_id: Option<String>) -> Result<(), JsValue> {
    panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(Level::Trace).unwrap();

    let gizmo = Gizmo::from(List { items: vec![], next_id: 0 });
    let view = View::from(gizmo.view_builder());
    if let Some(id) = parent_id {
        let parent = utils::document()
            .get_element_by_id(&id)
            .unwrap();
        view.run_in_container(&parent)
    } else {
        view.run()
    }
}

Notice that the main of this example takes an optional string. This allows us to pass the id of an element that we'd like to append our list/parent component to. This allows us to load the example on the page right here.

Play with it

Single Page App Routing

SPA routing is often needed to represent different resources within a single page application. Here we define a method of routing that relies on conversions of types and mogwai's patch:children RSX attribute.

Contents

Explanation

We first define a Route type which will hold all our available routes. Then we create conversion implementations to convert from a window hash string into a Route and back again. For this routing example (and in order to keep it simple) we also implement From<Route> for ViewBuilder, which we'll use to create views from our route.


/// Here we enumerate all our app's routes.
#[derive(Clone, Debug, PartialEq)]
pub enum Route {
    Home,
    Settings,
    Profile {
        username: String,
        is_favorites: bool,
    },
}

/// We'll use TryFrom::try_from to convert the window's url hash into a Route.
impl TryFrom<&str> for Route {
    type Error = String;

    fn try_from(s: &str) -> Result<Route, String> {
        trace!("route try_from: {}", s);
        // remove the scheme, if it has one
        let hash_split = s.split("#").collect::<Vec<_>>();
        let after_hash = match hash_split.as_slice() {
            [_, after] => Ok(after),
            _ => Err(format!("route must have a hash: {}", s)),
        }?;

        let paths: Vec<&str> = after_hash.split("/").collect::<Vec<_>>();
        trace!("route paths: {:?}", paths);

        match paths.as_slice() {
            [""] => Ok(Route::Home),
            ["", ""] => Ok(Route::Home),
            ["", "settings"] => Ok(Route::Settings),
            // Here you can see that profile may match two different routes -
            // '#/profile/{username}' and '#/profile/{username}/favorites'
            ["", "profile", username] => Ok(Route::Profile {
                username: username.to_string(),
                is_favorites: false,
            }),
            ["", "profile", username, "favorites"] => Ok(Route::Profile {
                username: username.to_string(),
                is_favorites: true,
            }),
            r => Err(format!("unsupported route: {:?}", r)),
        }
    }
}

#[cfg(test)]
mod test_route_try_from {
    use super::*;

    #[test]
    fn can_convert_string_to_route() {
        let s = "https://localhost:8080/#/";
        assert_eq!(Route::try_from(s), Ok(Route::Home));
    }
}

/// Convert the route into its hashed string.
/// This should match the inverse conversion in TryFrom above.
impl From<Route> for String {
    fn from(route: Route) -> String {
        match route {
            Route::Home => "#/".into(),
            Route::Settings => "#/settings".into(),
            Route::Profile {
                username,
                is_favorites,
            } => {
                if is_favorites {
                    format!("#/profile/{}/favorites", username)
                } else {
                    format!("#/profile/{}", username)
                }
            }
        }
    }
}

/// We can convert a route into a ViewBuilder in order to embed it in a gizmo.
/// This is just a suggestion for this specific example. The general idea is
/// to use the route to inform your app that it needs to change the page. This
/// is just one of many ways to accomplish that.
impl From<&Route> for ViewBuilder<HtmlElement> {
    fn from(route: &Route) -> Self {
        match route {
            Route::Home => builder! {
                <main>
                    <h1>"Welcome to the homepage"</h1>
                </main>
            },
            Route::Settings => builder! {
                <main>
                    <h1>"Update your settings"</h1>
                </main>
            },
            Route::Profile {
                username,
                is_favorites,
            } => builder! {
                <main>
                    <h1>{username}"'s Profile"</h1>
                    {if *is_favorites {
                        Some(builder!{
                            <h2>"Favorites"</h2>
                        })
                    } else {
                        None
                    }}
                </main>
            },
        }
    }
}

impl From<&Route> for View<HtmlElement> {
    fn from(route: &Route) -> Self {
        ViewBuilder::from(route).into()
    }
}

Our main App gizmo will hold a route.

struct App {
    route: Route,
}

Its view will wait for the window's hashchange event. When it receives a hashchange event it will extract the new URL and send a message to App::update. We'll also set up a pre element to hold any potential error messages.

    fn view(&self, tx: &Transmitter<AppModel>, rx: &Receiver<AppView>) -> ViewBuilder<HtmlElement> {
        let username: String = "Reasonable-Human".into();
        builder! {
            <slot
                window:hashchange=tx.contra_filter_map(|ev:&Event| {
                    let hev = ev.dyn_ref::<HashChangeEvent>().unwrap().clone();
                    let hash = hev.new_url();
                    Some(AppModel::HashChange(hash))
                })
                patch:children=rx.branch_filter_map(AppView::patch_page)>
                <nav>
                    <ul>
                        <li class=self.route.nav_home_class()>
                            <a href=String::from(Route::Home)>"Home"</a>
                        </li>
                        <li class=self.route.nav_settings_class()>
                            <a href=String::from(Route::Settings)>"Settings"</a>
                        </li>
                        <li class=self.route.nav_settings_class()>
                            <a href=String::from(Route::Profile {
                                username: username.clone(),
                                is_favorites: true
                            })>
                                {format!("{}'s Profile", username)}
                            </a>
                        </li>
                    </ul>
                </nav>
                <pre>{rx.branch_filter_map(AppView::error)}</pre>
                {ViewBuilder::from(&self.route)}
            </slot>
        }
    }
}

App::update receives the hashchange, attempting to convert it into a Route and either patches the DOM with a new page or sends an error message to our error element.

    fn update(&mut self, msg: &AppModel, tx: &Transmitter<AppView>, _sub: &Subscriber<AppModel>) {
        match msg {
            AppModel::HashChange(hash) => {
                // When we get a hash change, attempt to convert it into one of our routes
                match Route::try_from(hash.as_str()) {
                    // If we can't, let's send an error message to the view
                    Err(msg) => tx.send(&AppView::Error(msg)),
                    // If we _can_, create a new view from the route and send a patch message to
                    // the view
                    Ok(route) => {
                        if route != self.route {
                            let view = View::from(ViewBuilder::from(&route));
                            self.route = route;
                            tx.send(&AppView::PatchPage(Patch::Replace {
                                index: 2,
                                value: view,
                            }));
                        }
                    }
                }
            }
        }
    }

That's the bulk of the work.

Code

Here's the whole example.

#![allow(unused_braces)]
use log::{trace, Level};
use mogwai::prelude::*;
use std::panic;
use wasm_bindgen::prelude::*;
use web_sys::HashChangeEvent;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

/// Here we enumerate all our app's routes.
#[derive(Clone, Debug, PartialEq)]
pub enum Route {
    Home,
    Settings,
    Profile {
        username: String,
        is_favorites: bool,
    },
}

/// We'll use TryFrom::try_from to convert the window's url hash into a Route.
impl TryFrom<&str> for Route {
    type Error = String;

    fn try_from(s: &str) -> Result<Route, String> {
        trace!("route try_from: {}", s);
        // remove the scheme, if it has one
        let hash_split = s.split("#").collect::<Vec<_>>();
        let after_hash = match hash_split.as_slice() {
            [_, after] => Ok(after),
            _ => Err(format!("route must have a hash: {}", s)),
        }?;

        let paths: Vec<&str> = after_hash.split("/").collect::<Vec<_>>();
        trace!("route paths: {:?}", paths);

        match paths.as_slice() {
            [""] => Ok(Route::Home),
            ["", ""] => Ok(Route::Home),
            ["", "settings"] => Ok(Route::Settings),
            // Here you can see that profile may match two different routes -
            // '#/profile/{username}' and '#/profile/{username}/favorites'
            ["", "profile", username] => Ok(Route::Profile {
                username: username.to_string(),
                is_favorites: false,
            }),
            ["", "profile", username, "favorites"] => Ok(Route::Profile {
                username: username.to_string(),
                is_favorites: true,
            }),
            r => Err(format!("unsupported route: {:?}", r)),
        }
    }
}

#[cfg(test)]
mod test_route_try_from {
    use super::*;

    #[test]
    fn can_convert_string_to_route() {
        let s = "https://localhost:8080/#/";
        assert_eq!(Route::try_from(s), Ok(Route::Home));
    }
}

/// Convert the route into its hashed string.
/// This should match the inverse conversion in TryFrom above.
impl From<Route> for String {
    fn from(route: Route) -> String {
        match route {
            Route::Home => "#/".into(),
            Route::Settings => "#/settings".into(),
            Route::Profile {
                username,
                is_favorites,
            } => {
                if is_favorites {
                    format!("#/profile/{}/favorites", username)
                } else {
                    format!("#/profile/{}", username)
                }
            }
        }
    }
}

/// We can convert a route into a ViewBuilder in order to embed it in a gizmo.
/// This is just a suggestion for this specific example. The general idea is
/// to use the route to inform your app that it needs to change the page. This
/// is just one of many ways to accomplish that.
impl From<&Route> for ViewBuilder<HtmlElement> {
    fn from(route: &Route) -> Self {
        match route {
            Route::Home => builder! {
                <main>
                    <h1>"Welcome to the homepage"</h1>
                </main>
            },
            Route::Settings => builder! {
                <main>
                    <h1>"Update your settings"</h1>
                </main>
            },
            Route::Profile {
                username,
                is_favorites,
            } => builder! {
                <main>
                    <h1>{username}"'s Profile"</h1>
                    {if *is_favorites {
                        Some(builder!{
                            <h2>"Favorites"</h2>
                        })
                    } else {
                        None
                    }}
                </main>
            },
        }
    }
}

impl From<&Route> for View<HtmlElement> {
    fn from(route: &Route) -> Self {
        ViewBuilder::from(route).into()
    }
}

/// Here we'll define some helpers for displaying information about the current route.
impl Route {
    pub fn nav_home_class(&self) -> String {
        match self {
            Route::Home => "nav-link active",
            _ => "nav-link",
        }
        .to_string()
    }

    pub fn nav_settings_class(&self) -> String {
        match self {
            Route::Settings { .. } => "nav-link active",
            _ => "nav-link",
        }
        .to_string()
    }

    pub fn nav_profile_class(&self) -> String {
        match self {
            Route::Profile { .. } => "nav-link active",
            _ => "nav-link",
        }
        .to_string()
    }
}

struct App {
    route: Route,
}

#[derive(Clone)]
enum AppModel {
    HashChange(String),
}

#[derive(Clone)]
enum AppView {
    PatchPage(Patch<View<HtmlElement>>),
    Error(String),
}

impl AppView {
    fn error(&self) -> Option<String> {
        match self {
            AppView::Error(msg) => Some(msg.clone()),
            _ => None,
        }
    }

    /// If the message is a new route, convert it into a patch to replace the current main page.
    fn patch_page(&self) -> Option<Patch<View<HtmlElement>>> {
        match self {
            AppView::PatchPage(patch) => Some(patch.clone()),
            _ => None,
        }
    }
}

impl Component for App {
    type DomNode = HtmlElement;
    type ModelMsg = AppModel;
    type ViewMsg = AppView;

    fn update(&mut self, msg: &AppModel, tx: &Transmitter<AppView>, _sub: &Subscriber<AppModel>) {
        match msg {
            AppModel::HashChange(hash) => {
                // When we get a hash change, attempt to convert it into one of our routes
                match Route::try_from(hash.as_str()) {
                    // If we can't, let's send an error message to the view
                    Err(msg) => tx.send(&AppView::Error(msg)),
                    // If we _can_, create a new view from the route and send a patch message to
                    // the view
                    Ok(route) => {
                        if route != self.route {
                            let view = View::from(ViewBuilder::from(&route));
                            self.route = route;
                            tx.send(&AppView::PatchPage(Patch::Replace {
                                index: 2,
                                value: view,
                            }));
                        }
                    }
                }
            }
        }
    }

    fn view(&self, tx: &Transmitter<AppModel>, rx: &Receiver<AppView>) -> ViewBuilder<HtmlElement> {
        let username: String = "Reasonable-Human".into();
        builder! {
            <slot
                window:hashchange=tx.contra_filter_map(|ev:&Event| {
                    let hev = ev.dyn_ref::<HashChangeEvent>().unwrap().clone();
                    let hash = hev.new_url();
                    Some(AppModel::HashChange(hash))
                })
                patch:children=rx.branch_filter_map(AppView::patch_page)>
                <nav>
                    <ul>
                        <li class=self.route.nav_home_class()>
                            <a href=String::from(Route::Home)>"Home"</a>
                        </li>
                        <li class=self.route.nav_settings_class()>
                            <a href=String::from(Route::Settings)>"Settings"</a>
                        </li>
                        <li class=self.route.nav_settings_class()>
                            <a href=String::from(Route::Profile {
                                username: username.clone(),
                                is_favorites: true
                            })>
                                {format!("{}'s Profile", username)}
                            </a>
                        </li>
                    </ul>
                </nav>
                <pre>{rx.branch_filter_map(AppView::error)}</pre>
                {ViewBuilder::from(&self.route)}
            </slot>
        }
    }
}


#[wasm_bindgen]
pub fn main(parent_id: Option<String>) -> Result<(), JsValue> {
    panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(Level::Trace).unwrap();

    let gizmo = Gizmo::from(App { route: Route::Home });
    let view = View::from(gizmo.view_builder());
    if let Some(id) = parent_id {
        let parent = utils::document()
            .get_element_by_id(&id)
            .unwrap();
        view.run_in_container(&parent)
    } else {
        view.run()
    }
}

Notice that the main of this example takes an optional string. This allows us to pass the id of an element that we'd like to append our routing component to. This allows us to load the example on the page right here. Click on the links below to see the route and our page change. Click on the links in the table of contents to see our errors propagate into the view.

Play with it

Creating SVGs with mogwai

Scalable Vector Graphics are images that are specified with XML. An SVG's markup looks a lot like HTML markup and allows developers and artists to quickly create images that can be resized without degrading the quality of the image.

Contents

Explanation

In mogwai we create SVG images using the same RSX we use to create any other View. There's just one extra attribute we need to specify that lets the browser know that we're drawing an SVG image instead of HTML - the xmlns attribute.

Notes

Unfortunately we must supply this namespace for each SVG node. It ends up not being too much of a burden.

Code

#![allow(unused_braces)]
use log::Level;
use mogwai::prelude::*;
use std::panic;
use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

/// Create an SVG circle using the xmlns attribute and the SVG namespace.
fn my_circle() -> ViewBuilder<HtmlElement> {
    let ns = "http://www.w3.org/2000/svg";
    builder! {
        <svg xmlns=ns width="100" height="100">
            <circle xmlns=ns
                cx="50"
                cy="50"
                r="40"
                stroke="green"
                stroke-width="4"
                fill="yellow" />
        </svg>
    }
}

#[wasm_bindgen]
pub fn main(parent_id: Option<String>) -> Result<(), JsValue> {
    panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(Level::Trace).unwrap();

    let view = View::from(my_circle());

    if let Some(id) = parent_id {
        let parent = utils::document()
            .get_element_by_id(&id)
            .unwrap();
        view.run_in_container(&parent)
    } else {
        view.run()
    }
}

Notice that the main of this example takes an optional string. This allows us to pass the id of an element that we'd like to append our list/parent component to. This allows us to load the example on the page right here.

Example