hobo
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 u64
s 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::HtmlElement
s. 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 thestyle
attribute, which can only take a bunch of properties without any selectors, so a property tuple is used..class()
,.set_class()
andtagged
ortyped
variants use theclass
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
becomediv >> a
because Rust doesn't have semantic whitespaces.- selectors like
div.active
work mostly the same (except have to be written likediv.("active")
ordiv .("active")
)
- selectors like
- 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
- there's an escape hatch in
- 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 orstyled-components
- in other words, it's similar to
.[T]
whereT
is some marker type will be replaced with the generated classname for the typeT
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 thecss::<prop>()
family of macros (e.g.css::width!()
,css::flex_shrink!()
, etc)Vec<css::Property>
()
&'static str
andString
as escape hatchesFnOnce(&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
orcss::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 ofnot
CamelCase
instead ofkebab-case
&&
instead ofand
- 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 aType
. 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 thanType
. The regular.class()
method uses this internally with just an incrementingu64
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
- callsset_text
whenever the signal value changeschild_signal
/add_child_signal
- initially creates an emptydiv
, then calls.replace_with
every time the signal value changesclass_signal
/set_class_signal
andclass_typed_signal
/set_class_typed_signal
andclass_tagged_signal
/set_class_tagged_signal
- callsset_class_tagged
whenever the signal value changes- will always replace the first class so take care
attr_signal
/set_attr_signal
andbool_attr_signal
/set_bool_attr_signal
- callsset_attr
whenever the signal value changesstyle_signal
/set_style_signal
- callsset_style
whenever the signal value changesmark_signal
- callsmark
/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.