hobo

Crate Info API Docs

hobo is an opinionated, batteries-included Rust frontend framework. Works on stable Rust.
STILL WIP although used in production by GR Digital.
Check out the Book!

Notable features:

  • no virtual DOM - html elements are just components added to entities and can be accessed directly via web_sys::HtmlElement
  • no Model-View-Update (aka Elm architecture) - state management is manual, usually via Entity-Component relations
  • no HTML macros - just Rust functions
  • built-in macro-based styling, kind of like CSS-in-JS except it's just Rust
  • reactivity support via futures-signals
  • Entity-Component based approach allowing flexible state propagation and cohesion between elements without coupling or a need for global store or state

Sneak peek:

pub use hobo::{
    prelude::*,
    create as e,
    signals::signal::{Mutable, SignalExt}
};

fn counter() -> impl hobo::AsElement {
    let counter = Mutable::new(0);

    e::div()
        .class((
            css::display!(flex),
            css::flex_direction!(column),
            css::width!(400 px),
        ))
        .child(e::div()
            .text_signal(counter.signal().map(|value| {
                format!("Counter value is: {value}")
            }))
        )
        .child(e::button()
            .text("increment")
            .on_click(move |_| *counter.lock_mut() += 1)
        )
}

Getting Started

Here's a basic counter component:

pub use hobo::{
    prelude::*, 
    create as e,
    signals::signal::{Mutable, SignalExt}
}

// <div class="s-f4d1763947b5e1ff">
//   <div>Counter value is: 0</div>
//   <button>increment</button>
//   <button>decrement</button>
// </div>

fn counter() -> impl hobo::AsElement {
    let counter_value = Mutable::new(0_i32);

    e::div()
        .class((
            // enum-like properties can also be set like `css::Display::Flex`
            css::display!(flex),
            css::width!(400 px),
            // #AA0000FF or #AA0000 or #A00 in normal css
            css::background_color!(rgb 0xAA_00_00),
            css::align_items!(center),
            css::justify_content!(space-between),
        ))
        .child(e::div()
            .text_signal(counter_value.signal().map(|value| {
                format!("Counter value is: {}", value)
            }))
        )
        .component(counter_value)
        .with(move |&counter_div| counter_div
            .child(e::button()
                .class(css::style!(
                    // .& is replaced with "current" class name, similar to SASS
                    // or styled-components
                    .& {
                        // shortcut for padding-left and padding-right
                        css::padding_horizontal!(16 px),
                        css::background_color!(css::color::PALEVIOLETRED),
                    }

                    .&:hover {
                        css::background_color!(css::color::GREEN),
                    }
                ))
                .text("increment")
                .on_click(move |_| {
                    *counter_div.get_cmp::<Mutable<i32>>().lock_mut() += 1;
                })
            )
            .add_child(e::button() // same as .child but non-chaining
                // since this style is identical to the one above it - the class will be
                // reused to avoid copypasting - the button generating code can be
                // moved into a function or maybe just the code that defines the style
                .class(css::style!(
                    .& {
                        css::padding_horizontal!(16 px),
                        css::background_color!(css::color::PALEVIOLETRED),
                    }

                    .&:hover {
                        css::background_color!(css::color::GREEN),
                    }
                ))
                .text("decrement")
                .on_click(move |_| {
                    *counter_div.get_cmp::<Mutable<i32>>().lock_mut() -= 1;
                })
            )
        )
}

Core Concepts

This chapter outlines core types, traits and styling facilities that hobo employs.

Some note on terms used:

  • Entity: a (usually) copyable id, that has components associated with it
  • Element: not to be confused with HTML elements, an Entity that has HTML or SVG components (which represent HTML elements) associated with it and so can have children, class, attributes, etc
  • Component: any kind of data that may be associated with an Entity
  • Mutable: not to be confused with Rust's notion of mutability, a type from futures_signals that can be used to produce signals

Entities, Components (and Resources)

The backbone of the framework is the Entity-Component approach of associating data. Entities are just incrementing u64s under the hood, they carry no data.

Elements are no different in this regard, the only difference is that Elements have a compile time promise that these entities have web_sys::Node, web_sys::Element, web_sys::EventTarget and one of web_sys::HtmlElement or web_sys::SvgElement attached to them. As a consequence, these Entities can get styled, get attributes and compose into DOM.

Resources are same as Components but they are accessible globally, they aren't associated with any entity. Only one instance of a type of Resource can exist at any time, in this way they are similar to singletons from other programming languages.

hobo::create

This module has a snake_case function which returns a corresponding PascalCase concrete type that implements AsElement.

let some_div: hobo::create::Div = hobo::create::div();

Element has methods that aren't available on regular entities.

hobo::AsElement and hobo::AsEntity

Sometimes it's useful to have custom types so you can have some special capabilities on your Entities or Elements.

#[derive(hobo::AsElement, Clone, Copy /* etc */)]
struct Checkbox(hobo::create::Div);

// just an example of why you might want to do this
impl Checkbox {
	fn is_checked(&self) -> bool {
		*self.get_cmp::<bool>()
	}

	fn set_checked(&self, checked: bool) {
		*self.get_cmp_mut_or_default::<bool>() = checked;
	}

	// probably etc methods
}

The hobo::AsElement derive macro expects either a tuple struct or a regular struct where the Entity field is named element e.g.

#[derive(hobo::AsElement, Clone, Copy /* etc */)]
struct CustomSelect {
	element: hobo::create::Select,
	// etc
}

Element and type erasure

It's often useful to mix different types of Elements, for example:

fn content() -> impl hobo::AsElement {
	match tab {
		Tab::Main => main_page(), // hobo::create::Div
		Tab::Blogpost => article(), // hobo::create::Article
		// etc
	}
}

This won't compile, but the distinction between types in this case isn't useful. So we can erase the concrete types and get the general Element:

fn content() -> impl hobo::Element {
	match tab {
		Tab::Main => main_page().as_element(), // hobo::Element
		Tab::Blogpost => article().as_element(), // hobo::Element
		// etc
	}
}

If you have a regular Entity or something that at least implements hobo::AsEntity - you can recover Element capabilities by just constructing a Element:

let elem = hobo::Element(some_entity);

This pattern is often useful when using queries to find elements, as queries often return entities (more on them in queries)

let (entity, _) = hobo::find_ond::<Entity, With<ComponentFoo>>();
// We know that this entity is an Input element we've made,
// but we need it's type to be an Input, not Entity,
// to e.g. access it's value via the get/set_value methods
let input_element = hobo::create::Input(entity);
let input_value = input_element.get_value();

One can think of it almost as casting - we're fetching an entity which we, as the writer, know is an Input - however, we need to "cast" this Entity to an Input type in order to access Input capabilities.

Children and Parent

Hierarchical DOM relations in hobo are maintained through regular Components - hobo::Children and hobo::Parent.

Usually you won't have to care about it since .add_child() (and the like) and .remove() already take care of updating Children and Parent components of affected entities.

hobo::Children is just a Vec of hobo::Entity, hobo::Parent is a newtype wrapper over hobo::Entity as well. If you have an Element and you want to operate on all (or some) of its children - it's as simple as:

let children = foo.get_cmp::<hobo::Children>()
    .iter()
    .map(|entity| hobo::SomeElement(entity));

for child in children {
    child.set_text("hello from hobo!");
}

It is possible to detach a child from its parent to reattach it to a different Element later, but it's not as simple as removing hobo::Parent and fixing up hobo::Children of the parent entity since the DOM has to be modified as well. A convenient method exists however:

// this removes parent and fixes children component in parent as well
some_child.leave_parent();
new_parent.add_child(some_child);

Removing and replacing elements

Removing an Element (or an Entity) is as simple as calling .remove(). The method will recursively remove all entities in hobo::Children of the entity to be removed as well. All components that have been added to entities that are being removed are also removed and dropped.

It is possible to replace an Element inplace, fixing up the hobo::Children in parent entity as well. This, however, replaces it with a new Entity so if a copy is held somewhere - it won't be valid anymore, so take care.

let new_element = hobo::create::div();
old_element.replace_with(new_element);

DOM Events and EventHandlerCallback

Elements have methods that allow reacting to DOM Events. All of these methods are snake_case in the form of .on_<name> e.g. .on_click or .on_touch_start. Not all possible events are supported currently, but adding new ones is very easy - PRs welcome!

element
    .on_click(move |_| { // the argument here is web_sys::MouseEvent
        element.set_text("I am clicked!");
    })

These methods operate by means of, unsurprisingly, adding or modifying a Component on the element. The callback itself gets wrapped in hobo::dom_events::EventHandlerCallback, which will unsubscribe from DOM when dropped. A Component with a Vec<EventHandlerCallback> is created unless it already exists, then the just created EventHandlerCallback is just pushed into it.

It's possible to manage subscribing/unsubscribing manually by calling the functions on raw web_sys::HtmlElements. For example, when you're doing some kind of a slider and you want some logic in on_mouse_move even if the mouse leaves the element:

element
    .on_mouse_down(move |_| {
        // "drag" start
    })
    .component((
        web_sys::window().unwrap().on_mouse_move(move |e| {
            // if dragging, run some dragging logic even once mouse leaves the element
        }),
        web_sys::window().unwrap().on_mouse_up(move |e| {
            // "drag" stop
        }),
    ))

Borrowing and Storage

Components for entities are stored in a simple map - HashMap<Entity, Component> (see, hobo::storage::SimpleStorage).

(This also makes searching for components via hobo::find_one very cheap).

Rust's ownership rules ensure that a mutable borrow is exclusive, which means that we cannot have mutable references to components while immutable ones exists (or vice-versa). Here's an example of how this affects hobo:

// src\example_file.rs

mod example_module {
    pub use hobo::{prelude::*, create as e};

    struct Foo;

    pub fn test() -> impl hobo::AsElement {
        e::div()
            .component(Foo)
            .with(|&element| {
                // Ok
                let foo1 = element.get_cmp::<Foo>();
                // Still ok
                let foo2 = element.get_cmp::<Foo>();
                // Panic!
                let foo3 = element.get_cmp_mut::<Foo>();
            })
    }
}

This, of course, also applies to queries/find/etc.

This can be a bit tricky to debug in Wasm, which is why when compiling in debug mode, hobo will display the following helpful message in the browser's console if a borrow-related runtime panic is encountered:

panicked at ''already borrowed': Trying to mutably borrow `example_module::Foo`    
storage at `src\example_file.rs:16:50` while other borrows to it already exist:

(mut) src\example_file.rs:16:50
      src\example_flib.rs:14:50
      src\example_flib.rs:12:50

This will list only the currently active borrows, as well as the mutable one, descending in order of access.

(Every type we store as a component will have it's own storage, so it's fine to mutably borrow storages of different types.)

As an example of where this could arise as an issue, imagine the following situation:

We want to replace an element with a new one, using some data we stored in it.

struct SomeData {
    big_data: u64,
};

pub fn update_element(old_element: impl hobo::AsElement + Copy) {
    let some_data = old_element.get_cmp::<SomeData>();

    let new_element = process_data_and_return_div(some_data);
    
    // Runtime panic!
    old_element.replace_with(new_element);
}

This will panic at runtime - this is because when we delete the old element (via replace) we need to mutably borrow the storage to all of it's components, in order to delete them too. However, we are already holding a reference to one of the components.

The way to circumvent this would be similar to how one would for any other ownership issue:

You can drop the guard, ensuring that no references conflict:

pub fn update_element(old_element: impl hobo::AsElement + Copy) {
    let some_data = old_element.get_cmp::<SomeData>();

    let new_element = process_data_and_return_div(some_data);
    
    drop(some_data);

    old_element.replace_with(new_element);
}

Or, you can clone the value:

#[derive(Clone)]
struct SomeData {
    big_data: u64,
};

pub fn update_element(element: impl hobo::AsElement + Copy) {
    let some_data = old_element.get_cmp::<SomeData>().clone();

    let new_element = process_data_and_return_div(&some_data);
    
    old_element.replace_with(new_element);
}

Styling facilities

Most Elements will be styled with either .class() or .style() functions, where either css::style!() or a property tuple will be used.

  • .style() and .set_style() use the style attribute, which can only take a bunch of properties without any selectors, so a property tuple is used.
  • .class(), .set_class() and tagged or typed variants use the class attribute:

For example, here's a style:

hobo::create::div()
    .class(css::style!(
        .& {
            css::height!(393 px),
            css::Display::Flex, // can also be `css::display!(flex)`
            css::AlignItems::Center,
            css::Position::Relative,
        }

        .& > svg {
            css::width!(12 px),
            css::height!(100%),
            css::Cursor::Pointer,
            css::flex_shrink!(0),
            css::UserSelect::None,
        }

        .& > :not(:nth_child(0, 1)) { // nth_child will convert to An+B syntax
            css::z_index!(200),
        }

        .& > div:not(:nth_child(0, 1)) {
            css::width!(17.5%),
            css::height!(100%),
            css::Display::Flex,
            css::AlignItems::Center,
        }

        // doubling up on the class name increases specificity
        .&.& > :nth_child(0, 5) { 
            css::width!(30%),
        }

        .& > *:nth_child(0, 3) > img,
        .& > *:nth_child(0, 4) > img,
        .& > svg:last_child {
            css::TransformFunction::TranslateX(css::unit!(50%)),
        }

        .& >> img { // this is same as `.& img` selector in css
            css::height!(100%),
        }
    ))

Property tuple example:

    hobo::create::div()
        .style((
            // Shortcut for same width and height
            css::size!(12 px),
            css::Display::Flex,
        ))

If only a single property is used, one can ommit the tuple:

    hobo::create::div()
        .class(css::Display::Flex)

Chaining vs non-chaining syntax: .style() is the chaining syntax, .set_style() is the non-chaining alternative. Similarly, .class() and .set_class(). More about chaining vs non-chaining syntax in Building the DOM.

Selector

hobo selectors mirror css selectors with minor changes, most notably:

  • descendant selectors like div a become div >> a because Rust doesn't have semantic whitespaces.
    • selectors like div.active work mostly the same (except have to be written like div.("active") or div .("active"))
  • ids have to be written like #("foo-1234")
  • pseudo-classes use _ instead of - and must always use single colon syntax, e.g. :active or :last_child
    • there's an escape hatch in :raw("-webkit-prop".to_string()) for browser-specific or other weird things
  • pseudo-elements use _ instead of - and must always use double colon syntax, e.g. ::after or ::first_line

There are also several additions:

  • .& will be replaced at runtime with the name of a class, which will be generated from the rules in the style it belongs to
    • in other words, it's similar to & in SASS or styled-components
  • .[T] where T is some marker type will be replaced with the generated classname for the type T so you could select based on custom marker type.
use hobo::create as e;

struct ButtonMarker;

e::div()
    .class(css::style!(
        .& >> .[ButtonMarker] {
            css::cursor!(pointer),
        }
    ))
    .child(e::div()
        .mark::<ButtonMarker>()
        .text("button 1")
    )
    .child(e::div()
        .mark::<ButtonMarker>()
        .text("button 2")
    )

Property

Most css properties will be expressed as tuples of anything that implements hobo::css::AppendProperty, which includes:

  • css::Property such as created by the css::<prop>() family of macros (e.g. css::width!(), css::flex_shrink!(), etc)
  • Vec<css::Property>
  • ()
  • &'static str and String as escape hatches
  • FnOnce(&mut Vec<Property>) for rare complex logic
  • Other tuples of things that implement hobo::css::AppendProperty
  • Enum-like property variants e.g. css::Display::Flex or css::TextDecorationStyle::Solid

Conditional property inclusion could be expressed as different Vec<css::Property> where one is empty, e.g.

(
    css::display!(flex),
    if active {
        vec![css::background_color!(0x00_00_FF_FF)],
    } else {
        vec![],
    },
)

Or alternatively, by leveraging FnOnce

(
    css::display!(flex),
    move |props| if active { props.push(css::background_color!(0x00_00_FF_FF)); },
)

@-rules

Right now hobo only supports @font-face and a subset of @media

@font-face

The block following @font-face is passed as initialization for css::font_face::FontFace. Check out the docs.
It looks something like this:

#![allow(unused)]
fn main() {
@font-face {
    src: vec![("https://fonts.gstatic.com/.../....woff2".into(), Some(Format::Woff2))],
    font_family: "Encode Sans".into(),
    font_weight: (Weight::Number(400), None),
}
}

@media

The syntax is different to @media rules in css:

  • specifying media type is not optional
  • ! instead of not
  • CamelCase instead of kebab-case
  • && instead of and
  • no grouping rules in not clauses

So these two would be equivalent:

#![allow(unused)]
fn main() {
@media All && MaxWidth(css::unit!(1023 px)) {
    html {
        css::background_color!(rgb 0xFF_00_00),
    }
}
}
@media all and (max-width: 1023px) {
    html {
        background-color: #FF0000;
    }
}

Support for @keyframes and @page is planned.

Colors

Color property macros like css::color! and css::fill! and the like have shorthands for full-alpha RGB colors as well as grayscale.

css::color!(rgb 0xFF_00_00), // same as css::color!(0xFF_00_00_FF) or #F00 in css
css::color!(gray 0xAD), // same as css::color!(0xAD_AD_AD_FF) or #ADADAD in css

Css named colors also can be used

css::color!(css::color::PALEVIOLETRED),
css::color!(css::color::GREEN),

Every way to make a class

Apart from regular .class()/.set_class() options there's several others:

  • .mark::<T>()/.unmark::<T>() - can generate classes from any type for targeted selection.
use hobo::create as e;

struct ButtonMarker;

e::div()
    .class(css::style!(
        .& >> .[ButtonMarker] {
            css::cursor!(pointer),
        }
    ))
    .child(e::div()
        .mark::<ButtonMarker>()
        .text("button 1")
    )
    .child(e::div()
        .mark::<ButtonMarker>()
        .text("button 2")
    )

Every call to .class()/.set_class() will append a new class - if you want to override an existing one, there are two options:

  • .set_class_typed::<Type>(style) - generates a tag from a Type. This is usually the preferred method, in the rare case that you need to override classes.
use hobo::create as e;

struct Flexible;

e::div()
    .class((css::display!(flex), css::background_color!(css::color::RED)))
    .class_typed::<Flexible>((
        css::flex_direction!(row),
        css::width!(100 px),
    ))
    .with(|&element| element.add_on_click(move |_| {
        element
            .set_class_typed::<Flexible>((
                css::flex_direction!(column),
                css::height!(100 px),
            ))
    }))
  • .set_class_tagged::<Tag: Hash>(tag, style) - Similar to .set_class_tagged, but uses an instance of a type rather than Type. The regular .class() method uses this internally with just an incrementing u64 for a tag.
use hobo::create as e;

e::div()
    .class(css::display!(flex))
    .class_tagged("Flexible", (
        css::flex_direction!(row),
        css::width!(100 px),
    ))
    .on_click(|&element| {
        element
            .set_class_tagged("Flexible", (
                css::flex_direction!(column),
                css::height!(100 px),
            ))
    })

Prefer using this over .set_class_typed if your tag is computed at runtime.

  • signals - you can have your classes be set reactively, in response to some changes in a Mutable. This is the preferred method for anything reactive, such as switching between themes:
enum Theme {
    Light,
    Dark,
}

let theme = Mutable::new(Theme::Light);

e::div()
    .class_typed_signal::<Theme, _, _>(theme.signal().map(|theme| {
        match theme {
            Theme::Light => css::background_color!(css::color::WHITE),
            Theme::Dark => css::background_color!(css::color::BLACK),
        }
    }))
    .component(theme)

Managing state and relations with Components

The next few chapters will outline how hobo deals with shared state.

Queries

Queries allow finding individual Entities or collections of Entities. Best shown by examples:

struct Foo {
    // some fields
}

// find the first (presumably only) entity with some component Foo
let (entity, _) = hobo::find_one::<(Entity, With<Foo>)>();
let element = hobo::Element(entity);
element.set_text("This entity has Foo");
struct Frobnicator {
    num_fraculations: u32,
    // other fields
}

// find all entities with a Frobnicator component and mutate it
// perhaps as a result of some combined transformation
for frobnicator in hobo::find::<&mut Frobnicator>() {
    frobnicator.num_fraculations += 1;
}

Queries are tuples of & T, &mut T or With<T> where T is some component or, as a special case, Entity. The result of hobo::find (or hobo::find_one) are tuples where each member is what was requested by the query (With<T> will always return true in its position because any entity that doesn't have T won't be included in the output).

Queries are also often useful to establish relations with distant Elements. For example, an Element in one part of the DOM can get an Element from a completely unrelated part of the DOM.

use hobo::create as e;

struct SettingsData {
    speed: f32,
}

let settings_container = e::div()
    // etc
    .component(SettingsData { speed: 0.35 })

// -- snip --

let unrelated_display = e::div()
    //etc
    .text(hobo::find_one::<&SettingsData>().speed.to_string())

Signals

Hobo has some useful reactivity facilities. The core of this is futures_signals::signal::Mutable, from which signals are created, check the futures-signals documentation for details on how to do that.

Hobo re-exports futures_signals as hobo::signals.

  • text_signal/set_text_signal - calls set_text whenever the signal value changes
  • child_signal/add_child_signal - initially creates an empty div, then calls .replace_with every time the signal value changes
  • class_signal/set_class_signal and class_typed_signal/set_class_typed_signal and class_tagged_signal/set_class_tagged_signal - calls set_class_tagged whenever the signal value changes
    • will always replace the first class so take care
  • attr_signal/set_attr_signal and bool_attr_signal/set_bool_attr_signal - calls set_attr whenever the signal value changes
  • style_signal/set_style_signal - calls set_style whenever the signal value changes
  • mark_signal - calls mark/unmark whenever the signal value changes

Building the DOM

Assembling elements is usually done via function chaining, but every function has a non-chained variant for use in loops or in case ownership gets tricky.

Here's an example of a somewhat involved element:

#![allow(unused)]
fn main() {
pub use hobo::{prelude::*, create as e};

#[derive(hobo::Element)]
pub struct Input {
    element: e::Div,
    pub input: e::Input,
}

impl Input {
    pub fn new(caption_text: &str, svg: Option<e::Svg>) -> Self {
        let input = e::input()
            // shortcut for .attr(web_str::r#type(), web_str::text())
            .type_text()
            .class(css::class! {
                // some style
            });

        let caption_text = e::div()
            .text(caption_text)
            .class(css::class! {
                // some style
            });

        let mut element = e::div()
            .class(css::style! {
                // some style
            })
            .child(input)
            .child(caption_text);

        if let Some(svg) = svg {
            element.add_child(
                svg.class(css::class! {
                    // some style
                })
            );
        }

        Self { element, input }
    }
}
}

.children()

Same as .child() but can consume an impl IntoIterator, convenient when taking a Vec<T> as an argument in list-like element constructors.
There is no .children_signal() but it could potentially exist - PRs welcome!

Chaining vs non-chaining syntax

Most functions have a chaining syntax, handy when constructing the element, and also non-chaining syntax for use in loops or other contexts. The convention is .<foo> for chaining and .add_<foo> for non-chaining. This goes against the more common Rust convention of .with_<foo> being the chaining syntax, this is because most code will be simple elements constructed in bulk, so most of these calls will be chaining so a shorter name is preferred.

  • .child()/.child_signal() vs .add_child()/.add_child_signal()
  • .children() vs .add_children()
  • .class()/.class_tagged()/.class_typed()/.class_signal() vs .set_class()/.set_class_tagged()/.set_class_typed()/.set_class_signal()
  • .style()/.style_signal() vs .set_style()/.set_style_signal()
  • .attr()/.bool_attr()/.attr_signal()/.bool_attr_signal() vs .set_attr()/.set_bool_attr()/.set_attr_signal()/.set_bool_attr_signal()
  • .<event>() vs .add_<event>()
  • .text()/.text_signal() vs .set_text()/.set_text_signal()
  • .component() vs .add_component()

Other utilities

This chapter outlines useful helpers for common tasks that didn't make it into the core.

web_str

The web_str module is just a bunch of commonly used interned strings. It includes all element names, all event names and a bunch of common attributes and values like class, min, max, checked, href, readonly, etc. If something is missing - PRs welcome!

To read more about what is string interning and why is it useful: wasm-bindgen docs.

Events

There is a simple way to fire and respond to global events.

pub use hobo::{
    prelude::*,
    create as e,
};

struct MyEvent(u64);

fn make_foo() -> impl hobo::AsElement {
    e::div()
        // etc children and styles
        .component(hobo::events::on(move |&MyEvent(x)| {
            // do something with x
        }))
}

// -- snip --

hobo::events::fire(&MyEvent(123));

The subscribers are notified based on event type, so it's better to create new types for different events rather than fire an event with a string or an enum.

Recipes

This chapter outlines common idioms and patterns.

Logging

There is no stdout in the browser so the simplest way is to use the log crate with wasm-logger and console_error_panic_hook to see nicely formatted errors:

#[wasm_bindgen(start)]
pub fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    console_error_panic_hook::set_once();

    // etc init and mounting of elements
    
    log::info!("it works!");
}

Elements that change

Since there's no VDOM, rebuilding the DOM is done manually by literally rebuilding the altered parts. It is on the developer to minimize this to maintain element focus, scroll position, performance, etc. The same goes for styling - any complex modification is best expressed as recreating the whole style.

However, most modifications can often be expressed with signals, with some child, style or text of an element just being a result of some computation based on one or multiple Mutables. With regards to styling in particular, most of the style is probably not going to change, with only minor changes based on something like theme.

.class_typed_signal::<Theme, _, _>(theme.signal().map(|theme| {
    match theme {
        Theme::Light => css::background_color!(css::color::WHITE),
        Theme::Dark => css::background_color!(css::color::BLACK),
    }
}))

SVGs

There is a way to conveniently create inline SVGs without rewriting them manually with hobo's syntax.

thread_local! {
    static LAST_ID: RefCell<u64> = RefCell::new(0);
}

fn get_svg_element(xml_node: &roxmltree::Node, id: u64) -> web_sys::SvgElement {
    let node: web_sys::SvgElement = wasm_bindgen::JsCast::unchecked_into(document().create_element_ns(Some(wasm_bindgen::intern("http://www.w3.org/2000/svg")), xml_node.tag_name().name()).unwrap());

    for attribute in xml_node.attributes() {
        // need to fixup ids to avoid id collisions in html if the same icon is used multiple times
        if attribute.name() == "id" {
            node.set_attribute(wasm_bindgen::intern(attribute.name()), &format!("{}{:x}", attribute.value(), id)).unwrap();
        } else {
            let mut value = attribute.value().to_owned();
            // optimistic expectation that ids only used in url references
            if value.contains("url(#") {
                value = value.replace(')', &format!("{:x})", id))
            }
            node.set_attribute(wasm_bindgen::intern(attribute.name()), &value).unwrap();
        }
    }

    for child in xml_node.children().filter(roxmltree::Node::is_element) {
        node.append_child(&get_svg_element(&child, id)).unwrap();
    }

    node
}

macro_rules! svg {
    ($($name:ident => $address:expr),*$(,)*) => {$(
        #[must_use]
        pub fn $name() -> e::Svg {
            let id = LAST_ID.with(move |last_id| {
                let mut last_id = last_id.borrow_mut();
                let id = *last_id;
                *last_id += 1;
                id
            });
            let element: web_sys::SvgElement = get_svg_element(&roxmltree::Document::parse(include_str!($address)).unwrap().root_element(), id);
            e::Svg(hobo::create::svg_element(&element))
        }
    )*};
}

svg![
    logo => r"../../public/img/icons/etc/logo.svg",
    discord => r"../../public/img/icons/shapes/discord.svg",
];

Constructing inline SVGs

Of course, if you need to algorithmically construct an svg, such as if you're making a chart, you can do that too:

#![allow(unused)]
fn main() {
let svg = e::svg()
    .attr(web_str::viewBox(), "-1 -1 2 2")
    .child(e::circle()
        .attr(web_str::cx(), "0")
        .attr(web_str::cy(), "0")
        .attr(web_str::r(), "1")
        .class((
            css::fill!(colors::gray6),
        ))
    );
}

Async and .is_dead()

Be careful accessing entities with abandon from an async context. Make sure to check that your entity is still mounted by the time your async computations finish and you're trying to change something.

use std::future::Future;

pub fn spawn_complain<T>(x: impl Future<Output = anyhow::Result<T>> + 'static) {
    wasm_bindgen_futures::spawn_local(async move { if let Err(e) = x.await {
        log::error!("{:?}", e);
    }});
}

e::div()
    .with(move |&element| spawn_complain(async move {
        let value = do_some_request_or_something().await?;
        if element.is_dead() { return Ok(()); }
        element.set_text(value);
        Ok(())
    }))

This isn't necessary outside of async context because wasm is single-threaded so your element can't get unmounted due to user actions, but in some complex scenarios it might be useful anyway.