From 905521171d453a258a180d7ce73aafadfd271664 Mon Sep 17 00:00:00 2001 From: glennsl Date: Sat, 1 May 2021 19:44:37 +0200 Subject: [PATCH 1/4] initial experimental component API example --- Cargo.lock | 7 ++ Cargo.toml | 1 + examples/experimental_component/Cargo.toml | 11 +++ examples/experimental_component/Makefile.toml | 27 ++++++ examples/experimental_component/README.md | 16 ++++ examples/experimental_component/index.html | 18 ++++ examples/experimental_component/src/button.rs | 94 +++++++++++++++++++ examples/experimental_component/src/lib.rs | 86 +++++++++++++++++ 8 files changed, 260 insertions(+) create mode 100644 examples/experimental_component/Cargo.toml create mode 100644 examples/experimental_component/Makefile.toml create mode 100644 examples/experimental_component/README.md create mode 100644 examples/experimental_component/index.html create mode 100644 examples/experimental_component/src/button.rs create mode 100644 examples/experimental_component/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c9aef7ef1..76551dc1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,6 +520,13 @@ dependencies = [ "serde", ] +[[package]] +name = "experimental_component" +version = "0.1.0" +dependencies = [ + "seed", +] + [[package]] name = "failure" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 6636dd551..c17658a40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ members = [ "examples/custom_elements", "examples/drop_zone", "examples/el_key", + "examples/experimental_component", "examples/graphql", "examples/i18n", "examples/markdown", diff --git a/examples/experimental_component/Cargo.toml b/examples/experimental_component/Cargo.toml new file mode 100644 index 000000000..a1ec991d3 --- /dev/null +++ b/examples/experimental_component/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "experimental_component" +version = "0.1.0" +authors = ["glennsl"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +seed = {path = "../../"} diff --git a/examples/experimental_component/Makefile.toml b/examples/experimental_component/Makefile.toml new file mode 100644 index 000000000..e188fabea --- /dev/null +++ b/examples/experimental_component/Makefile.toml @@ -0,0 +1,27 @@ +extend = "../../Makefile.toml" + +# ---- BUILD ---- + +[tasks.build] +alias = "default_build" + +[tasks.build_release] +alias = "default_build_release" + +# ---- START ---- + +[tasks.start] +alias = "default_start" + +[tasks.start_release] +alias = "default_start_release" + +# ---- TEST ---- + +[tasks.test_firefox] +alias = "default_test_firefox" + +# ---- LINT ---- + +[tasks.clippy] +alias = "default_clippy" diff --git a/examples/experimental_component/README.md b/examples/experimental_component/README.md new file mode 100644 index 000000000..dc0f17017 --- /dev/null +++ b/examples/experimental_component/README.md @@ -0,0 +1,16 @@ +## Experimental component API example + +Demonstrates a component API that has: + +- Labeled required properties, ensured to be present at compile-time +- Labeled optional properties +- Polymorphic for all properties +- A convenient consumer interface, through a stupidly simple macro that is very easy to understand + +--- + +```bash +cargo make start +``` + +Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser. diff --git a/examples/experimental_component/index.html b/examples/experimental_component/index.html new file mode 100644 index 000000000..0dfc2eb95 --- /dev/null +++ b/examples/experimental_component/index.html @@ -0,0 +1,18 @@ + + + + + + + Experimental component API example + + + +
+ + + + diff --git a/examples/experimental_component/src/button.rs b/examples/experimental_component/src/button.rs new file mode 100644 index 000000000..5fad6f6b3 --- /dev/null +++ b/examples/experimental_component/src/button.rs @@ -0,0 +1,94 @@ +#![allow(dead_code)] + +use seed::{prelude::*, *}; +use std::borrow::Cow; +use std::rc::Rc; + +pub struct Button { + pub label: S, +} + +impl>> Button { + pub fn into_component(self) -> Component { + Component { + label: self.label.into(), + outlined: false, + disabled: false, + on_clicks: Vec::new(), + } + } +} + +pub struct Component { + label: Cow<'static, str>, + outlined: bool, + disabled: bool, + on_clicks: Vec Ms>>, +} + +impl Component { + pub const fn outlined(mut self, outlined: bool) -> Self { + self.outlined = outlined; + self + } + + pub const fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn on_click(mut self, on_click: impl FnOnce() -> Ms + Clone + 'static) -> Self { + self.on_clicks.push(Rc::new(move || on_click.clone()())); + self + } + + pub fn into_node(self) -> Node { + let attrs = { + let mut attrs = attrs! {}; + + if self.disabled { + attrs.add(At::from("aria-disabled"), true); + attrs.add(At::TabIndex, -1); + attrs.add(At::Disabled, AtValue::None); + } + + attrs + }; + + let css = { + let color = "teal"; + + let mut css = style! { + St::TextDecoration => "none", + }; + + if self.outlined { + css.merge(style! { + St::Color => color, + St::BackgroundColor => "transparent", + St::Border => format!("{} {} {}", px(2), "solid", color), + }); + } else { + css.merge(style! { St::Color => "white", St::BackgroundColor => color }); + }; + + if self.disabled { + css.merge(style! {St::Opacity => 0.5}); + } else { + css.merge(style! {St::Cursor => "pointer"}); + } + + css + }; + + let mut button = button![css, attrs, self.label]; + + if !self.disabled { + for on_click in self.on_clicks { + button.add_event_handler(ev(Ev::Click, move |_| on_click())); + } + } + + button + } +} diff --git a/examples/experimental_component/src/lib.rs b/examples/experimental_component/src/lib.rs new file mode 100644 index 000000000..819f87487 --- /dev/null +++ b/examples/experimental_component/src/lib.rs @@ -0,0 +1,86 @@ +use seed::{prelude::*, *}; + +mod button; +use button::Button; + +// ------ ------ +// Init +// ------ ------ + +fn init(_: Url, _: &mut impl Orders) -> Model { + Model::default() +} + +// ------ ------ +// Model +// ------ ------ + +type Model = i32; + +// ------ ------ +// Update +// ------ ------ + +enum Msg { + Increment(i32), + Decrement(i32), +} + +#[allow(clippy::needless_pass_by_value)] +fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { + match msg { + Msg::Increment(d) => *model += d, + Msg::Decrement(d) => *model -= d, + } +} + +// ------ ------ +// View +// ------ ------ + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn view(model: &Model) -> Node { + div![ + style! { + St::Display => "flex" + St::AlignItems => "center", + }, + comp![Button { label: "-100" }, + disabled => true, + on_click => || Msg::Decrement(100), + ], + comp![Button { label: "-10" }, on_click => || Msg::Decrement(10)], + comp![Button { label: "-1" }, + outlined => true, + on_click => || Msg::Decrement(1), + ], + div![style! { St::Margin => "0 1em" }, model], + comp![Button { label: "+1" }, + outlined => true, + on_click => || Msg::Increment(1), + ], + comp![Button { label: "+10" }, on_click => || Msg::Increment(10)], + comp![Button { label: "+100" }, + disabled => true, + on_click => || Msg::Increment(100), + ] + ] +} + +#[macro_export] +macro_rules! comp { + ($init:expr, $($opt_field:ident => $opt_val:expr),* $(,)?) => { + $init.into_component() + $( .$opt_field($opt_val) )* + .into_node() + }; +} + +// ------ ------ +// Start +// ------ ------ + +#[wasm_bindgen(start)] +pub fn start() { + App::start("app", init, update, view); +} From 1c9fc180f2c061450811af89cb4937be7bb69886 Mon Sep 17 00:00:00 2001 From: glennsl Date: Sun, 2 May 2021 19:29:46 +0200 Subject: [PATCH 2/4] add Component trait and feature-flagged comp! macro to seed proper --- CHANGELOG.md | 1 + Cargo.toml | 1 + examples/experimental_component/Cargo.toml | 2 +- examples/experimental_component/src/button.rs | 15 +++++++++------ examples/experimental_component/src/lib.rs | 9 --------- src/lib.rs | 4 ++-- src/shortcuts.rs | 14 ++++++++++++++ src/virtual_dom.rs | 2 ++ src/virtual_dom/component.rs | 17 +++++++++++++++++ 9 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 src/virtual_dom/component.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0571f1e..8b9c83b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Added macro `raw_svg!` (#589). - Added `browser::dom::Namespace` to `prelude`. - Adapted to Rust 1.51.0. +- Added an experimental component API, feature-flagged behind `experimental-component-api`, and the `experimental-component` example to demonstrate its use. ## v0.8.0 - [BREAKING] Rename `linear_gradient!` to `linearGradient!` for consistency with the other svg macros (same with `radial_gradient!` and `mesh_gradient!`) (#377). diff --git a/Cargo.toml b/Cargo.toml index c17658a40..194d7eb7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,3 +164,4 @@ exclude = [ default = ["panic-hook"] panic-hook = ["console_error_panic_hook"] markdown = ["pulldown-cmark"] +experimental-component-api = [] diff --git a/examples/experimental_component/Cargo.toml b/examples/experimental_component/Cargo.toml index a1ec991d3..5386105b5 100644 --- a/examples/experimental_component/Cargo.toml +++ b/examples/experimental_component/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" crate-type = ["cdylib"] [dependencies] -seed = {path = "../../"} +seed = { path = "../../", features = ["experimental-component-api"] } diff --git a/examples/experimental_component/src/button.rs b/examples/experimental_component/src/button.rs index 5fad6f6b3..65bcf0f0a 100644 --- a/examples/experimental_component/src/button.rs +++ b/examples/experimental_component/src/button.rs @@ -9,8 +9,8 @@ pub struct Button { } impl>> Button { - pub fn into_component(self) -> Component { - Component { + pub fn into_component(self) -> ButtonComponent { + ButtonComponent { label: self.label.into(), outlined: false, disabled: false, @@ -19,14 +19,15 @@ impl>> Button { } } -pub struct Component { +#[allow(clippy::module_name_repetitions)] +pub struct ButtonComponent { label: Cow<'static, str>, outlined: bool, disabled: bool, on_clicks: Vec Ms>>, } -impl Component { +impl ButtonComponent { pub const fn outlined(mut self, outlined: bool) -> Self { self.outlined = outlined; self @@ -41,8 +42,10 @@ impl Component { self.on_clicks.push(Rc::new(move || on_click.clone()())); self } +} - pub fn into_node(self) -> Node { +impl Component for ButtonComponent { + fn render(&self) -> Node { let attrs = { let mut attrs = attrs! {}; @@ -84,7 +87,7 @@ impl Component { let mut button = button![css, attrs, self.label]; if !self.disabled { - for on_click in self.on_clicks { + for on_click in self.on_clicks.iter().cloned() { button.add_event_handler(ev(Ev::Click, move |_| on_click())); } } diff --git a/examples/experimental_component/src/lib.rs b/examples/experimental_component/src/lib.rs index 819f87487..6b4125434 100644 --- a/examples/experimental_component/src/lib.rs +++ b/examples/experimental_component/src/lib.rs @@ -67,15 +67,6 @@ fn view(model: &Model) -> Node { ] } -#[macro_export] -macro_rules! comp { - ($init:expr, $($opt_field:ident => $opt_val:expr),* $(,)?) => { - $init.into_component() - $( .$opt_field($opt_val) )* - .into_node() - }; -} - // ------ ------ // Start // ------ ------ diff --git a/src/lib.rs b/src/lib.rs index e914dd55b..a9365bf75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,8 +198,8 @@ pub mod prelude { // https://github.com/rust-lang-nursery/reference/blob/master/src/macros-by-example.md shortcuts::*, virtual_dom::{ - el_key, el_ref::el_ref, AsAtValue, At, AtValue, CSSValue, El, ElRef, Ev, EventHandler, - IntoNodes, Node, St, Tag, ToClasses, UpdateEl, UpdateElForIterator, View, + el_key, el_ref::el_ref, AsAtValue, At, AtValue, CSSValue, Component, El, ElRef, Ev, + EventHandler, IntoNodes, Node, St, Tag, ToClasses, UpdateEl, UpdateElForIterator, View, }, }; pub use indexmap::IndexMap; // for attrs and style to work. diff --git a/src/shortcuts.rs b/src/shortcuts.rs index 68555c48a..bb218f2cc 100644 --- a/src/shortcuts.rs +++ b/src/shortcuts.rs @@ -553,3 +553,17 @@ macro_rules! key_value_pairs { } }; } + +#[cfg(feature = "experimental-component-api")] +/// Instantiates and renders a `Component` +/// +/// NOTE: This is an experimental API that requires the `experimental-component-api` feature. +#[macro_export] +macro_rules! comp { + ($init:expr, $($opt_field:ident => $opt_val:expr),* $(,)?) => { + $crate::virtual_dom::component::instantiate( + $init.into_component() + $( .$opt_field($opt_val) )* + ) + }; +} diff --git a/src/virtual_dom.rs b/src/virtual_dom.rs index 3dbb8c373..74f34cb6a 100644 --- a/src/virtual_dom.rs +++ b/src/virtual_dom.rs @@ -1,4 +1,5 @@ pub mod attrs; +pub mod component; pub mod el_ref; pub mod event_handler_manager; pub mod mailbox; @@ -11,6 +12,7 @@ pub mod values; pub mod view; pub use attrs::Attrs; +pub use component::Component; pub use el_ref::{el_ref, ElRef, SharedNodeWs}; pub use event_handler_manager::{EventHandler, EventHandlerManager, Listener}; pub use mailbox::Mailbox; diff --git a/src/virtual_dom/component.rs b/src/virtual_dom/component.rs new file mode 100644 index 000000000..0f5b31a9e --- /dev/null +++ b/src/virtual_dom/component.rs @@ -0,0 +1,17 @@ +use super::Node; + +pub trait Component { + fn render(&self) -> Node; +} + +#[allow(clippy::needless_pass_by_value)] +pub fn instantiate>(component: C) -> Node { + // TODO: This is where we'd create a boundary node and a state container + // that can then either be passed to `render` to be populated, or capture + // hook calls indirectly like React does. + // + // The boundary node will own the state container and remember the component + // configuration, so that it can do a local re-render when triggered by a + // hook. + component.render() +} From 4b1e71973d6965a08b817c317797d7329264ed4e Mon Sep 17 00:00:00 2001 From: glennsl Date: Sun, 2 May 2021 19:35:36 +0200 Subject: [PATCH 3/4] tweak experimental-component README --- examples/experimental_component/README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/experimental_component/README.md b/examples/experimental_component/README.md index dc0f17017..acff2a3e4 100644 --- a/examples/experimental_component/README.md +++ b/examples/experimental_component/README.md @@ -1,11 +1,6 @@ ## Experimental component API example -Demonstrates a component API that has: - -- Labeled required properties, ensured to be present at compile-time -- Labeled optional properties -- Polymorphic for all properties -- A convenient consumer interface, through a stupidly simple macro that is very easy to understand +Demonstrates how to use the experimental component API. --- From b57c930724e4a2a457873457d0326f54904af83a Mon Sep 17 00:00:00 2001 From: glennsl Date: Mon, 3 May 2021 19:10:53 +0200 Subject: [PATCH 4/4] render -> view --- examples/experimental_component/src/button.rs | 2 +- src/virtual_dom/component.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/experimental_component/src/button.rs b/examples/experimental_component/src/button.rs index 65bcf0f0a..a5bde1b0f 100644 --- a/examples/experimental_component/src/button.rs +++ b/examples/experimental_component/src/button.rs @@ -45,7 +45,7 @@ impl ButtonComponent { } impl Component for ButtonComponent { - fn render(&self) -> Node { + fn view(&self) -> Node { let attrs = { let mut attrs = attrs! {}; diff --git a/src/virtual_dom/component.rs b/src/virtual_dom/component.rs index 0f5b31a9e..8451cd10d 100644 --- a/src/virtual_dom/component.rs +++ b/src/virtual_dom/component.rs @@ -1,7 +1,7 @@ use super::Node; pub trait Component { - fn render(&self) -> Node; + fn view(&self) -> Node; } #[allow(clippy::needless_pass_by_value)] @@ -13,5 +13,5 @@ pub fn instantiate>(component: C) -> Node { // The boundary node will own the state container and remember the component // configuration, so that it can do a local re-render when triggered by a // hook. - component.render() + component.view() }