on master help sign in
Merge pull request #16 from fitzgen/fill-out-examples
ca435e8Nick Fitzgerald
Cargo.lock+65−3
@@ -47,7 +47,7 @@ dependencies = [
47
47
48[[package]]
48[[package]]
49name = "bumpalo"
49name = "bumpalo"
50−version = "2.0.0"
50+version = "2.1.0"
51source = "registry+https://github.com/rust-lang/crates.io-index"
51source = "registry+https://github.com/rust-lang/crates.io-index"
52
52
53[[package]]
53[[package]]
@@ -82,7 +82,7 @@ dependencies = [
82name = "dodrio"
82name = "dodrio"
83version = "0.1.0"
83version = "0.1.0"
84dependencies = [
84dependencies = [
85− "bumpalo 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
85+ "bumpalo 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
86 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
86 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
87 "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
87 "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
88 "js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
88 "js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -128,6 +128,25 @@ dependencies = [
128 "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
128 "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
129]
129]
130
130
131+[[package]]
132+name = "dodrio-todomvc"
133+version = "0.1.0"
134+dependencies = [
135+ "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
136+ "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
137+ "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
138+ "dodrio 0.1.0",
139+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
140+ "js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
141+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
142+ "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)",
143+ "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
144+ "wasm-bindgen 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
145+ "wasm-bindgen-futures 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
146+ "wasm-bindgen-test 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
147+ "web-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
148+]
149+
131[[package]]
150[[package]]
132name = "env_logger"
151name = "env_logger"
133version = "0.6.0"
152version = "0.6.0"
@@ -181,6 +200,11 @@ dependencies = [
181 "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
200 "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
182]
201]
183
202
203+[[package]]
204+name = "itoa"
205+version = "0.4.3"
206+source = "registry+https://github.com/rust-lang/crates.io-index"
207+
184[[package]]
208[[package]]
185name = "js-sys"
209name = "js-sys"
186version = "0.3.11"
210version = "0.3.11"
@@ -280,11 +304,44 @@ name = "rustc-demangle"
280version = "0.1.13"
304version = "0.1.13"
281source = "registry+https://github.com/rust-lang/crates.io-index"
305source = "registry+https://github.com/rust-lang/crates.io-index"
282
306
307+[[package]]
308+name = "ryu"
309+version = "0.2.7"
310+source = "registry+https://github.com/rust-lang/crates.io-index"
311+
283[[package]]
312[[package]]
284name = "scoped-tls"
313name = "scoped-tls"
285version = "0.1.2"
314version = "0.1.2"
286source = "registry+https://github.com/rust-lang/crates.io-index"
315source = "registry+https://github.com/rust-lang/crates.io-index"
287
316
317+[[package]]
318+name = "serde"
319+version = "1.0.87"
320+source = "registry+https://github.com/rust-lang/crates.io-index"
321+dependencies = [
322+ "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)",
323+]
324+
325+[[package]]
326+name = "serde_derive"
327+version = "1.0.87"
328+source = "registry+https://github.com/rust-lang/crates.io-index"
329+dependencies = [
330+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
331+ "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
332+ "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
333+]
334+
335+[[package]]
336+name = "serde_json"
337+version = "1.0.38"
338+source = "registry+https://github.com/rust-lang/crates.io-index"
339+dependencies = [
340+ "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
341+ "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
342+ "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)",
343+]
344+
288[[package]]
345[[package]]
289name = "sourcefile"
346name = "sourcefile"
290version = "0.1.4"
347version = "0.1.4"
@@ -520,7 +577,7 @@ dependencies = [
520"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
577"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
521"checksum backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b5b493b66e03090ebc4343eb02f94ff944e0cbc9ac6571491d170ba026741eb5"
578"checksum backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b5b493b66e03090ebc4343eb02f94ff944e0cbc9ac6571491d170ba026741eb5"
522"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
579"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
523−"checksum bumpalo 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa2dc7f1b2cc9e9b0d80ab7765699bd5314653930f7f54ba635a13bdd406165"
580+"checksum bumpalo 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "21ef2109240a377370f55ea3ef0b486b46d7b5c0f7455ab0ec676d73f875d58a"
524"checksum cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)" = "4390a3b5f4f6bce9c1d0c00128379df433e53777fdd30e92f16a529332baec4e"
581"checksum cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)" = "4390a3b5f4f6bce9c1d0c00128379df433e53777fdd30e92f16a529332baec4e"
525"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
582"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
526"checksum console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6c5dd2c094474ec60a6acaf31780af270275e3153bafff2db5995b715295762e"
583"checksum console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6c5dd2c094474ec60a6acaf31780af270275e3153bafff2db5995b715295762e"
@@ -531,6 +588,7 @@ dependencies = [
531"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
588"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
532"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
589"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
533"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
590"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
591+"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
534"checksum js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "838d3c46e859486a1c7d24defa6fb2a4d4a7fc64ab79079a2b8dd6a92b4c327f"
592"checksum js-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "838d3c46e859486a1c7d24defa6fb2a4d4a7fc64ab79079a2b8dd6a92b4c327f"
535"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1"
593"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1"
536"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047"
594"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047"
@@ -545,7 +603,11 @@ dependencies = [
545"checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f"
603"checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f"
546"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
604"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
547"checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619"
605"checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619"
606+"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
548"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
607"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
608+"checksum serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)" = "2e20fde37801e83c891a2dc4ebd3b81f0da4d1fb67a9e0a2a3b921e2536a58ee"
609+"checksum serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)" = "633e97856567e518b59ffb2ad7c7a4fd4c5d91d9c7f32dd38a27b2bf7e8114ea"
610+"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9"
549"checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3"
611"checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3"
550"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
612"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
551"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
613"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
Cargo.toml+10−8
@@ -8,15 +8,15 @@ edition = "2018"
8default = []
8default = []
9
9
10[dependencies]
10[dependencies]
11−bumpalo = "2.0.0"
11+bumpalo = "2.1.0"
12−futures = "0.1"
12+futures = "0.1.25"
13wasm-bindgen = "0.2.34"
13wasm-bindgen = "0.2.34"
14−wasm-bindgen-futures = "0.3.10"
14+wasm-bindgen-futures = "0.3.11"
15−js-sys = "0.3.10"
15+js-sys = "0.3.11"
16log = "0.4.6"
16log = "0.4.6"
17
17
18[dependencies.web-sys]
18[dependencies.web-sys]
19−version = "0.3.10"
19+version = "0.3.11"
20features = [
20features = [
21 "console",
21 "console",
22 "Document",
22 "Document",
@@ -27,11 +27,11 @@ features = [
27]
27]
28
28
29[dev-dependencies]
29[dev-dependencies]
30−wasm-bindgen-test = "0.2"
30+wasm-bindgen-test = "0.2.34"
31console_log = "0.1.2"
31console_log = "0.1.2"
32
32
33[dev-dependencies.web-sys]
33[dev-dependencies.web-sys]
34−version = "0.3.10"
34+version = "0.3.11"
35features = [
35features = [
36 "Attr",
36 "Attr",
37 "EventTarget",
37 "EventTarget",
@@ -41,7 +41,8 @@ features = [
41]
41]
42
42
43[profile.release]
43[profile.release]
44−# Tell `rustc` to optimize for small code size.
44+incremental = false
45+lto = true
45opt-level = "s"
46opt-level = "s"
46
47
47[workspace]
48[workspace]
@@ -49,4 +50,5 @@ members = [
49 "./examples/counter",
50 "./examples/counter",
50 "./examples/hello-world",
51 "./examples/hello-world",
51 "./examples/input-form",
52 "./examples/input-form",
53+ "./examples/todomvc",
52]
54]
examples/counter/Cargo.toml+5−5
@@ -5,19 +5,19 @@ authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5edition = "2018"
5edition = "2018"
6
6
7[lib]
7[lib]
8−crate-type = ["cdylib", "rlib"]
8+crate-type = ["cdylib"]
9
9
10[features]
10[features]
11
11
12[dependencies]
12[dependencies]
13−console_error_panic_hook = "0.1.1"
13+console_error_panic_hook = "0.1.5"
14console_log = "0.1.2"
14console_log = "0.1.2"
15dodrio = { path = "../.." }
15dodrio = { path = "../.." }
16log = "0.4.6"
16log = "0.4.6"
17−wasm-bindgen = "0.2"
17+wasm-bindgen = "0.2.34"
18
18
19[dependencies.web-sys]
19[dependencies.web-sys]
20−version = "0.3.8"
20+version = "0.3.11"
21features = [
21features = [
22 "console",
22 "console",
23 "Document",
23 "Document",
@@ -30,4 +30,4 @@ features = [
30]
30]
31
31
32[dev-dependencies]
32[dev-dependencies]
33−wasm-bindgen-test = "0.2"
33+wasm-bindgen-test = "0.2.34"
examples/counter/README.md+12−20
@@ -1,29 +1,21 @@
1−# 🦀🕸 `rust-webpack-template`
1+# Counter
2
2
3−> **Kickstart your Rust, WebAssembly, and Webpack project!**
3+A counter that can be incremented and decremented.
4
4
5−This template is designed for creating monorepo-style Web applications with
5+## Source
6−Rust-generated WebAssembly and Webpack without publishing your wasm to NPM.
7
6
8−* Want to create and publish NPM packages with Rust and WebAssembly? [Check out
7+See `src/lib.rs`.
9− `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template)
10
8
11−## 🔋 Batteries Included
9+## Build
12
10
13−This template comes pre-configured with all the boilerplate for compiling Rust
11+```
14−to WebAssembly and hooking into a Webpack build pipeline.
12+wasm-pack build --target no-modules
15−
13+```
16−* `npm run start` -- Serve the project locally for development at
17− `http://localhost:8080`.
18−
19−* `npm run build` -- Bundle the project (in production mode).
20−
21−## 🚴 Using This Template
22
14
23−First, [install `wasm-pack`!](https://rustwasm.github.io/wasm-pack/installer/)
15+## Serve
24
16
25−Then, use `npm init` to clone this template:
17+Use any HTTP server, for example:
26
18
27−```sh
19+```
28−npm init rust-webpack my-app
20+python -m SimpleHTTPServer
29```
21```
examples/counter/src/lib.rs+25−7
@@ -3,30 +3,37 @@ use dodrio::{on, Node, Render};
3use log::*;
3use log::*;
4use wasm_bindgen::prelude::*;
4use wasm_bindgen::prelude::*;
5
5
6+/// A counter that can be incremented and decrmented!
6struct Counter {
7struct Counter {
7− val: isize,
8+ count: isize,
8}
9}
9
10
10impl Counter {
11impl Counter {
12+ /// Construct a new, zeroed counter.
11 fn new() -> Counter {
13 fn new() -> Counter {
12− Counter { val: 0 }
14+ Counter { count: 0 }
13 }
15 }
14
16
17+ /// Increment this counter's count.
15 fn increment(&mut self) {
18 fn increment(&mut self) {
16− self.val += 1;
19+ self.count += 1;
17 }
20 }
18
21
22+ /// Decrement this counter's count.
19 fn decrement(&mut self) {
23 fn decrement(&mut self) {
20− self.val -= 1;
24+ self.count -= 1;
21 }
25 }
22}
26}
23
27
28+// The `Render` implementation for `Counter`s displays the current count and has
29+// buttons to increment and decrement the count.
24impl Render for Counter {
30impl Render for Counter {
25 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
31 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
26 where
32 where
27 'a: 'bump,
33 'a: 'bump,
28 {
34 {
35+ // Stringify the count as a bump-allocated string.
29− let val = bumpalo::format!(in bump, "{}", self.val);
36+ let count = bumpalo::format!(in bump, "{}", self.count);
30
37
31 Node::element(
38 Node::element(
32 bump,
39 bump,
@@ -38,17 +45,26 @@ impl Render for Counter {
38 bump,
45 bump,
39 "button",
46 "button",
40 [on(bump, "click", |root, vdom, _event| {
47 [on(bump, "click", |root, vdom, _event| {
48+ // Cast the root render component to a `Counter`, since
49+ // we know that's what it is.
41− root.unwrap_mut::<Counter>().increment();
50+ let counter = root.unwrap_mut::<Counter>();
51+
52+ // Increment the counter.
53+ counter.increment();
54+
55+ // Since the count has updated, we should re-render the
56+ // counter on the next animation frame.
42 vdom.schedule_render();
57 vdom.schedule_render();
43 })],
58 })],
44 [],
59 [],
45 [Node::text("+")],
60 [Node::text("+")],
46 ),
61 ),
47− Node::text(val.into_bump_str()),
62+ Node::text(count.into_bump_str()),
48 Node::element(
63 Node::element(
49 bump,
64 bump,
50 "button",
65 "button",
51 [on(bump, "click", |root, vdom, _event| {
66 [on(bump, "click", |root, vdom, _event| {
67+ // Same as above, but decrementing instead of incrementing.
52 root.unwrap_mut::<Counter>().decrement();
68 root.unwrap_mut::<Counter>().decrement();
53 vdom.schedule_render();
69 vdom.schedule_render();
54 })],
70 })],
@@ -62,9 +78,11 @@ impl Render for Counter {
62
78
63#[wasm_bindgen(start)]
79#[wasm_bindgen(start)]
64pub fn run() {
80pub fn run() {
81+ // Initialize debug logging for if/when things go wrong.
65 console_error_panic_hook::set_once();
82 console_error_panic_hook::set_once();
66 console_log::init_with_level(Level::Trace).expect("should initialize logging OK");
83 console_log::init_with_level(Level::Trace).expect("should initialize logging OK");
67
84
85+ // Get the document's `<body>`.
68 let window = web_sys::window().unwrap();
86 let window = web_sys::window().unwrap();
69 let document = window.document().unwrap();
87 let document = window.document().unwrap();
70 let body = document.body().unwrap();
88 let body = document.body().unwrap();
examples/hello-world/Cargo.toml+5−5
@@ -5,17 +5,17 @@ authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5edition = "2018"
5edition = "2018"
6
6
7[lib]
7[lib]
8−crate-type = ["cdylib", "rlib"]
8+crate-type = ["cdylib"]
9
9
10[features]
10[features]
11
11
12[dependencies]
12[dependencies]
13dodrio = { path = "../.." }
13dodrio = { path = "../.." }
14−wasm-bindgen = "0.2"
14+wasm-bindgen = "0.2.34"
15−console_error_panic_hook = "0.1.1"
15+console_error_panic_hook = "0.1.5"
16
16
17[dependencies.web-sys]
17[dependencies.web-sys]
18−version = "0.3.8"
18+version = "0.3.11"
19features = [
19features = [
20 "Document",
20 "Document",
21 "HtmlElement",
21 "HtmlElement",
@@ -24,4 +24,4 @@ features = [
24]
24]
25
25
26[dev-dependencies]
26[dev-dependencies]
27−wasm-bindgen-test = "0.2"
27+wasm-bindgen-test = "0.2.34"
examples/hello-world/README.md+12−20
@@ -1,29 +1,21 @@
1−# 🦀🕸 `rust-webpack-template`
1+# Hello World
2
2
3−> **Kickstart your Rust, WebAssembly, and Webpack project!**
3+The most basic example!
4
4
5−This template is designed for creating monorepo-style Web applications with
5+## Source
6−Rust-generated WebAssembly and Webpack without publishing your wasm to NPM.
7
6
8−* Want to create and publish NPM packages with Rust and WebAssembly? [Check out
7+See `src/lib.rs`.
9− `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template)
10
8
11−## 🔋 Batteries Included
9+## Build
12
10
13−This template comes pre-configured with all the boilerplate for compiling Rust
11+```
14−to WebAssembly and hooking into a Webpack build pipeline.
12+wasm-pack build --target no-modules
15−
13+```
16−* `npm run start` -- Serve the project locally for development at
17− `http://localhost:8080`.
18−
19−* `npm run build` -- Bundle the project (in production mode).
20−
21−## 🚴 Using This Template
22
14
23−First, [install `wasm-pack`!](https://rustwasm.github.io/wasm-pack/installer/)
15+## Serve
24
16
25−Then, use `npm init` to clone this template:
17+Use any HTTP server, for example:
26
18
27−```sh
19+```
28−npm init rust-webpack my-app
20+python -m SimpleHTTPServer
29```
21```
examples/hello-world/src/lib.rs+11−7
@@ -2,16 +2,14 @@ use dodrio::bumpalo::Bump;
2use dodrio::{Node, Render, Vdom};
2use dodrio::{Node, Render, Vdom};
3use wasm_bindgen::prelude::*;
3use wasm_bindgen::prelude::*;
4
4
5+/// A rendering component that displays a greeting.
5struct Hello<'who> {
6struct Hello<'who> {
7+ /// Who to greet.
6 who: &'who str,
8 who: &'who str,
7}
9}
8
10
9−impl<'who> Hello<'who> {
11+// The `Render` implementation describes how to render a `Hello` component into
10− fn new(who: &str) -> Hello {
12+// HTML.
11− Hello { who }
12− }
13−}
14−
15impl<'who> Render for Hello<'who> {
13impl<'who> Render for Hello<'who> {
16 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
14 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
17 where
15 where
@@ -19,9 +17,13 @@ impl<'who> Render for Hello<'who> {
19 {
17 {
20 Node::element(
18 Node::element(
21 bump,
19 bump,
20+ // The element's tag name. In this case a `<p>` element.
22 "p",
21 "p",
22+ // Event listeners. Empty in this case.
23 [],
23 [],
24+ // Attributes. Again, empty in this case.
24 [],
25 [],
26+ // Child nodes.
25 [Node::text("Hello, "), Node::text(self.who), Node::text("!")],
27 [Node::text("Hello, "), Node::text(self.who), Node::text("!")],
26 )
28 )
27 }
29 }
@@ -29,14 +31,16 @@ impl<'who> Render for Hello<'who> {
29
31
30#[wasm_bindgen(start)]
32#[wasm_bindgen(start)]
31pub fn run() {
33pub fn run() {
34+ // Set up the panic hook for debugging when things go wrong.
32 console_error_panic_hook::set_once();
35 console_error_panic_hook::set_once();
33
36
37+ // Grab the document's `<body>`.
34 let window = web_sys::window().unwrap_throw();
38 let window = web_sys::window().unwrap_throw();
35 let document = window.document().unwrap_throw();
39 let document = window.document().unwrap_throw();
36 let body = document.body().unwrap_throw();
40 let body = document.body().unwrap_throw();
37
41
38 // Create a new `Hello` render component.
42 // Create a new `Hello` render component.
39− let component = Hello::new("World");
43+ let component = Hello { who: "World" };
40
44
41 // Create a virtual DOM and mount it and the `Hello` render component to the
45 // Create a virtual DOM and mount it and the `Hello` render component to the
42 // `<body>`.
46 // `<body>`.
examples/input-form/Cargo.toml+5−5
@@ -5,17 +5,17 @@ authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5edition = "2018"
5edition = "2018"
6
6
7[lib]
7[lib]
8−crate-type = ["cdylib", "rlib"]
8+crate-type = ["cdylib"]
9
9
10[features]
10[features]
11
11
12[dependencies]
12[dependencies]
13dodrio = { path = "../.." }
13dodrio = { path = "../.." }
14−wasm-bindgen = "0.2"
14+wasm-bindgen = "0.2.34"
15−console_error_panic_hook = "0.1.1"
15+console_error_panic_hook = "0.1.5"
16
16
17[dependencies.web-sys]
17[dependencies.web-sys]
18−version = "0.3.8"
18+version = "0.3.11"
19features = [
19features = [
20 "console",
20 "console",
21 "Document",
21 "Document",
@@ -29,4 +29,4 @@ features = [
29]
29]
30
30
31[dev-dependencies]
31[dev-dependencies]
32−wasm-bindgen-test = "0.2"
32+wasm-bindgen-test = "0.2.34"
examples/input-form/README.md+13−20
@@ -1,29 +1,22 @@
1−# 🦀🕸 `rust-webpack-template`
1+# Input Forms
2
2
3−> **Kickstart your Rust, WebAssembly, and Webpack project!**
3+Renders a greeting to a user-supplied person via an `<input>` value. The
4+greeting is live-updated with new inputs.
4
5
5−This template is designed for creating monorepo-style Web applications with
6+## Source
6−Rust-generated WebAssembly and Webpack without publishing your wasm to NPM.
7
7
8−* Want to create and publish NPM packages with Rust and WebAssembly? [Check out
8+See `src/lib.rs`.
9− `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template)
10
9
11−## 🔋 Batteries Included
10+## Build
12
11
13−This template comes pre-configured with all the boilerplate for compiling Rust
12+```
14−to WebAssembly and hooking into a Webpack build pipeline.
13+wasm-pack build --target no-modules
15−
14+```
16−* `npm run start` -- Serve the project locally for development at
17− `http://localhost:8080`.
18−
19−* `npm run build` -- Bundle the project (in production mode).
20−
21−## 🚴 Using This Template
22
15
23−First, [install `wasm-pack`!](https://rustwasm.github.io/wasm-pack/installer/)
16+## Serve
24
17
25−Then, use `npm init` to clone this template:
18+Use any HTTP server, for example:
26
19
27−```sh
20+```
28−npm init rust-webpack my-app
21+python -m SimpleHTTPServer
29```
22```
examples/input-form/src/lib.rs+17−2
@@ -3,21 +3,27 @@ use dodrio::{on, Attribute, Node, Render};
3use wasm_bindgen::prelude::*;
3use wasm_bindgen::prelude::*;
4use wasm_bindgen::JsCast;
4use wasm_bindgen::JsCast;
5
5
6+/// Say hello to someone.
6struct SayHelloTo {
7struct SayHelloTo {
8+ /// Who to say hello to.
7 who: String,
9 who: String,
8}
10}
9
11
10impl SayHelloTo {
12impl SayHelloTo {
13+ /// Construct a new `SayHelloTo` component.
11 fn new<S: Into<String>>(who: S) -> SayHelloTo {
14 fn new<S: Into<String>>(who: S) -> SayHelloTo {
12 let who = who.into();
15 let who = who.into();
13 SayHelloTo { who }
16 SayHelloTo { who }
14 }
17 }
15
18
19+ /// Update who to say hello to.
16 fn set_who(&mut self, who: String) {
20 fn set_who(&mut self, who: String) {
17 self.who = who;
21 self.who = who;
18 }
22 }
19}
23}
20
24
25+// The `Render` implementation has a text `<input>` and a `<div>` that shows a
26+// greeting to the `<input>`'s value.
21impl Render for SayHelloTo {
27impl Render for SayHelloTo {
22 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
28 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
23 where
29 where
@@ -27,15 +33,21 @@ impl Render for SayHelloTo {
27 bump,
33 bump,
28 "input",
34 "input",
29 [on(bump, "input", |root, vdom, event| {
35 [on(bump, "input", |root, vdom, event| {
36+ // If the event's target is our input...
30 let input = match event
37 let input = match event
31 .target()
38 .target()
32 .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
39 .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
33 {
40 {
34− Some(input) => input,
35 None => return,
41 None => return,
42+ Some(input) => input,
36 };
43 };
37
44
45+ // ...then get its value and update who we are greeting.
46+ let value = input.value();
38− root.unwrap_mut::<SayHelloTo>().set_who(input.value());
47+ let hello = root.unwrap_mut::<SayHelloTo>();
48+ hello.set_who(value);
49+
50+ // Finally, re-render the component on the next animation frame.
39 vdom.schedule_render();
51 vdom.schedule_render();
40 })],
52 })],
41 [
53 [
@@ -60,12 +72,15 @@ impl Render for SayHelloTo {
60
72
61#[wasm_bindgen(start)]
73#[wasm_bindgen(start)]
62pub fn run() {
74pub fn run() {
75+ // Initialize debugging for when/if something goes wrong.
63 console_error_panic_hook::set_once();
76 console_error_panic_hook::set_once();
64
77
78+ // Get the document's `<body>`.
65 let window = web_sys::window().unwrap();
79 let window = web_sys::window().unwrap();
66 let document = window.document().unwrap();
80 let document = window.document().unwrap();
67 let body = document.body().unwrap();
81 let body = document.body().unwrap();
68
82
83+ // Construct a new `SayHelloTo` rendering component.
69 let say_hello = SayHelloTo::new("World");
84 let say_hello = SayHelloTo::new("World");
70
85
71 // Mount the component to the `<body>`.
86 // Mount the component to the `<body>`.
examples/todomvc/Cargo.tomlnew file+48
@@ -0,0 +1,48 @@
1+[package]
2+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
3+edition = "2018"
4+name = "dodrio-todomvc"
5+version = "0.1.0"
6+
7+[dependencies]
8+cfg-if = "0.1.6"
9+dodrio = { path = "../.." }
10+futures = "0.1.25"
11+js-sys = "0.3.11"
12+serde = { features = ["derive"], version = "1.0.87" }
13+serde_json = "1.0.38"
14+wasm-bindgen = "0.2.34"
15+wasm-bindgen-futures = "0.3.11"
16+
17+# Optional dependencies for logging.
18+console_error_panic_hook = { optional = true, version = "0.1.5" }
19+console_log = { optional = true, version = "0.1.2" }
20+log = { optional = true, version = "0.4.6" }
21+
22+[dependencies.web-sys]
23+version = "0.3.11"
24+features = [
25+ "Document",
26+ "Event",
27+ "EventTarget",
28+ "HtmlElement",
29+ "HtmlInputElement",
30+ "KeyboardEvent",
31+ "Location",
32+ "Storage",
33+ "Node",
34+ "Window",
35+]
36+
37+[dev-dependencies]
38+wasm-bindgen-test = "0.2.34"
39+
40+[features]
41+logging = [
42+ "console_log",
43+ "console_error_panic_hook",
44+ "log",
45+]
46+
47+[lib]
48+crate-type = ["cdylib"]
examples/todomvc/README.mdnew file+33
@@ -0,0 +1,33 @@
1+# TodoMVC
2+
3+`dodrio` implementation of the popular [TodoMVC](http://todomvc.com/) app. It
4+correctly and completely fulfills [the
5+specification](https://github.com/tastejs/todomvc/blob/master/app-spec.md) to
6+the best of my knowledge.
7+
8+## Source
9+
10+There are a number of modules in this `dodrio` implementation of TodoMVC. The
11+most important are:
12+
13+* `src/lib.rs`: The entry point to the application.
14+* `src/todos.rs`: Definition of `Todos` model and its rendering.
15+* `src/todo.rs`: Definition of `Todo` model and its rendering.
16+* `src/controller.rs`: The controller handles UI interactions and translates
17+ them into updates on the model. Finally, it triggers re-rendering after those
18+ updates.
19+* `src/router.rs`: A simple URL hash-based router.
20+
21+## Build
22+
23+```
24+wasm-pack build --target no-modules
25+```
26+
27+## Serve
28+
29+Use any HTTP server, for example:
30+
31+```
32+python -m SimpleHTTPServer
33+```
examples/todomvc/index.htmlnew file+21
@@ -0,0 +1,21 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1">
6+ <title>dodrio • TodoMVC</title>
7+ <link rel="stylesheet" href="https://unpkg.com/todomvc-common@^1.0.0/base.css">
8+ <link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@^2.0.0/index.css">
9+ </head>
10+ <body>
11+ <section class="todoapp">
12+ </section>
13+ <footer class="info">
14+ <p>Double-click to edit a todo</p>
15+ </footer>
16+ <script src="pkg/dodrio_todomvc.js"></script>
17+ <script>
18+ wasm_bindgen("pkg/dodrio_todomvc_bg.wasm");
19+ </script>
20+ </body>
21+</html>
examples/todomvc/src/controller.rsnew file+140
@@ -0,0 +1,140 @@
1+//! The controller handles UI events, translates them into updates on the model,
2+//! and schedules re-renders.
3+
4+use crate::todo::{Todo, TodoActions};
5+use crate::todos::{Todos, TodosActions};
6+use crate::visibility::Visibility;
7+use dodrio::{RootRender, VdomWeak};
8+use serde::{Deserialize, Serialize};
9+use std::ops::{Deref, DerefMut};
10+
11+/// The controller for the TodoMVC app.
12+///
13+/// This `Controller` struct is never actually instantiated. It is only used for
14+/// its `*Actions` trait implementations, none of which take a `self` parameter.
15+///
16+/// One could imagine alternative controller implementations with `*Actions`
17+/// trait implementations for (e.g.) testing that will assert various expected
18+/// action methods are called after rendering todo items and sending DOM events.
19+#[derive(Default, Deserialize, Serialize)]
20+pub struct Controller;
21+
22+impl TodosActions for Controller {
23+ fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak) {
24+ let mut todos = AutoCommitTodos::new(root, vdom);
25+ let all_complete = todos.todos().iter().all(|t| t.is_complete());
26+ for t in todos.todos_mut() {
27+ t.set_complete(!all_complete);
28+ }
29+ }
30+
31+ fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String) {
32+ let mut todos = AutoCommitTodos::new(root, vdom);
33+ todos.set_draft(draft);
34+ }
35+
36+ fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak) {
37+ let mut todos = AutoCommitTodos::new(root, vdom);
38+ let title = todos.take_draft();
39+ let title = title.trim();
40+ if !title.is_empty() {
41+ let id = todos.todos().len();
42+ let new = Todo::new(id, title);
43+ todos.add_todo(new);
44+ }
45+ }
46+
47+ fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility) {
48+ let mut todos = AutoCommitTodos::new(root, vdom);
49+ todos.set_visibility(vis);
50+ }
51+
52+ fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak) {
53+ let mut todos = AutoCommitTodos::new(root, vdom);
54+ todos.delete_completed();
55+ }
56+}
57+
58+impl TodoActions for Controller {
59+ fn toggle_completed(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
60+ let mut todos = AutoCommitTodos::new(root, vdom);
61+ let t = &mut todos.todos_mut()[id];
62+ let completed = t.is_complete();
63+ t.set_complete(!completed);
64+ }
65+
66+ fn delete(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
67+ let mut todos = AutoCommitTodos::new(root, vdom);
68+ todos.delete_todo(id);
69+ }
70+
71+ fn begin_editing(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
72+ let mut todos = AutoCommitTodos::new(root, vdom);
73+ let t = &mut todos.todos_mut()[id];
74+ let desc = t.title().to_string();
75+ t.set_edits(Some(desc));
76+ }
77+
78+ fn update_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize, edits: String) {
79+ let mut todos = AutoCommitTodos::new(root, vdom);
80+ let t = &mut todos.todos_mut()[id];
81+ t.set_edits(Some(edits));
82+ }
83+
84+ fn finish_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
85+ let mut todos = AutoCommitTodos::new(root, vdom);
86+ let t = &mut todos.todos_mut()[id];
87+ if let Some(edits) = t.take_edits() {
88+ let edits = edits.trim();
89+ if edits.is_empty() {
90+ todos.delete_todo(id);
91+ } else {
92+ t.set_title(edits);
93+ }
94+ }
95+ }
96+
97+ fn cancel_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize) {
98+ let mut todos = AutoCommitTodos::new(root, vdom);
99+ let t = &mut todos.todos_mut()[id];
100+ let _ = t.take_edits();
101+ }
102+}
103+
104+/// An RAII type that dereferences to a `Todos` and once it is dropped, saves
105+/// the (presumably just modified) todos to local storage, and schedules a new
106+/// `dodrio` render.
107+pub struct AutoCommitTodos<'a> {
108+ todos: &'a mut Todos,
109+ vdom: VdomWeak,
110+}
111+
112+impl AutoCommitTodos<'_> {
113+ /// Construct a new `AutoCommitTodos` from the root rendering component and
114+ /// `vdom` handle.
115+ pub fn new(root: &mut dyn RootRender, vdom: VdomWeak) -> AutoCommitTodos {
116+ let todos = root.unwrap_mut::<Todos>();
117+ AutoCommitTodos { todos, vdom }
118+ }
119+}
120+
121+impl Drop for AutoCommitTodos<'_> {
122+ fn drop(&mut self) {
123+ self.todos.save_to_local_storage();
124+ self.vdom.schedule_render();
125+ }
126+}
127+
128+impl Deref for AutoCommitTodos<'_> {
129+ type Target = Todos;
130+
131+ fn deref(&self) -> &Todos {
132+ &self.todos
133+ }
134+}
135+
136+impl DerefMut for AutoCommitTodos<'_> {
137+ fn deref_mut(&mut self) -> &mut Todos {
138+ &mut self.todos
139+ }
140+}
examples/todomvc/src/keys.rsnew file+7
@@ -0,0 +1,7 @@
1+//! Constants for `KeyboardEvent::key_code`.`
2+
3+/// The key code for the enter key.
4+pub const ENTER: u32 = 13;
5+
6+/// The key code for the escape key.
7+pub const ESCAPE: u32 = 27;
examples/todomvc/src/lib.rsnew file+60
@@ -0,0 +1,60 @@
1+//! TodoMVC implemented in `dodrio`!
2+
3+#![deny(missing_docs)]
4+
5+pub mod controller;
6+pub mod keys;
7+pub mod router;
8+pub mod todo;
9+pub mod todos;
10+pub mod utils;
11+pub mod visibility;
12+
13+use controller::Controller;
14+use dodrio::Vdom;
15+use todos::Todos;
16+use wasm_bindgen::prelude::*;
17+
18+/// Run the TodoMVC app!
19+///
20+/// Since this is marked `#[wasm_bindgen(start)]` it is automatically invoked
21+/// once the wasm module instantiated on the Web page.
22+#[wasm_bindgen(start)]
23+pub fn run() -> Result<(), JsValue> {
24+ // Set up the logging for debugging if/when things go wrong.
25+ init_logging();
26+
27+ // Grab the todo app container.
28+ let document = utils::document();
29+ let container = document
30+ .query_selector(".todoapp")?
31+ .ok_or_else(|| js_sys::Error::new("could not find `.todoapp` container"))?;
32+
33+ // Create a new `Todos` render component.
34+ let todos = Todos::<Controller>::new();
35+
36+ // Create a virtual DOM and mount it and the `Todos` render component.
37+ let vdom = Vdom::new(&container, todos);
38+
39+ // Start the URL router.
40+ router::start(vdom.weak());
41+
42+ // Run the virtual DOM forever and don't unmount it.
43+ vdom.forget();
44+
45+ Ok(())
46+}
47+
48+cfg_if::cfg_if! {
49+ if #[cfg(feature = "logging")] {
50+ fn init_logging() {
51+ console_error_panic_hook::set_once();
52+ console_log::init_with_level(log::Level::Trace)
53+ .expect_throw("should initialize logging OK");
54+ }
55+ } else {
56+ fn init_logging() {
57+ // Do nothing.
58+ }
59+ }
60+}
examples/todomvc/src/router.rsnew file+66
@@ -0,0 +1,66 @@
1+//! A simple `#`-fragment router.
2+
3+use crate::todos::Todos;
4+use crate::utils;
5+use crate::visibility::Visibility;
6+use dodrio::VdomWeak;
7+use futures::prelude::*;
8+use std::str::FromStr;
9+use wasm_bindgen::prelude::*;
10+use wasm_bindgen::JsCast;
11+
12+/// Start the router.
13+pub fn start(vdom: VdomWeak) {
14+ // Callback fired whenever the URL's hash fragment changes. Keeps the root
15+ // todos collection's visibility in sync with the `#` fragment.
16+ let on_hash_change = move || {
17+ let new_vis = utils::hash()
18+ .and_then(|hash| {
19+ if hash.starts_with("#/") {
20+ Visibility::from_str(&hash[2..]).ok()
21+ } else {
22+ None
23+ }
24+ })
25+ .unwrap_or_else(|| {
26+ // If we couldn't parse a visibility, make sure we canonicalize
27+ // it back to our default hash.
28+ let v = Visibility::default();
29+ utils::set_hash(&format!("#/{}", v));
30+ v
31+ });
32+
33+ wasm_bindgen_futures::spawn_local(
34+ vdom.with_component({
35+ let vdom = vdom.clone();
36+ move |root| {
37+ let todos = root.unwrap_mut::<Todos>();
38+ // If the todos' visibility already matches the event's
39+ // visibility, then there is nothing to do (ha). If they
40+ // don't match, then we need to update the todos' visibility
41+ // and re-render.
42+ if todos.visibility() != new_vis {
43+ todos.set_visibility(new_vis);
44+ vdom.schedule_render();
45+ }
46+ }
47+ })
48+ .map_err(|_| ()),
49+ );
50+ };
51+
52+ // Call it once to handle the initial `#` fragment.
53+ on_hash_change();
54+
55+ // Now listen for hash changes forever.
56+ //
57+ // Note that if we ever intended to unmount our todos app, we would want to
58+ // provide a method for removing this router's event listener and cleaning
59+ // up after ourselves.
60+ let on_hash_change = Closure::wrap(Box::new(on_hash_change) as Box<FnMut()>);
61+ let window = utils::window();
62+ window
63+ .add_event_listener_with_callback("hashchange", on_hash_change.as_ref().unchecked_ref())
64+ .unwrap_throw();
65+ on_hash_change.forget();
66+}
examples/todomvc/src/todo.rsnew file+229
@@ -0,0 +1,229 @@
1+//! Type definition and `dodrio::Render` implementation for a single todo item.
2+
3+use crate::keys;
4+use dodrio::{bumpalo::Bump, on, Attribute, Node, Render, RootRender, VdomWeak};
5+use serde::{Deserialize, Serialize};
6+use std::marker::PhantomData;
7+use wasm_bindgen::{prelude::*, JsCast};
8+
9+/// A single todo item.
10+#[derive(Serialize, Deserialize)]
11+pub struct Todo<C> {
12+ id: usize,
13+ title: String,
14+ completed: bool,
15+
16+ #[serde(skip)]
17+ edits: Option<String>,
18+
19+ #[serde(skip)]
20+ _controller: PhantomData<C>,
21+}
22+
23+/// Actions on a single todo item that can be triggered from the UI.
24+pub trait TodoActions {
25+ /// Toggle the completion state of the todo item with the given id.
26+ fn toggle_completed(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
27+
28+ /// Delete the todo item with the given id.
29+ fn delete(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
30+
31+ /// Begin editing the todo item with the given id.
32+ fn begin_editing(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
33+
34+ /// Update the edits for the todo with the given id.
35+ fn update_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize, edits: String);
36+
37+ /// Finish editing the todo with the given id.
38+ fn finish_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
39+
40+ /// Cancel editing the todo with the given id.
41+ fn cancel_edits(root: &mut dyn RootRender, vdom: VdomWeak, id: usize);
42+}
43+
44+impl<C> Todo<C> {
45+ /// Construct a new `Todo` with the given identifier and title.
46+ pub fn new<S: Into<String>>(id: usize, title: S) -> Self {
47+ let title = title.into();
48+ let completed = false;
49+ let edits = None;
50+ Todo {
51+ id,
52+ title,
53+ completed,
54+ edits,
55+ _controller: PhantomData,
56+ }
57+ }
58+
59+ /// Set this todo item's id.
60+ pub fn set_id(&mut self, id: usize) {
61+ self.id = id;
62+ }
63+
64+ /// Is this `Todo` complete?
65+ pub fn is_complete(&self) -> bool {
66+ self.completed
67+ }
68+
69+ /// Mark the `Todo` as complete or not.
70+ pub fn set_complete(&mut self, to: bool) {
71+ self.completed = to;
72+ }
73+
74+ /// Get this todo's title.
75+ pub fn title(&self) -> &str {
76+ &self.title
77+ }
78+
79+ /// Set this todo item's title.
80+ pub fn set_title<S: Into<String>>(&mut self, title: S) {
81+ self.title = title.into();
82+ }
83+
84+ /// Set the edits for this todo.
85+ pub fn set_edits<S: Into<String>>(&mut self, edits: Option<S>) {
86+ self.edits = edits.map(Into::into);
87+ }
88+
89+ /// Take this todo's edits, leaving `None` in their place.
90+ pub fn take_edits(&mut self) -> Option<String> {
91+ self.edits.take()
92+ }
93+}
94+
95+impl<C: TodoActions> Render for Todo<C> {
96+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
97+ where
98+ 'a: 'bump,
99+ {
100+ use dodrio::bumpalo::{self, collections::String};
101+
102+ let mut class = String::new_in(bump);
103+ if self.completed {
104+ class.push_str("completed ");
105+ }
106+ if self.edits.is_some() {
107+ class.push_str("editing");
108+ }
109+
110+ let id = self.id;
111+ let title = self.edits.as_ref().unwrap_or(&self.title);
112+ let elem_id = bumpalo::format!(in bump, "todo-{}", id);
113+
114+ let mut input_attrs = bumpalo::vec![
115+ in bump;
116+ Attribute {
117+ name: "class",
118+ value: "toggle",
119+ },
120+ Attribute {
121+ name: "type",
122+ value: "checkbox",
123+ }
124+ ];
125+ if self.completed {
126+ input_attrs.push(Attribute {
127+ name: "checked",
128+ value: "",
129+ });
130+ }
131+ let input_attrs = input_attrs.into_bump_slice();
132+
133+ Node::element(
134+ bump,
135+ "li",
136+ [],
137+ [Attribute {
138+ name: "class",
139+ value: class.into_bump_str(),
140+ }],
141+ [
142+ Node::element(
143+ bump,
144+ "div",
145+ [],
146+ [Attribute {
147+ name: "class",
148+ value: "view",
149+ }],
150+ [
151+ Node::element(
152+ bump,
153+ "input",
154+ [on(bump, "click", move |root, vdom, _event| {
155+ C::toggle_completed(root, vdom, id);
156+ })],
157+ input_attrs,
158+ [],
159+ ),
160+ Node::element(
161+ bump,
162+ "label",
163+ [on(bump, "dblclick", move |root, vdom, _event| {
164+ C::begin_editing(root, vdom, id);
165+ })],
166+ [],
167+ [Node::text(title)],
168+ ),
169+ Node::element(
170+ bump,
171+ "button",
172+ [on(bump, "click", move |root, vdom, _event| {
173+ C::delete(root, vdom, id);
174+ })],
175+ [Attribute {
176+ name: "class",
177+ value: "destroy",
178+ }],
179+ [],
180+ ),
181+ ],
182+ ),
183+ Node::element(
184+ bump,
185+ "input",
186+ [
187+ on(bump, "input", move |root, vdom, event| {
188+ let input = event
189+ .target()
190+ .unwrap_throw()
191+ .unchecked_into::<web_sys::HtmlInputElement>();
192+ C::update_edits(root, vdom, id, input.value());
193+ }),
194+ on(bump, "blur", move |root, vdom, _event| {
195+ C::finish_edits(root, vdom, id);
196+ }),
197+ on(bump, "keydown", move |root, vdom, event| {
198+ let event = event.unchecked_into::<web_sys::KeyboardEvent>();
199+ match event.key_code() {
200+ keys::ENTER => C::finish_edits(root, vdom, id),
201+ keys::ESCAPE => C::cancel_edits(root, vdom, id),
202+ _ => {}
203+ }
204+ }),
205+ ],
206+ [
207+ Attribute {
208+ name: "class",
209+ value: "edit",
210+ },
211+ Attribute {
212+ name: "value",
213+ value: title,
214+ },
215+ Attribute {
216+ name: "name",
217+ value: "title",
218+ },
219+ Attribute {
220+ name: "id",
221+ value: elem_id.into_bump_str(),
222+ },
223+ ],
224+ [],
225+ ),
226+ ],
227+ )
228+ }
229+}
examples/todomvc/src/todos.rsnew file+450
@@ -0,0 +1,450 @@
1+//! Type definitions and `dodrio::Render` implementation for a collection of
2+//! todo items.
3+
4+use crate::controller::Controller;
5+use crate::todo::{Todo, TodoActions};
6+use crate::visibility::Visibility;
7+use crate::{keys, utils};
8+use dodrio::{
9+ bumpalo::{self, Bump},
10+ on, Attribute, Node, Render, RootRender, VdomWeak,
11+};
12+use serde::{Deserialize, Serialize};
13+use std::marker::PhantomData;
14+use std::mem;
15+use wasm_bindgen::prelude::*;
16+use wasm_bindgen::JsCast;
17+
18+/// A collection of todos.
19+#[derive(Default, Serialize, Deserialize)]
20+#[serde(rename = "todos-dodrio", bound = "")]
21+pub struct Todos<C = Controller> {
22+ todos: Vec<Todo<C>>,
23+
24+ #[serde(skip)]
25+ draft: String,
26+
27+ #[serde(skip)]
28+ visibility: Visibility,
29+
30+ #[serde(skip)]
31+ _controller: PhantomData<C>,
32+}
33+
34+/// Actions for `Todos` that can be triggered by UI interactions.
35+pub trait TodosActions: TodoActions {
36+ /// Toggle the completion state of all todo items.
37+ fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak);
38+
39+ /// Update the draft todo item's text.
40+ fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String);
41+
42+ /// Finish the current draft todo item and add it to the collection of
43+ /// todos.
44+ fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak);
45+
46+ /// Change the todo item visibility filtering to the given `Visibility`.
47+ fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility);
48+
49+ /// Delete all completed todo items.
50+ fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak);
51+}
52+
53+impl<C> Todos<C> {
54+ /// Construct a new todos set.
55+ ///
56+ /// If an existing set is available in local storage, then us that,
57+ /// otherwise create a new set.
58+ pub fn new() -> Self
59+ where
60+ C: Default,
61+ {
62+ Self::from_local_storage().unwrap_or_default()
63+ }
64+
65+ /// Deserialize a set of todos from local storage.
66+ pub fn from_local_storage() -> Option<Self> {
67+ utils::local_storage()
68+ .get("todomvc-dodrio")
69+ .ok()
70+ .and_then(|opt| opt)
71+ .and_then(|json| serde_json::from_str(&json).ok())
72+ }
73+
74+ /// Serialize this set of todos to local storage.
75+ pub fn save_to_local_storage(&self) {
76+ let serialized = serde_json::to_string(self).unwrap_throw();
77+ utils::local_storage()
78+ .set("todomvc-dodrio", &serialized)
79+ .unwrap_throw();
80+ }
81+
82+ /// Add a new todo item to this collection.
83+ pub fn add_todo(&mut self, todo: Todo<C>) {
84+ self.todos.push(todo);
85+ }
86+
87+ /// Delete the todo with the given id.
88+ pub fn delete_todo(&mut self, id: usize) {
89+ self.todos.remove(id);
90+ self.fix_ids();
91+ }
92+
93+ /// Delete all completed todo items.
94+ pub fn delete_completed(&mut self) {
95+ self.todos.retain(|t| !t.is_complete());
96+ self.fix_ids();
97+ }
98+
99+ // Fix all todo identifiers so that they match their index once again.
100+ fn fix_ids(&mut self) {
101+ for (id, todo) in self.todos.iter_mut().enumerate() {
102+ todo.set_id(id);
103+ }
104+ }
105+
106+ /// Get a shared slice of the underlying set of todo items.
107+ pub fn todos(&self) -> &[Todo<C>] {
108+ &self.todos
109+ }
110+
111+ /// Get an exclusive slice of the underlying set of todo items.
112+ pub fn todos_mut(&mut self) -> &mut [Todo<C>] {
113+ &mut self.todos
114+ }
115+
116+ /// Set the draft todo item text.
117+ pub fn set_draft<S: Into<String>>(&mut self, draft: S) {
118+ self.draft = draft.into();
119+ }
120+
121+ /// Take the current draft text and replace it with an empty string.
122+ pub fn take_draft(&mut self) -> String {
123+ mem::replace(&mut self.draft, String::new())
124+ }
125+
126+ /// Get the current visibility for these todos.
127+ pub fn visibility(&self) -> Visibility {
128+ self.visibility
129+ }
130+
131+ /// Set the visibility for these todoS.
132+ pub fn set_visibility(&mut self, vis: Visibility) {
133+ self.visibility = vis;
134+ }
135+}
136+
137+/// Rendering helpers.
138+impl<C: TodosActions> Todos<C> {
139+ fn header<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
140+ where
141+ 'a: 'bump,
142+ {
143+ Node::element(
144+ bump,
145+ "header",
146+ [],
147+ [Attribute {
148+ name: "class",
149+ value: "header",
150+ }],
151+ [
152+ Node::element(bump, "h1", [], [], [Node::text("todos")]),
153+ Node::element(
154+ bump,
155+ "input",
156+ [
157+ on(bump, "input", |root, vdom, event| {
158+ let input = event
159+ .target()
160+ .unwrap_throw()
161+ .unchecked_into::<web_sys::HtmlInputElement>();
162+ C::update_draft(root, vdom, input.value());
163+ }),
164+ on(bump, "keydown", |root, vdom, event| {
165+ let event = event.unchecked_into::<web_sys::KeyboardEvent>();
166+ if event.key_code() == keys::ENTER {
167+ C::finish_draft(root, vdom);
168+ }
169+ }),
170+ ],
171+ [
172+ Attribute {
173+ name: "class",
174+ value: "new-todo",
175+ },
176+ Attribute {
177+ name: "placeholder",
178+ value: "What needs to be done?",
179+ },
180+ Attribute {
181+ name: "autofocus",
182+ value: "",
183+ },
184+ Attribute {
185+ name: "value",
186+ value: &self.draft,
187+ },
188+ ],
189+ [],
190+ ),
191+ ],
192+ )
193+ }
194+
195+ fn todos_list<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
196+ where
197+ 'a: 'bump,
198+ {
199+ use dodrio::bumpalo::collections::Vec;
200+
201+ let mut todos = Vec::new_in(bump);
202+ todos.extend(
203+ self.todos
204+ .iter()
205+ .filter(|t| match self.visibility {
206+ Visibility::All => true,
207+ Visibility::Active => !t.is_complete(),
208+ Visibility::Completed => t.is_complete(),
209+ })
210+ .map(|t| t.render(bump)),
211+ );
212+ let todos = todos.into_bump_slice();
213+
214+ let mut input_attrs = bumpalo::vec![
215+ in bump;
216+ Attribute {
217+ name: "class",
218+ value: "toggle-all",
219+ },
220+ Attribute {
221+ name: "id",
222+ value: "toggle-all",
223+ },
224+ Attribute {
225+ name: "type",
226+ value: "checkbox",
227+ },
228+ Attribute {
229+ name: "name",
230+ value: "toggle",
231+ }
232+ ];
233+ if self.todos.iter().all(|t| t.is_complete()) {
234+ input_attrs.push(Attribute {
235+ name: "checked",
236+ value: "",
237+ });
238+ }
239+ let input_attrs = input_attrs.into_bump_slice();
240+
241+ Node::element(
242+ bump,
243+ "section",
244+ [],
245+ [
246+ Attribute {
247+ name: "class",
248+ value: "main",
249+ },
250+ Attribute {
251+ name: "visibility",
252+ value: if self.todos.is_empty() {
253+ "hidden"
254+ } else {
255+ "visible"
256+ },
257+ },
258+ ],
259+ [
260+ Node::element(
261+ bump,
262+ "input",
263+ [on(bump, "click", |root, vdom, _event| {
264+ C::toggle_all(root, vdom);
265+ })],
266+ input_attrs,
267+ [],
268+ ),
269+ Node::element(
270+ bump,
271+ "label",
272+ [],
273+ [Attribute {
274+ name: "for",
275+ value: "toggle-all",
276+ }],
277+ [Node::text("Mark all as complete")],
278+ ),
279+ Node::element(
280+ bump,
281+ "ul",
282+ [],
283+ [Attribute {
284+ name: "class",
285+ value: "todo-list",
286+ }],
287+ todos,
288+ ),
289+ ],
290+ )
291+ }
292+
293+ fn footer<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
294+ where
295+ 'a: 'bump,
296+ {
297+ let completed_count = self.todos.iter().filter(|t| t.is_complete()).count();
298+ let incomplete_count = self.todos.len() - completed_count;
299+ let items_left = if incomplete_count == 1 {
300+ " item left"
301+ } else {
302+ " items left"
303+ };
304+ let incomplete_count = bumpalo::format!(in bump, "{}", incomplete_count);
305+
306+ let attrs = if self.todos.is_empty() {
307+ &bump.alloc([
308+ Attribute {
309+ name: "class",
310+ value: "footer",
311+ },
312+ Attribute {
313+ name: "hidden",
314+ value: "",
315+ },
316+ ])[..]
317+ } else {
318+ &bump.alloc([Attribute {
319+ name: "class",
320+ value: "footer",
321+ }])[..]
322+ };
323+
324+ let clear_completed_text = bumpalo::format!(
325+ in bump,
326+ "Clear completed ({})",
327+ self.todos.iter().filter(|t| t.is_complete()).count()
328+ );
329+
330+ Node::element(
331+ bump,
332+ "footer",
333+ [],
334+ attrs,
335+ [
336+ Node::element(
337+ bump,
338+ "span",
339+ [],
340+ [Attribute {
341+ name: "class",
342+ value: "todo-count",
343+ }],
344+ [
345+ Node::element(
346+ bump,
347+ "strong",
348+ [],
349+ [],
350+ [Node::text(incomplete_count.into_bump_str())],
351+ ),
352+ Node::text(items_left),
353+ ],
354+ ),
355+ Node::element(
356+ bump,
357+ "ul",
358+ [],
359+ [Attribute {
360+ name: "class",
361+ value: "filters",
362+ }],
363+ [
364+ self.visibility_swap(bump, "#/", Visibility::All),
365+ self.visibility_swap(bump, "#/active", Visibility::Active),
366+ self.visibility_swap(bump, "#/completed", Visibility::Completed),
367+ ],
368+ ),
369+ Node::element(
370+ bump,
371+ "button",
372+ [on(bump, "click", |root, vdom, _event| {
373+ C::delete_completed(root, vdom);
374+ })],
375+ {
376+ let mut attrs = bumpalo::vec![
377+ in bump;
378+ Attribute {
379+ name: "class",
380+ value: "clear-completed",
381+ }
382+ ];
383+ if self.todos.iter().all(|t| !t.is_complete()) {
384+ attrs.push(Attribute {
385+ name: "hidden",
386+ value: "",
387+ });
388+ }
389+ attrs
390+ },
391+ [Node::text(clear_completed_text.into_bump_str())],
392+ ),
393+ ],
394+ )
395+ }
396+
397+ fn visibility_swap<'a, 'bump>(
398+ &'a self,
399+ bump: &'bump Bump,
400+ url: &'static str,
401+ target_vis: Visibility,
402+ ) -> Node<'bump>
403+ where
404+ 'a: 'bump,
405+ {
406+ Node::element(
407+ bump,
408+ "li",
409+ [on(bump, "click", move |root, vdom, _event| {
410+ C::change_visibility(root, vdom, target_vis);
411+ })],
412+ [],
413+ [Node::element(
414+ bump,
415+ "a",
416+ [],
417+ [
418+ Attribute {
419+ name: "href",
420+ value: url,
421+ },
422+ Attribute {
423+ name: "class",
424+ value: if self.visibility == target_vis {
425+ "selected"
426+ } else {
427+ ""
428+ },
429+ },
430+ ],
431+ [Node::text(target_vis.label())],
432+ )],
433+ )
434+ }
435+}
436+
437+impl<C: TodosActions> Render for Todos<C> {
438+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
439+ where
440+ 'a: 'bump,
441+ {
442+ Node::element(
443+ bump,
444+ "div",
445+ [],
446+ [],
447+ [self.header(bump), self.todos_list(bump), self.footer(bump)],
448+ )
449+ }
450+}
examples/todomvc/src/utils.rsnew file+32
@@ -0,0 +1,32 @@
1+//! Small utility functions.
2+
3+use wasm_bindgen::UnwrapThrowExt;
4+
5+/// Get the top-level window.
6+pub fn window() -> web_sys::Window {
7+ web_sys::window().unwrap_throw()
8+}
9+
10+/// Get the current location hash, if any.
11+pub fn hash() -> Option<String> {
12+ window()
13+ .location()
14+ .hash()
15+ .ok()
16+ .and_then(|h| if h.is_empty() { None } else { Some(h) })
17+}
18+
19+/// Set the current location hash.
20+pub fn set_hash(hash: &str) {
21+ window().location().set_hash(hash).unwrap_throw();
22+}
23+
24+/// Get the top-level document.
25+pub fn document() -> web_sys::Document {
26+ window().document().unwrap_throw()
27+}
28+
29+/// Get the top-level window's local storage.
30+pub fn local_storage() -> web_sys::Storage {
31+ window().local_storage().unwrap_throw().unwrap_throw()
32+}
examples/todomvc/src/visibility.rsnew file+51
@@ -0,0 +1,51 @@
1+//! Visibility filtering.
2+
3+use std::fmt;
4+use std::str::FromStr;
5+
6+/// The visibility filtering for todo items.
7+#[derive(Clone, Copy, Eq, PartialEq)]
8+pub enum Visibility {
9+ /// Show all todos.
10+ All,
11+ /// Show only active, incomplete todos.
12+ Active,
13+ /// Show only inactive, completed todos.
14+ Completed,
15+}
16+
17+impl Default for Visibility {
18+ fn default() -> Visibility {
19+ Visibility::All
20+ }
21+}
22+
23+impl FromStr for Visibility {
24+ type Err = ();
25+
26+ fn from_str(s: &str) -> Result<Self, Self::Err> {
27+ match s {
28+ "all" => Ok(Visibility::All),
29+ "active" => Ok(Visibility::Active),
30+ "completed" => Ok(Visibility::Completed),
31+ _ => Err(()),
32+ }
33+ }
34+}
35+
36+impl fmt::Display for Visibility {
37+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38+ write!(f, "{}", self.label().to_lowercase())
39+ }
40+}
41+
42+impl Visibility {
43+ /// Get a string label for this visibility.
44+ pub fn label(self) -> &'static str {
45+ match self {
46+ Visibility::All => "All",
47+ Visibility::Active => "Active",
48+ Visibility::Completed => "Completed",
49+ }
50+ }
51+}
js/change-list.js+28−2
@@ -49,7 +49,20 @@ const OP_TABLE = [
49 const pointer2 = mem32[i++];
49 const pointer2 = mem32[i++];
50 const length2 = mem32[i++];
50 const length2 = mem32[i++];
51 const value = string(mem8, pointer2, length2);
51 const value = string(mem8, pointer2, length2);
52+ const node = top(changeList.stack);
52− top(changeList.stack).setAttribute(name, value);
53+ node.setAttribute(name, value);
54+
55+ // Some attributes are "volatile" and don't work through `setAttribute`.
56+ if (name === "value") {
57+ node.value = value;
58+ }
59+ if (name === "checked") {
60+ node.checked = true;
61+ }
62+ if (name === "selected") {
63+ node.selected = true;
64+ }
65+
53 return i;
66 return i;
54 },
67 },
55
68
@@ -58,7 +71,20 @@ const OP_TABLE = [
58 const pointer = mem32[i++];
71 const pointer = mem32[i++];
59 const length = mem32[i++];
72 const length = mem32[i++];
60 const name = string(mem8, pointer, length);
73 const name = string(mem8, pointer, length);
74+ const node = top(changeList.stack);
61− top(changeList.stack).removeAttribute(name);
75+ node.removeAttribute(name);
76+
77+ // Some attributes are "volatile" and don't work through `removeAttribute`.
78+ if (name === "value") {
79+ node.value = null;
80+ }
81+ if (name === "checked") {
82+ node.checked = false;
83+ }
84+ if (name === "selected") {
85+ node.selected = false;
86+ }
87+
62 return i;
88 return i;
63 },
89 },
64
90
src/cached.rs+1
@@ -13,6 +13,7 @@ use std::ops::{Deref, DerefMut};
13
13
14/// A renderable that supports caching for when rendering is expensive but can
14/// A renderable that supports caching for when rendering is expensive but can
15/// generate the same DOM tree.
15/// generate the same DOM tree.
16+#[derive(Debug)]
16pub struct Cached<R> {
17pub struct Cached<R> {
17 inner: R,
18 inner: R,
18 bump: bumpalo::Bump,
19 bump: bumpalo::Bump,
src/change_list.rs+2−1
@@ -100,7 +100,8 @@ impl ChangeList {
100}
100}
101
101
102#[repr(u32)]
102#[repr(u32)]
103+#[derive(Clone, Copy, Debug)]
103−pub enum ChangeDiscriminant {
104+enum ChangeDiscriminant {
104 /// Immediates: `(pointer, length)`
105 /// Immediates: `(pointer, length)`
105 ///
106 ///
106 /// Stack: `[... TextNode] -> [... TextNode]`
107 /// Stack: `[... TextNode] -> [... TextNode]`
src/lib.rs+3−107
@@ -54,15 +54,11 @@
54//! }
54//! }
55//! }
55//! }
56//! ```
56//! ```
57+#![deny(missing_docs, missing_debug_implementations)]
57
58
58// Re-export the `bumpalo` crate.
59// Re-export the `bumpalo` crate.
59pub use bumpalo;
60pub use bumpalo;
60
61
61−use bumpalo::Bump;
62−use std::any::Any;
63−use std::rc::Rc;
64−use wasm_bindgen::UnwrapThrowExt;
65−
66// Only `pub` so that the wasm-bindgen bindings work.
62// Only `pub` so that the wasm-bindgen bindings work.
67#[doc(hidden)]
63#[doc(hidden)]
68pub mod change_list;
64pub mod change_list;
@@ -70,111 +66,11 @@ pub mod change_list;
70mod cached;
66mod cached;
71mod events;
67mod events;
72mod node;
68mod node;
69+mod render;
73mod vdom;
70mod vdom;
74
71
75// Re-export items at the top level.
72// Re-export items at the top level.
76pub use self::cached::Cached;
73pub use self::cached::Cached;
77pub use self::node::{on, Attribute, ElementNode, Listener, ListenerCallback, Node, TextNode};
74pub use self::node::{on, Attribute, ElementNode, Listener, ListenerCallback, Node, TextNode};
75+pub use self::render::{Render, RootRender};
78pub use self::vdom::{Vdom, VdomWeak};
76pub use self::vdom::{Vdom, VdomWeak};
79−
80−pub trait Render {
81− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
82− where
83− 'a: 'bump;
84−}
85−
86−impl<'r, R> Render for &'r R
87−where
88− R: Render,
89−{
90− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
91− where
92− 'a: 'bump,
93− {
94− (**self).render(bump)
95− }
96−}
97−
98−impl<R> Render for Rc<R>
99−where
100− R: Render,
101−{
102− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
103− where
104− 'a: 'bump,
105− {
106− (**self).render(bump)
107− }
108−}
109−
110−/// A `RootRender` is a render component that can be the root rendering component
111−/// mounted to a virtual DOM.
112−///
113−/// In addition to rendering, it must also be `'static` so that it can be owned
114−/// by the virtual DOM and `Any` so that it can be downcast to its concrete type
115−/// by event listener callbacks.
116−///
117−/// You do not need to implement this trait by hand: there is a blanket
118−/// implementation for all `Render` types that fulfill the `RootRender`
119−/// requirements.
120−pub trait RootRender: Any + Render {
121− /// Get this `&RootRender` trait object as an `&Any` trait object reference.
122− fn as_any(&self) -> &Any;
123−
124− /// Get this `&mut RootRender` trait object as an `&mut Any` trait object
125− /// reference.
126− fn as_any_mut(&mut self) -> &mut Any;
127−}
128−
129−impl<T: Any + Render> RootRender for T {
130− fn as_any(&self) -> &Any {
131− self
132− }
133−
134− fn as_any_mut(&mut self) -> &mut Any {
135− self
136− }
137−}
138−
139−impl dyn RootRender {
140− /// Downcast this shared `&dyn RootRender` trait object reference to its
141− /// underlying concrete type.
142− ///
143− /// # Panics
144− ///
145− /// Panics if this virtual DOM's root rendering component is not an `R`
146− /// instance.
147− pub fn unwrap_ref<R: RootRender>(&self) -> &R {
148− self.as_any()
149− .downcast_ref::<R>()
150− .expect_throw("bad `RootRender::unwrap_ref` call")
151− }
152−
153− /// Downcast this exclusive `&mut dyn RootRender` trait object reference to
154− /// its underlying concrete type.
155− ///
156− /// # Panics
157− ///
158− /// Panics if this virtual DOM's root rendering component is not an `R`
159− /// instance.
160− pub fn unwrap_mut<R: RootRender>(&mut self) -> &mut R {
161− self.as_any_mut()
162− .downcast_mut::<R>()
163− .expect_throw("bad `RootRender::unwrap_ref` call")
164− }
165−}
166−
167−#[cfg(test)]
168−mod tests {
169− #[test]
170− fn render_is_object_safe() {
171− #[allow(dead_code)]
172− fn takes_dyn_render(_: &dyn super::Render) {}
173− }
174−
175− #[test]
176− fn root_render_is_object_safe() {
177− #[allow(dead_code)]
178− fn takes_dyn_render(_: &dyn super::RootRender) {}
179− }
180−}
src/node.rs+18
@@ -48,9 +48,13 @@ pub struct Listener<'a> {
48 pub callback: ListenerCallback<'a>,
48 pub callback: ListenerCallback<'a>,
49}
49}
50
50
51+/// An attribute on a DOM node, such as `id="my-thing"` or
52+/// `href="https://example.com"`.
51#[derive(Clone, Debug)]
53#[derive(Clone, Debug)]
52pub struct Attribute<'a> {
54pub struct Attribute<'a> {
55+ /// The attribute name, such as `id`.
53 pub name: &'a str,
56 pub name: &'a str,
57+ /// The attribute value, such as `"my-thing"`.
54 pub value: &'a str,
58 pub value: &'a str,
55}
59}
56
60
@@ -66,6 +70,20 @@ impl fmt::Debug for Listener<'_> {
66 }
70 }
67}
71}
68
72
73+impl<'a> Attribute<'a> {
74+ /// Certain attributes are considered "volatile" and can change via user
75+ /// input that we can't see when diffing against the old virtual DOM. For
76+ /// these attributes, we want to always re-set the attribute on the physical
77+ /// DOM node, even if the old and new virtual DOM nodes have the same value.
78+ #[inline]
79+ pub(crate) fn is_volatile(&self) -> bool {
80+ match self.name {
81+ "value" | "checked" | "selected" => true,
82+ _ => false,
83+ }
84+ }
85+}
86+
69impl<'a> Node<'a> {
87impl<'a> Node<'a> {
70 /// Construct a new element node with the given tag name and children.
88 /// Construct a new element node with the given tag name and children.
71 #[inline]
89 #[inline]
src/render.rsnew file+145
@@ -0,0 +1,145 @@
1+use crate::Node;
2+use bumpalo::Bump;
3+use std::any::Any;
4+use std::rc::Rc;
5+use wasm_bindgen::UnwrapThrowExt;
6+
7+/// A trait for any component that can be rendered to HTML.
8+///
9+/// Takes a shared reference to `self` and generates the virtual DOM that
10+/// represents its rendered HTML.
11+///
12+/// ## `Bump` Allocation
13+///
14+/// `Render` implementations can use the provided `Bump` for very fast
15+/// allocation for anything that needs to be allocated during rendering.
16+///
17+/// ## The `'a: 'bump` Lifetime Bound
18+///
19+/// The `'a: 'bump` bounds enforce that `self` outlives the given bump
20+/// allocator. This means that if `self` contains a string, the string does not
21+/// need to be copied into the output `Node` and can be used by reference
22+/// instead (i.e. it prevents accidentally using the string after its been
23+/// freed). The `'a: 'bump` bound also enables abstractions like
24+/// `dodrio::Cached` that can re-use cached `Node`s across `render`s without
25+/// copying them.
26+///
27+/// ## Example
28+///
29+/// ```no_run
30+/// use dodrio::{bumpalo::Bump, Node, Render};
31+///
32+/// pub struct MyComponent;
33+///
34+/// impl Render for MyComponent {
35+/// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
36+/// where
37+/// 'a: 'bump
38+/// {
39+/// Node::text("This is my component rendered!")
40+/// }
41+/// }
42+/// ```
43+pub trait Render {
44+ /// Render `self` as a virtual DOM. Use the given `Bump` for temporary
45+ /// allocations.
46+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
47+ where
48+ 'a: 'bump;
49+}
50+
51+impl<'r, R> Render for &'r R
52+where
53+ R: Render,
54+{
55+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
56+ where
57+ 'a: 'bump,
58+ {
59+ (**self).render(bump)
60+ }
61+}
62+
63+impl<R> Render for Rc<R>
64+where
65+ R: Render,
66+{
67+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
68+ where
69+ 'a: 'bump,
70+ {
71+ (**self).render(bump)
72+ }
73+}
74+
75+/// A `RootRender` is a render component that can be the root rendering component
76+/// mounted to a virtual DOM.
77+///
78+/// In addition to rendering, it must also be `'static` so that it can be owned
79+/// by the virtual DOM and `Any` so that it can be downcast to its concrete type
80+/// by event listener callbacks.
81+///
82+/// You do not need to implement this trait by hand: there is a blanket
83+/// implementation for all `Render` types that fulfill the `RootRender`
84+/// requirements.
85+pub trait RootRender: Any + Render {
86+ /// Get this `&RootRender` trait object as an `&Any` trait object reference.
87+ fn as_any(&self) -> &Any;
88+
89+ /// Get this `&mut RootRender` trait object as an `&mut Any` trait object
90+ /// reference.
91+ fn as_any_mut(&mut self) -> &mut Any;
92+}
93+
94+impl<T: Any + Render> RootRender for T {
95+ fn as_any(&self) -> &Any {
96+ self
97+ }
98+
99+ fn as_any_mut(&mut self) -> &mut Any {
100+ self
101+ }
102+}
103+
104+impl dyn RootRender {
105+ /// Downcast this shared `&dyn RootRender` trait object reference to its
106+ /// underlying concrete type.
107+ ///
108+ /// # Panics
109+ ///
110+ /// Panics if this virtual DOM's root rendering component is not an `R`
111+ /// instance.
112+ pub fn unwrap_ref<R: RootRender>(&self) -> &R {
113+ self.as_any()
114+ .downcast_ref::<R>()
115+ .expect_throw("bad `RootRender::unwrap_ref` call")
116+ }
117+
118+ /// Downcast this exclusive `&mut dyn RootRender` trait object reference to
119+ /// its underlying concrete type.
120+ ///
121+ /// # Panics
122+ ///
123+ /// Panics if this virtual DOM's root rendering component is not an `R`
124+ /// instance.
125+ pub fn unwrap_mut<R: RootRender>(&mut self) -> &mut R {
126+ self.as_any_mut()
127+ .downcast_mut::<R>()
128+ .expect_throw("bad `RootRender::unwrap_ref` call")
129+ }
130+}
131+
132+#[cfg(test)]
133+mod tests {
134+ #[test]
135+ fn render_is_object_safe() {
136+ #[allow(dead_code)]
137+ fn takes_dyn_render(_: &dyn super::Render) {}
138+ }
139+
140+ #[test]
141+ fn root_render_is_object_safe() {
142+ #[allow(dead_code)]
143+ fn takes_dyn_render(_: &dyn super::RootRender) {}
144+ }
145+}
src/vdom.rs+14−8
@@ -22,6 +22,7 @@ use wasm_bindgen_futures::JsFuture;
22/// removed. To keep it mounted forever, use the `Vdom::forget` method.
22/// removed. To keep it mounted forever, use the `Vdom::forget` method.
23#[must_use = "A `Vdom` will only keep rendering and listening to events while it has not been \
23#[must_use = "A `Vdom` will only keep rendering and listening to events while it has not been \
24 dropped. If you want a `Vdom` to run forever, call `Vdom::forget`."]
24 dropped. If you want a `Vdom` to run forever, call `Vdom::forget`."]
25+#[derive(Debug)]
25pub struct Vdom {
26pub struct Vdom {
26 inner: Rc<VdomInner>,
27 inner: Rc<VdomInner>,
27}
28}
@@ -363,17 +364,22 @@ impl VdomInnerExclusive {
363 // Do O(n^2) passes to add/update and remove attributes, since
364 // Do O(n^2) passes to add/update and remove attributes, since
364 // there are almost always very few attributes.
365 // there are almost always very few attributes.
365 'outer: for new_attr in new {
366 'outer: for new_attr in new {
367+ if new_attr.is_volatile() {
368+ self.change_list
369+ .emit_set_attribute(new_attr.name, new_attr.value);
370+ } else {
366− for old_attr in old {
371+ for old_attr in old {
367− if old_attr.name == new_attr.name {
372+ if old_attr.name == new_attr.name {
368− if old_attr.value != new_attr.value {
373+ if old_attr.value != new_attr.value {
369− self.change_list
374+ self.change_list
370− .emit_set_attribute(new_attr.name, new_attr.value);
375+ .emit_set_attribute(new_attr.name, new_attr.value);
376+ }
377+ continue 'outer;
371 }
378 }
372− continue 'outer;
373 }
379 }
380+ self.change_list
381+ .emit_set_attribute(new_attr.name, new_attr.value);
374 }
382 }
375− self.change_list
376− .emit_set_attribute(new_attr.name, new_attr.value);
377 }
383 }
378
384
379 'outer2: for old_attr in old {
385 'outer2: for old_attr in old {