Skip to content

Commit 7a63e5c

Browse files
committed
examples: component_builder
1 parent f0cc362 commit 7a63e5c

File tree

9 files changed

+444
-1
lines changed

9 files changed

+444
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
- [BREAKING] Rename `linear_gradient!` to `linearGradient!` for consistency with the other svg macros (same with `radial_gradient!` and `mesh_gradient!`) (#377).
55
- Fixed `base_path` with a trailing slash parsing / handling.
66
- Fixed `C` macro memory / WASM file size issue.
7-
- Added example `unsaved_changes` (#459).
7+
- Added examples `component_builder` and `unsaved_changes` (#459).
88
- Fixed `UrlRequested` handling (#459).
99
- [BREAKING] Added `Effect` variant `TriggeredHandler`.
1010
- [BREAKING] Renamed module `effects` to `effect`.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ members = [
119119
"examples/animation",
120120
"examples/auth",
121121
"examples/bunnies",
122+
"examples/component_builder",
122123
"examples/counter",
123124
"examples/canvas",
124125
"examples/custom_elements",

examples/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Intended as a demo of [Shipyard](https://github.com/leudz/shipyard) (Entity Comp
2222
### [Canvas](canvas)
2323
How to make a canvas element and use `ElRef`s.
2424

25+
## [Component builder](component_builder)
26+
How to write reusable views / components with builder pattern.
27+
2528
### [Counter](counter)
2629
Intended as a demo of basic functionality.
2730

examples/component_builder/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "component_builder"
3+
version = "0.1.0"
4+
authors = ["Martin Kavík <[email protected]>"]
5+
edition = "2018"
6+
7+
[lib]
8+
crate-type = ["cdylib"]
9+
10+
[dependencies]
11+
seed = {path = "../../"}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
extend = "../../Makefile.toml"
2+
3+
# ---- BUILD ----
4+
5+
[tasks.build]
6+
alias = "default_build"
7+
8+
[tasks.build_release]
9+
alias = "default_build_release"
10+
11+
# ---- START ----
12+
13+
[tasks.start]
14+
alias = "default_start"
15+
16+
[tasks.start_release]
17+
alias = "default_start_release"
18+
19+
# ---- LINT ----
20+
21+
[tasks.clippy]
22+
alias = "default_clippy"

examples/component_builder/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Component builder example
2+
3+
How to write reusable views / components with builder pattern.
4+
5+
---
6+
7+
```bash
8+
cargo make start
9+
```
10+
11+
Open [127.0.0.1:8000](http://127.0.0.1:8000) in your browser.

examples/component_builder/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
7+
<title>Component builder example</title>
8+
</head>
9+
10+
<body>
11+
<section id="app"></section>
12+
<script type="module">
13+
import init from '/pkg/package.js';
14+
init('/pkg/package_bg.wasm');
15+
</script>
16+
</body>
17+
18+
</html>
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
#![allow(dead_code)]
2+
3+
use seed::virtual_dom::IntoNodes;
4+
use seed::{prelude::*, Style as Css, *};
5+
use std::borrow::Cow;
6+
use std::rc::Rc;
7+
use web_sys::{HtmlElement, MouseEvent};
8+
9+
// ------ Button ------
10+
11+
pub struct Button<Ms: 'static> {
12+
title: Option<Cow<'static, str>>,
13+
style: Style,
14+
outline: bool,
15+
size: Size,
16+
block: bool,
17+
element: Element,
18+
attrs: Attrs,
19+
disabled: bool,
20+
on_clicks: Vec<Rc<dyn Fn(MouseEvent) -> Ms>>,
21+
content: Vec<Node<Ms>>,
22+
el_ref: ElRef<HtmlElement>,
23+
css: Css,
24+
}
25+
26+
impl<Ms> Button<Ms> {
27+
pub fn new(title: impl Into<Cow<'static, str>>) -> Self {
28+
Self::default().title(title)
29+
}
30+
31+
pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
32+
self.title = Some(title.into());
33+
self
34+
}
35+
36+
// --- style ---
37+
38+
const fn style(mut self, style: Style) -> Self {
39+
self.style = style;
40+
self
41+
}
42+
43+
pub const fn primary(self) -> Self {
44+
self.style(Style::Primary)
45+
}
46+
47+
pub const fn secondary(self) -> Self {
48+
self.style(Style::Secondary)
49+
}
50+
51+
// --- // ---
52+
53+
pub const fn outline(mut self) -> Self {
54+
self.outline = true;
55+
self
56+
}
57+
58+
// --- size ---
59+
60+
const fn size(mut self, size: Size) -> Self {
61+
self.size = size;
62+
self
63+
}
64+
65+
pub const fn medium(self) -> Self {
66+
self.size(Size::Medium)
67+
}
68+
69+
pub const fn large(self) -> Self {
70+
self.size(Size::Large)
71+
}
72+
73+
// --- // ---
74+
75+
pub const fn block(mut self) -> Self {
76+
self.block = true;
77+
self
78+
}
79+
80+
// --- element ---
81+
82+
#[allow(clippy::missing_const_for_fn)]
83+
fn element(mut self, element: Element) -> Self {
84+
self.element = element;
85+
self
86+
}
87+
88+
pub fn a(self, href: impl Into<Cow<'static, str>>) -> Self {
89+
self.element(Element::A(Href(href.into())))
90+
}
91+
92+
pub fn button(self) -> Self {
93+
self.element(Element::Button)
94+
}
95+
96+
// --- // ---
97+
98+
pub fn add_attrs(mut self, attrs: Attrs) -> Self {
99+
self.attrs.merge(attrs);
100+
self
101+
}
102+
103+
pub fn add_css(mut self, css: Css) -> Self {
104+
self.css.merge(css);
105+
self
106+
}
107+
108+
pub const fn disabled(mut self, disabled: bool) -> Self {
109+
self.disabled = disabled;
110+
self
111+
}
112+
113+
pub fn add_on_click(
114+
mut self,
115+
on_click: impl FnOnce(MouseEvent) -> Ms + Clone + 'static,
116+
) -> Self {
117+
self.on_clicks
118+
.push(Rc::new(move |event| on_click.clone()(event)));
119+
self
120+
}
121+
122+
pub fn content(mut self, content: impl IntoNodes<Ms>) -> Self {
123+
self.content = content.into_nodes();
124+
self
125+
}
126+
127+
pub fn el_ref(mut self, el_ref: &ElRef<HtmlElement>) -> Self {
128+
self.el_ref = el_ref.clone();
129+
self
130+
}
131+
132+
fn view(mut self) -> Node<Ms> {
133+
let tag = {
134+
match self.element {
135+
Element::A(_) => Tag::A,
136+
Element::Button => Tag::Button,
137+
}
138+
};
139+
140+
let content = self.title.take().map(Node::new_text);
141+
142+
let attrs = {
143+
let mut attrs = attrs! {};
144+
145+
if self.disabled {
146+
attrs.add(At::from("aria-disabled"), true);
147+
attrs.add(At::TabIndex, -1);
148+
}
149+
150+
match self.element {
151+
Element::A(href) => {
152+
if not(self.disabled) {
153+
attrs.add(At::Href, href.as_str());
154+
}
155+
}
156+
Element::Button => {
157+
if self.disabled {
158+
attrs.add(At::Disabled, AtValue::None);
159+
}
160+
}
161+
}
162+
attrs
163+
};
164+
165+
let css = {
166+
let mut css = style! {
167+
St::TextDecoration => "none",
168+
};
169+
170+
let color = match self.style {
171+
Style::Primary => "blue",
172+
Style::Secondary => "gray",
173+
};
174+
175+
if self.outline {
176+
css.merge(style! {
177+
St::Color => color,
178+
St::BackgroundColor => "transparent",
179+
St::Border => format!("{} {} {}", px(2), "solid", color),
180+
});
181+
} else {
182+
css.merge(style! { St::Color => "white", St::BackgroundColor => color });
183+
};
184+
185+
match self.size {
186+
Size::Medium => {
187+
css.merge(style! {St::Padding => format!("{} {}", rem(0.2), rem(0.7))});
188+
}
189+
Size::Large => {
190+
css.merge(style!{St::Padding => format!("{} {}", rem(0.5), rem(1)), St::FontSize => rem(1.25)});
191+
}
192+
}
193+
194+
if self.block {
195+
css.merge(style! {St::Display => "block"});
196+
}
197+
198+
if self.disabled {
199+
css.merge(style! {St::Opacity => 0.5});
200+
} else {
201+
css.merge(style! {St::Cursor => "pointer"});
202+
}
203+
204+
css
205+
};
206+
207+
let mut button = custom![
208+
tag,
209+
el_ref(&self.el_ref),
210+
css,
211+
self.css,
212+
attrs,
213+
self.attrs,
214+
content,
215+
self.content,
216+
];
217+
218+
if !self.disabled {
219+
for on_click in self.on_clicks {
220+
button.add_event_handler(mouse_ev(Ev::Click, move |event| on_click(event)));
221+
}
222+
}
223+
224+
button
225+
}
226+
}
227+
228+
impl<Ms> Default for Button<Ms> {
229+
fn default() -> Self {
230+
Self {
231+
title: None,
232+
style: Style::default(),
233+
outline: false,
234+
size: Size::default(),
235+
block: false,
236+
element: Element::default(),
237+
attrs: Attrs::empty(),
238+
disabled: false,
239+
on_clicks: Vec::new(),
240+
content: Vec::new(),
241+
el_ref: ElRef::default(),
242+
css: Css::empty(),
243+
}
244+
}
245+
}
246+
247+
// It allows us to use `Button` directly in element macros without calling `view` explicitly.
248+
// E.g. `div![Button::new("My button")]`
249+
impl<Ms> UpdateEl<Ms> for Button<Ms> {
250+
fn update_el(self, el: &mut El<Ms>) {
251+
self.view().update_el(el)
252+
}
253+
}
254+
255+
// ------ Style ------
256+
257+
enum Style {
258+
Primary,
259+
Secondary,
260+
}
261+
262+
impl Default for Style {
263+
fn default() -> Self {
264+
Self::Primary
265+
}
266+
}
267+
268+
// ------ Size ------
269+
270+
enum Size {
271+
Medium,
272+
Large,
273+
}
274+
275+
impl Default for Size {
276+
fn default() -> Self {
277+
Self::Medium
278+
}
279+
}
280+
281+
// ------ Element ------
282+
283+
enum Element {
284+
A(Href),
285+
Button,
286+
}
287+
288+
impl Default for Element {
289+
fn default() -> Self {
290+
Self::Button
291+
}
292+
}
293+
294+
// ------ Href ------
295+
296+
pub struct Href(Cow<'static, str>);
297+
298+
impl Href {
299+
fn as_str(&self) -> &str {
300+
self.0.as_ref()
301+
}
302+
}

0 commit comments

Comments
 (0)