-
Notifications
You must be signed in to change notification settings - Fork 155
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
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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"] } |
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" |
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. |
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> |
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> { | ||
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 render(&self) -> Node<Ms> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the difference between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uhm, yeah. I'm not sure what I was thinking there :) Fixed in b57c930 |
||
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 | ||
} | ||
} |
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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use super::Node; | ||
|
||
pub trait Component<Ms> { | ||
fn render(&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.render() | ||
} |
There was a problem hiding this comment.
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.