#savory #element #themes #ui-elements #counter #html

savory-derive

为Savory库提供自定义 derive 支持

1个不稳定版本

0.5.1 2020年5月9日

#5 in #savory

MIT/Apache

23KB
516

Savory

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

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

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

文档

功能

  • 视图:视图可以是任何实现 View 特性的类型或任何返回 Node 的独立函数,视图可以是特例对象,这使得它们非常易于组合。
  • 元素:Savory在构建有状态的UI时使用元素作为核心构建单元。元素拥有自己的状态,并通过消息处理用户输入。
  • 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

源代码

Counter As 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();
}

预览:截图

源代码

在这个例子中有许多事情发生,首先我们创建了元素结构Counter,并定义了它的属性、事件和样式类型,这一切都是通过Element宏完成的,我们将在后面解释它是如何工作的,然后我们定义了一个包含计数器元素的应用程序元素,并在init函数中初始化它。最后,我们只需调用start方法来挂载和启动应用程序。

Counter使用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"

待办事项

生态系统

许可

根据你的选择,许可协议为Apache License,版本2.0MIT许可证

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

依赖关系

~1.5MB
~41K SLoC