diff --git a/Cargo.lock b/Cargo.lock index 3948f30b..30075502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,6 +510,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 6636dd55..c17658a4 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 00000000..a1ec991d --- /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 00000000..e188fabe --- /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 00000000..dc0f1701 --- /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 00000000..0dfc2eb9 --- /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 00000000..c2ff8d62 --- /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) -> ButtonComponent { + ButtonComponent { + label: self.label.into(), + outlined: false, + disabled: false, + on_clicks: Vec::new(), + } + } +} + +pub struct ButtonComponent { + label: Cow<'static, str>, + outlined: bool, + disabled: bool, + on_clicks: Vec Ms>>, +} + +impl ButtonComponent { + 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 00000000..819f8748 --- /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); +}