Introducing Smithy — A WebAssembly framework for Rust

What is Smithy?

A simple example

#[wasm_bindgen]
pub fn start(div_id: String) {
let doc = web_sys::window().unwrap().document().unwrap();
let root_element: web_sys::Element =
doc.get_element_by_id(&div_id).unwrap();
let mut count: u32 = 0;
let app = smithy::smd!(
<div on_click={|_| count = count + 1}>
Click count: <b>{count}</b>
</div>
);
smithy::mount(Box::new(app), root_element);
}

That’s it?

  • You exported a public function that was annotated with #[wasm_bidgen]
  • You created a smithy app using the smd! macro
  • You passed this app to smithy::mount

Whirlwind tour

Wow! That’s simple. How does the smd! work under the hood?

let app = smithy::types::SmithyComponent(Box::new(move |phase| match phase {
smithy::types::Phase::Rendering => {
/* return a representation of the rendered DOM */
},
smithy::types::Phase::UiEventHandling(ui_event_handling) => {
/* increment count */
},
/* other phases */
}));

What does smithy::mount do with app?

pub enum Phase<'a> {
Rendering,
RefAssignment(Vec<usize>),
PostRendering,
UiEventHandling((&'a crate::UiEvent, &'a Path)),
WindowEventHandling(&'a crate::WindowEvent),
}
  • Rendering: First, Smithy will ask the app for the "virtual DOM" representing how that app would like to be rendered. Smithy then writes that to the DOM.
  • RefAssignment: Next, Smithy will take any ref's passed to dom nodes (as ref={ref /* &mut Option<web_sys::HtmlElement> */}) and assign them.
  • PostRendering: Next, Smithy executes any code that must happen after the DOM has been updated, or after you have a reference to the actual DOM node. Examples include modifying the value of an input in response to changes in state.
  • At this point, Smithy waits for events. Smithy can respond to DOM events (such as clicks, in the UiEventHandlingphase) and window events (such as hash change events, in the WindowEventHandling phase.) After an event occurs, Smithy will re-render the app.
let div_ref: Option<web_sys::HtmlElement> = None;
let app = smd!(
on_hash_change={|hash_change_event| {
// this on_hash_change event handler is used in the
// WindowEventHandling phase
}};
post_render={|| {
// this is executed during the PostRendering phase
}};
// the following two lines get used during the Rendering phase
<div
class="some-class"
on_click={|mouse_event| {
// this on_click handles is used in the UiEventHandling phase
}}
// The following line is used in the RefAssignment phase
ref={div_ref}
/>
)

Conclusion

Advanced topics

What is valid smd! syntax?

smd!(
on_hash_change=|e: web_sys::HashChangeEvent| {
// window events are handled at the beginning of your smd! macro
// and are followed by a semi colon
};
post_render=|| {
// your post_render callback goes in the same place
};
// Afterward, you can have as many dom nodes,
// as much text, and as many
// interpolated values as you'd like.
<div>
// Child nodes are supported
<b>Hello world</b>
// Are are self closing tags
<div />
// Attributes work like you'd expect
<div class="foo" />
</div>
You can include text.
// Interpolating components is simple.
// Just stick whatever you want between
// curly brackets. (You may need to interpolate
// a mutable reference, though)
{ &mut child_component }
// You can also render components in-line.
// They're just regular functions!
{ another_module::render_component() }
// You can interpolate other values, too
{ 42 }
// You can include multiple statements inside the curlies, too!
{
let message = "Hi there!";
message
}
<non standard up elements are allowed too />
)

Why does the smd! macro create a Box<FnMut> instead of a struct?

let app = StructComponent(DomElement::Node {
node_type: "div",
event_handlers: EventHandlers { on_click: |_| count = count + 1 },
children: vec![
DomElement::String(count.to_string()),
]
}

How are Dom elements represented?

pub enum Node {
Dom(HtmlToken),
Text(String),
Vec(Vec<Node>),
Comment(Option<String>),
}
pub struct HtmlToken {
pub node_type: String,
pub children: Vec<Node>,
pub attributes: Attributes,
}

How does Smithy know what to update? Does it re-render the entire DOM tree?

How does interpolation work?

let coolest_spaceship = "x-wing";
smd!({ coolest_spaceship })

What gotchas are there with interpolation?

let plane = match model {
// many more lines...
};
smd!({ plane })
smd!({
let plane = match model {
// many more lines...
};
plane
})

More interpolation gotchas

fn outer<'a>() -> SmithyComponent<'a> {
let mut count = 0;
smd!({ inner(count) })
}
fn inner<'a>(&'a mut count: i32) -> SmithyComponent<'a> {
smd!(<div on_click={|_| count = count + 1}>Increase count</div>)
}
fn outer<'a>() -> SmithyComponent<'a> {
let mut count = 0;
smd!(
{ count }
<div on_click={|_| count = count + 1}>
Increase count
</div>
)
}
fn outer<'a>() -> SmithyComponent<'a> {
let mut count = 0;
smd!({ count }{ inner(&mut count) })
}
fn inner<'a>(count: &'a mut i32) -> SmithyComponent<'a> {
smd!(<div on_click={|_| *count = *count + 1}>Increase count</div>)
}

Okay, so what do I do?

fn outer<'a>() -> SmithyComponent<'a> {
let count = 0;
let count = Rc::new(RefCell::new(count));
let count_2 = count.clone();
smd!({ *count.borrow() }{ inner(&count_2) })
}
fn inner<'a>(count: &'a Rc<RefCell<usize>>) -> SmithyComponent<'a> {
smd!(<div on_click={|_| {}}>Increase count</div>)
}

How do I call child components?

let child_component = render_child_component(params);
smd!(<div>{ &mut child_component }</div>)
smd!(<div>{ render_child_component(params) }</div>)

What are some gotchas when using child components?

fn main() -> impl Component {
smd!(<div>{ render_child_component() }</div>)
}
fn render_child_component() -> impl Component {
let input_ref: Option<web_sys::HtmlElement> = None;
smd!(
post_render={|| {
if Some(el) = input_ref {
// N.B. this will never be executed!
}
}};
<input
ref={input_ref}
/>
)
}

What about futures?

Does Smithy compile on stable Rust?

I want to be involved!

--

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Ultimate Pandas Guide — Joining data with Python

A Panda eating bamboo as it contemplates how to join data in Pandas

3 P’s of Project Management

How to install openVINO on ubuntu 18.04?

Installing Neo4j on Ubuntu 14.04 - Step by Step guide

Breaking into Competitive Coding

Cookies

The 5 Stages of a SaaS Subscription

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Robert Balicki

Robert Balicki

More from Medium

Learn Rust from simple guess game.

Base64 Encoding Implementation in Rust

RustcContributor::new Update

How to publish a package in Rust