1 个不稳定版本

0.0.1 2020年5月9日

#19 in #ui-elements

MIT/Apache

2KB

Savory

Rust / Wasm 前端库,用于构建用户界面。

master docs · crate info · pipeline · rustc version · unsafe forbidden

Savory 是一个基于 Seed 构建用户界面的库

文档

特性

  • 视图:视图可以是实现 View 特质的任何类型或返回 Node 的任何独立函数,视图可以是特质的对象,这使得它们非常易于组合。
  • 元素:当构建具有状态的 UI 时,Savory 使用元素作为核心构建单元。元素拥有自己的状态并通过消息处理用户输入。
  • UI 元素的集合:Savory 随附了一系列可重用和可主题化的 UI 元素。
  • 主题:UI 元素可以通过实现 ThemeImpl 特质的任何类型进行主题化,主题可以完全控制元素的外观。
  • 类型化 HTML:使用类型化的 CSS 和 HTML 属性,Savory 尽量不依赖于字符串来创建 CSS 和 HTML 属性,因为这些可能会导致难以调试的错误。
  • 增强 Seed API:增强 Seed API,使得使用 NodeOrders 更加有趣。

Savory 试图让编写 UI 元素变得有趣且无需样板代码。

屏幕截图

Screenshot

示例

我们将创建在 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();
}

预览:Screenshot

源代码

计数器作为元素

现在我们将创建计数器元素和应用元素,这展示了如何创建父元素和子元素,以及如何创建可重用和可定制的元素。

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 完成的,我们将在后面解释它是如何工作的,然后我们定义了一个包含计数器元素的 app 元素,并在 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"

TODO

生态系统

许可证

根据您的选择,许可协议为 Apache License, Version 2.0MIT 许可证

除非您明确声明,否则根据 Apache-2.0 许可证定义的,您有意提交给 Savory 的任何贡献,都应如上所述双许可,不附加任何额外条款或条件。

无运行时依赖