1 个不稳定版本
0.0.1 | 2020年5月9日 |
---|
#18 in #ui-elements
2KB
Savory
Rust/Wasm 前端库,用于构建用户界面。
Savory 是一个基于 Seed 的构建用户界面的库
功能
- 视图:视图可以是实现
View
特性的任何类型,或任何返回Node
的独立函数,视图可以是特质对象,这使得它们非常易于组合。 - 元素:Savory 在构建有状态的 UI 时使用元素作为核心构建单元。元素拥有自己的状态,并通过消息处理用户输入。
- UI 元素集合:Savory 附带一系列可重用和可主题化的 UI 元素。
- 主题:UI 元素可以通过实现
ThemeImpl
特性的任何类型进行主题化,主题对元素的外观有完全的控制。 - 类型化 HTML:使用类型化 CSS 和 HTML 属性,Savory 尽量不依赖于字符串来创建 CSS 和 HTML 属性,因为这些可能导致难以调试的错误。
- 增强 Seed API:增强 Seed API,使使用
Node
、Orders
更加有趣。
Savory 旨在让编写 UI 元素变得有趣且无样板代码。
截图
示例
在这里,我们将创建一个与 Elm 教程 中相同的计数器应用程序,然后我们将使用样式化和可重用的元素编写相同的应用程序。
简单计数器
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();
}
预览:
计数器作为元素
现在我们将创建一个计数器元素和一个应用程序元素,这展示了如何创建父子元素,以及如何创建可复用和可样式的元素。
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();
}
预览:
在这个例子中有许多事情发生,首先我们创建了元素结构 Counter
,并定义了其属性、事件和样式类型,所有这些都是在 derive 宏 Element
的帮助下完成的,我们将在后面解释它是如何工作的,然后我们在 init
函数中定义了一个包含计数器元素的应用程序元素并初始化它。最后,我们只是调用 start
方法来挂载并启动应用程序。
使用 Savory Elements 的计数器!
Savory 提供了一系列元素,我们将使用它们来构建计数器应用程序,并查看 Savory 元素给我们带来了哪些特性。
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();
}
预览:
正如你所见,这个例子行数更少,特性更多,真是一举两得。
事实上,Savory 元素有一个 SpinEntry
元素,它的工作方式就像计数器一样,我们在我们的例子中就这样使用了它,所以 Savory 尝试提供你最需要的元素,这样你就不需要从头开始构建每件事,即使你想要以某种方式构建自己的元素,你仍然可以使用 Savory 元素作为你自己的元素的建设块。
快速入门
首先,将 savory crates 添加到你的 Cargo.toml
文件中
savory-core = "0.5.0"
savory-html = "0.5.0"
savory-elements = "0.5.0"
wasm-bindgen = "0.2.55"
待办事项
生态系统
savory
- savory CLIsavory-core
- 用于构建用户界面的库savory-html
- Savory 的类型化 HTMLsavory-elements
- 基于 Savory 的 UI 元素savory-derive
- 辅助 derivesavory-theme
- Savory 的官方主题savory-icons
- Savory 的可复用图标
许可证
根据您的选择,许可协议为 Apache License, Version 2.0 或 MIT 许可证。
除非您明确说明,否则任何有意提交以包含在 Savory 中的贡献,如 Apache-2.0 许可证中定义的,应按上述方式双重许可,不附加任何其他条款或条件。