1 unstable release
0.0.1 | May 9, 2020 |
---|
#20 in #ui-elements
2KB
Savory
Rust / Wasm frontend library for building user interfaces.
Savory is library for building user interface based on Seed
Features
- Views: Views can be any type implement
View
trait or any standalone function that returnsNode
, views can be trait object which make them very composable. - Elements: Savory uses elements as core building unit when building stateful UI. Elements owns thier state and handle user inputs via messages.
- Collection of UI elements: Savory ships with collection of resuable and themeable UI elements.
- Theme: UI elements can be themed by any type that implement
ThemeImpl
trait, themes have full control on the element appearance. - Typed HTML: Use typed CSS and HTML attributes, Savory try hard not to rely on strings when creating CSS and HTML attributes since these can produce hard to debug bugs.
- Enhance Seed API: Enhancement on Seed API that makes working with
Node
,Orders
fun.
Savory tries to make writing UI elements fun and boilerplate free.
Screenshot
Examples
Here we will create the same counter app found in Elm tutorial, then we will write the same app but with styled and reusable element.
Simple Counter
use savory_core::prelude::*;
use savory_html::prelude::*;
use wasm_bindgen::prelude::*;
// app element (the model)
pub struct Counter(i32);
// app message
pub enum Msg {
Increment,
Decrement,
}
impl Element for Counter {
type Message = Msg;
type Config = Url;
// initialize the app in this function
fn init(_: Url, _: &mut impl Orders<Msg>) -> Self {
Self(0)
}
// handle app messages
fn update(&mut self, msg: Msg, _: &mut impl Orders<Msg>) {
match msg {
Msg::Increment => self.0 += 1,
Msg::Decrement => self.0 -= 1,
}
}
}
impl View<Node<Msg>> for Counter {
// view the app
fn view(&self) -> Node<Msg> {
let inc_btn = html::button().add("Increment").on_click(|_| Msg::Increment);
let dec_btn = html::button().add("Decrement").on_click(|_| Msg::Decrement);
html::div()
.add(inc_btn)
.add(self.0.to_string())
.add(dec_btn)
}
}
#[wasm_bindgen(start)]
pub fn view() {
// mount and start the app at `app` element
Counter::start();
}
Preview:
Counter As Element
Now we will make counter element and an app element this illustrate how to make parent and child element, and how to make resuable and stylable element.
use savory_core::prelude::*;
use savory_elements::prelude::*;
use savory_html::{
css::{unit::px, values as val, Color, St},
prelude::*,
};
use wasm_bindgen::prelude::*;
#[derive(Element)]
#[element(style(inc_btn, dec_btn))]
pub struct Counter {
#[element(config(default = "10"))]
value: i32,
}
pub enum Msg {
Increment,
Decrement,
}
impl Element for Counter {
type Message = Msg;
type Config = Config;
fn init(config: Self::Config, _: &mut impl Orders<Msg>) -> Self {
Self {
value: config.value,
}
}
fn update(&mut self, msg: Msg, _: &mut impl Orders<Msg>) {
match msg {
Msg::Increment => self.value += 1,
Msg::Decrement => self.value -= 1,
}
}
}
impl View<Node<Msg>> for Counter {
fn view(&self) -> Node<Msg> {
// sharde style for buttons
let style_btns = |conf: css::Style| {
conf.add(St::Appearance, val::None)
.background(Color::SlateBlue)
.text(Color::White)
.and_border(|conf| conf.none().radius(px(4)))
.margin(px(4))
.padding(px(4))
};
// create style
let style = Style::default()
.and_inc_btn(style_btns)
.and_dec_btn(style_btns);
// increment button node
let inc_btn = html::button()
.class("inc-btn")
.set(style.inc_btn)
.on_click(|_| Msg::Increment)
.add("Increment");
// decrement button node
let dec_btn = html::button()
.class("dec-btn")
.set(style.dec_btn)
.on_click(|_| Msg::Decrement)
.add("Decrement");
// contianer node
html::div()
.add(dec_btn)
.add(self.value.to_string())
.add(inc_btn)
}
}
// convenient way to convert Config into Counter
impl Config {
pub fn init(self, orders: &mut impl Orders<Msg>) -> Counter {
Counter::init(self, orders)
}
}
// App Element ---
pub enum AppMsg {
Counter(Msg),
}
pub struct MyApp {
counter_element: Counter,
}
impl Element for MyApp {
type Message = AppMsg;
type Config = Url;
fn init(_: Url, orders: &mut impl Orders<AppMsg>) -> Self {
Self {
counter_element: Counter::config()
// give it starting value. 10 will be used as default value if
// we didn't pass value
.value(100)
.init(&mut orders.proxy(AppMsg::Counter)),
}
}
fn update(&mut self, msg: AppMsg, orders: &mut impl Orders<AppMsg>) {
match msg {
AppMsg::Counter(msg) => self
.counter_element
.update(msg, &mut orders.proxy(AppMsg::Counter)),
}
}
}
impl View<Node<AppMsg>> for MyApp {
fn view(&self) -> Node<AppMsg> {
self.counter_element.view().map_msg(AppMsg::Counter)
}
}
#[wasm_bindgen(start)]
pub fn view() {
// mount and start the app at `app` element
MyApp::start();
}
Preview:
A lot of things happening in this example, first we have create element struct
Counter
, and defined its properties, events and style types, this is all done
by the derive macro Element
which we will explain how it work later, then we
defined an app element that containes the counter element and initialize it in
the init
funtion. at the end we just call start
method to mount and start
the app.
Counter using Savory Elements!
Savory ships with collections of elements, and we will use them to build counter app and see what features Savory elements gives us.
use savory_core::prelude::*;
use savory_elements::prelude::*;
use wasm_bindgen::prelude::*;
pub struct MyApp {
spin_entry: SpinEntry,
}
pub enum Msg {
SpinEntry(spin_entry::Msg),
}
impl Element for MyApp {
type Message = Msg;
type Config = Url;
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Self {
let spin_entry = SpinEntry::config()
.min(-40.)
.placeholder(44.)
.step(5.)
.max(40.)
.init(&mut orders.proxy(Msg::SpinEntry));
Self { spin_entry }
}
fn update(&mut self, msg: Msg, orders: &mut impl Orders<Msg>) {
match msg {
Msg::SpinEntry(msg) => self
.spin_entry
.update(msg, &mut orders.proxy(Msg::SpinEntry)),
};
}
}
impl View<Node<Msg>> for MyApp {
fn view(&self) -> Node<Msg> {
Flexbox::new()
.center()
.add(self.spin_entry.view().map_msg(Msg::SpinEntry))
.and_size(|conf| conf.full())
.view()
}
}
#[wasm_bindgen(start)]
pub fn view() {
MyApp::start();
}
Preview:
As you can see this example have less lines and more features, what a neat.
It happens that Savory elements have SpinEntry
element, which work just like
counter, and we used it in our example as simple as that, so Savory tries to
provides you the most needed elements so you don't need to build every thing
from scratch, even if you want build your own element in some way, you can still
use Savory elements as building block in your own element.
Qucikstart
first thing first, add savory crates into your Cargo.toml
file:
savory-core = "0.5.0"
savory-html = "0.5.0"
savory-elements = "0.5.0"
wasm-bindgen = "0.2.55"
TODO
Ecosystem
savory
- savory CLIsavory-core
- Library for building user interfacesavory-html
- Typed HTML for Savorysavory-elements
- UI Elements based on Savorysavory-derive
- Helper derivessavory-theme
- The official theme for Savorysavory-icons
- Reusable icons for Savory
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Savory by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.