Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental component API #599

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ members = [
"examples/custom_elements",
"examples/drop_zone",
"examples/el_key",
"examples/experimental_component",
"examples/graphql",
"examples/i18n",
"examples/markdown",
Expand Down Expand Up @@ -163,3 +164,4 @@ exclude = [
default = ["panic-hook"]
panic-hook = ["console_error_panic_hook"]
markdown = ["pulldown-cmark"]
experimental-component-api = []
11 changes: 11 additions & 0 deletions examples/experimental_component/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "experimental_component"
version = "0.1.0"
authors = ["glennsl"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
seed = { path = "../../", features = ["experimental-component-api"] }
27 changes: 27 additions & 0 deletions examples/experimental_component/Makefile.toml
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 11 additions & 0 deletions examples/experimental_component/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Experimental component API example

Demonstrates how to use the experimental component API.

---

```bash
cargo make start
```

Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
18 changes: 18 additions & 0 deletions examples/experimental_component/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Experimental component API example</title>
</head>

<body>
<section id="app"></section>
<script type="module">
import init from '/pkg/package.js';
init('/pkg/package_bg.wasm');
</script>
</body>

</html>
97 changes: 97 additions & 0 deletions examples/experimental_component/src/button.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#![allow(dead_code)]

use seed::{prelude::*, *};
use std::borrow::Cow;
use std::rc::Rc;

pub struct Button<S> {
pub label: S,
}

impl<S: Into<Cow<'static, str>>> Button<S> {
pub fn into_component<Ms>(self) -> ButtonComponent<Ms> {
ButtonComponent {
label: self.label.into(),
outlined: false,
disabled: false,
on_clicks: Vec::new(),
}
}
}

#[allow(clippy::module_name_repetitions)]
pub struct ButtonComponent<Ms: 'static> {
Comment on lines +22 to +23
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of an unsightly wart for those who use clippy. Not entirely sure what to do about it, but probably either the trait or the naming convention here needs to be renamed.

label: Cow<'static, str>,
outlined: bool,
disabled: bool,
on_clicks: Vec<Rc<dyn Fn() -> Ms>>,
}

impl<Ms> ButtonComponent<Ms> {
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
}
}

impl<Ms> Component<Ms> for ButtonComponent<Ms> {
fn view(&self) -> Node<Ms> {
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.iter().cloned() {
button.add_event_handler(ev(Ev::Click, move |_| on_click()));
}
}

button
}
}
77 changes: 77 additions & 0 deletions examples/experimental_component/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use seed::{prelude::*, *};

mod button;
use button::Button;

// ------ ------
// Init
// ------ ------

fn init(_: Url, _: &mut impl Orders<Msg>) -> 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<Msg>) {
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<Msg> {
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),
]
]
}

// ------ ------
// Start
// ------ ------

#[wasm_bindgen(start)]
pub fn start() {
App::start("app", init, update, view);
}
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions src/shortcuts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) )*
)
};
}
2 changes: 2 additions & 0 deletions src/virtual_dom.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod attrs;
pub mod component;
pub mod el_ref;
pub mod event_handler_manager;
pub mod mailbox;
Expand All @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions src/virtual_dom/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use super::Node;

pub trait Component<Ms> {
fn view(&self) -> Node<Ms>;
}

#[allow(clippy::needless_pass_by_value)]
pub fn instantiate<Ms, C: Component<Ms>>(component: C) -> Node<Ms> {
// 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.view()
}