on master help sign in
Add event handling infrastructure
commit9a82a845d13fe2b44ac415e8e180cb417e529339 authorNick Fitzgerald <fitzgen@gmail.com> dateWed 30 Jan 2019, 09:49 (8 years ago) parent42d221a
17 files changed+1639−380
Cargo.lock+57−77 Cargo.toml+18−4 examples/counter/crate/Cargo.toml+3−1 examples/counter/crate/src/lib.rs+32−38 examples/hello-world/.bin/create-rust-webpack.js−38 examples/hello-world/crate/src/lib.rs+21−15 examples/input-form/crate/src/lib.rs+18−17 js/change-list.js+93−12 src/change_list.rs+110−2 src/events.rs+99 src/lib.rs+143−3 src/node.rs+108−29 src/vdom.rs+417−69 tests/web/cached.rs+65−17 tests/web/events.rs+371 tests/web/main.rs+25−10 tests/web/render.rs+59−48
Cargo.lock+57−77
@@ -1,3 +1,5 @@
1+# This file is automatically @generated by Cargo.
2+# It is not intended for manual editing.
13[[package]]
24name = "aho-corasick"
35version = "0.6.9"
@@ -46,7 +48,6 @@ dependencies = [
4648[[package]]
4749name = "bumpalo"
4850version = "1.2.0"
49−source = "registry+https://github.com/rust-lang/crates.io-index"
5051
5152[[package]]
5253name = "cc"
@@ -81,7 +82,7 @@ dependencies = [
8182 "rustfix 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
8283 "serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
8384 "serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
84− "serde_json 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
85+ "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
8586 "tester 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
8687 "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
8788]
@@ -89,10 +90,9 @@ dependencies = [
8990[[package]]
9091name = "console_error_panic_hook"
9192version = "0.1.5"
92−source = "registry+https://github.com/rust-lang/crates.io-index"
9393dependencies = [
9494 "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
95− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
95+ "wasm-bindgen 0.2.33",
9696]
9797
9898[[package]]
@@ -101,7 +101,7 @@ version = "0.1.2"
101101source = "registry+https://github.com/rust-lang/crates.io-index"
102102dependencies = [
103103 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
104− "web-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
104+ "web-sys 0.3.10",
105105]
106106
107107[[package]]
@@ -113,46 +113,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
113113name = "dodrio"
114114version = "0.1.0"
115115dependencies = [
116− "bumpalo 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
116+ "bumpalo 1.2.0",
117117 "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
118− "js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
118+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
119+ "js-sys 0.3.10",
119120 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
120− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
121− "wasm-bindgen-test 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
122− "web-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
121+ "wasm-bindgen 0.2.33",
122+ "wasm-bindgen-futures 0.3.10",
123+ "wasm-bindgen-test 0.2.33",
124+ "web-sys 0.3.10",
123125]
124126
125127[[package]]
126128name = "dodrio-counter"
127129version = "0.1.0"
128130dependencies = [
129− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
131+ "console_error_panic_hook 0.1.5",
132+ "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
130133 "dodrio 0.1.0",
131− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
132− "wasm-bindgen-test 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
133− "web-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
134+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
135+ "wasm-bindgen 0.2.33",
136+ "wasm-bindgen-test 0.2.33",
137+ "web-sys 0.3.10",
134138]
135139
136140[[package]]
137141name = "dodrio-hello-world"
138142version = "0.1.0"
139143dependencies = [
140− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
144+ "console_error_panic_hook 0.1.5",
141145 "dodrio 0.1.0",
142− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
143− "wasm-bindgen-test 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
144− "web-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
146+ "wasm-bindgen 0.2.33",
147+ "wasm-bindgen-test 0.2.33",
148+ "web-sys 0.3.10",
145149]
146150
147151[[package]]
148152name = "dodrio-input-form"
149153version = "0.1.0"
150154dependencies = [
151− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
155+ "console_error_panic_hook 0.1.5",
152156 "dodrio 0.1.0",
153− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
154− "wasm-bindgen-test 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
155− "web-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
157+ "wasm-bindgen 0.2.33",
158+ "wasm-bindgen-test 0.2.33",
159+ "web-sys 0.3.10",
156160]
157161
158162[[package]]
@@ -181,7 +185,7 @@ name = "failure_derive"
181185version = "0.1.5"
182186source = "registry+https://github.com/rust-lang/crates.io-index"
183187dependencies = [
184− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
188+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
185189 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
186190 "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
187191 "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -234,9 +238,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
234238[[package]]
235239name = "js-sys"
236240version = "0.3.10"
237−source = "registry+https://github.com/rust-lang/crates.io-index"
238241dependencies = [
239− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
242+ "wasm-bindgen 0.2.33",
240243]
241244
242245[[package]]
@@ -295,7 +298,7 @@ dependencies = [
295298
296299[[package]]
297300name = "proc-macro2"
298−version = "0.4.26"
301+version = "0.4.27"
299302source = "registry+https://github.com/rust-lang/crates.io-index"
300303dependencies = [
301304 "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -311,7 +314,7 @@ name = "quote"
311314version = "0.6.11"
312315source = "registry+https://github.com/rust-lang/crates.io-index"
313316dependencies = [
314− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
317+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
315318]
316319
317320[[package]]
@@ -361,7 +364,7 @@ dependencies = [
361364 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
362365 "serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
363366 "serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)",
364− "serde_json 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
367+ "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
365368]
366369
367370[[package]]
@@ -384,14 +387,14 @@ name = "serde_derive"
384387version = "1.0.85"
385388source = "registry+https://github.com/rust-lang/crates.io-index"
386389dependencies = [
387− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
390+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
388391 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
389392 "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
390393]
391394
392395[[package]]
393396name = "serde_json"
394−version = "1.0.37"
397+version = "1.0.38"
395398source = "registry+https://github.com/rust-lang/crates.io-index"
396399dependencies = [
397400 "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -420,7 +423,7 @@ name = "syn"
420423version = "0.15.26"
421424source = "registry+https://github.com/rust-lang/crates.io-index"
422425dependencies = [
423− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
426+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
424427 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
425428 "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
426429]
@@ -430,7 +433,7 @@ name = "synstructure"
430433version = "0.10.1"
431434source = "registry+https://github.com/rust-lang/crates.io-index"
432435dependencies = [
433− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
436+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
434437 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
435438 "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
436439 "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -514,109 +517,99 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
514517[[package]]
515518name = "wasm-bindgen"
516519version = "0.2.33"
517−source = "registry+https://github.com/rust-lang/crates.io-index"
518520dependencies = [
519− "wasm-bindgen-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
521+ "wasm-bindgen-macro 0.2.33",
520522]
521523
522524[[package]]
523525name = "wasm-bindgen-backend"
524526version = "0.2.33"
525−source = "registry+https://github.com/rust-lang/crates.io-index"
526527dependencies = [
527528 "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
528529 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
529− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
530+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
530531 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
531532 "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
532− "wasm-bindgen-shared 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
533+ "wasm-bindgen-shared 0.2.33",
533534]
534535
535536[[package]]
536537name = "wasm-bindgen-futures"
537538version = "0.3.10"
538−source = "registry+https://github.com/rust-lang/crates.io-index"
539539dependencies = [
540540 "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
541− "js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
542− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
541+ "js-sys 0.3.10",
542+ "wasm-bindgen 0.2.33",
543543]
544544
545545[[package]]
546546name = "wasm-bindgen-macro"
547547version = "0.2.33"
548−source = "registry+https://github.com/rust-lang/crates.io-index"
549548dependencies = [
550549 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
551− "wasm-bindgen-macro-support 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
550+ "wasm-bindgen-macro-support 0.2.33",
552551]
553552
554553[[package]]
555554name = "wasm-bindgen-macro-support"
556555version = "0.2.33"
557−source = "registry+https://github.com/rust-lang/crates.io-index"
558556dependencies = [
559− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
557+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
560558 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
561559 "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
562− "wasm-bindgen-backend 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
563− "wasm-bindgen-shared 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
560+ "wasm-bindgen-backend 0.2.33",
561+ "wasm-bindgen-shared 0.2.33",
564562]
565563
566564[[package]]
567565name = "wasm-bindgen-shared"
568566version = "0.2.33"
569−source = "registry+https://github.com/rust-lang/crates.io-index"
570567
571568[[package]]
572569name = "wasm-bindgen-test"
573570version = "0.2.33"
574−source = "registry+https://github.com/rust-lang/crates.io-index"
575571dependencies = [
576− "console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
572+ "console_error_panic_hook 0.1.5",
577573 "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
578− "js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
574+ "js-sys 0.3.10",
579575 "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
580− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
581− "wasm-bindgen-futures 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
582− "wasm-bindgen-test-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
576+ "wasm-bindgen 0.2.33",
577+ "wasm-bindgen-futures 0.3.10",
578+ "wasm-bindgen-test-macro 0.2.33",
583579]
584580
585581[[package]]
586582name = "wasm-bindgen-test-macro"
587583version = "0.2.33"
588−source = "registry+https://github.com/rust-lang/crates.io-index"
589584dependencies = [
590− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
585+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
591586 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
592587]
593588
594589[[package]]
595590name = "wasm-bindgen-webidl"
596591version = "0.2.27"
597−source = "registry+https://github.com/rust-lang/crates.io-index"
598592dependencies = [
599593 "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
600594 "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
601595 "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
602− "proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)",
596+ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
603597 "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
604598 "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)",
605− "wasm-bindgen-backend 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
599+ "wasm-bindgen-backend 0.2.33",
606600 "weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
607601]
608602
609603[[package]]
610604name = "web-sys"
611605version = "0.3.10"
612−source = "registry+https://github.com/rust-lang/crates.io-index"
613606dependencies = [
614607 "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
615608 "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
616− "js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)",
609+ "js-sys 0.3.10",
617610 "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
618− "wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
619− "wasm-bindgen-webidl 0.2.27 (registry+https://github.com/rust-lang/crates.io-index)",
611+ "wasm-bindgen 0.2.33",
612+ "wasm-bindgen-webidl 0.2.27",
620613]
621614
622615[[package]]
@@ -679,11 +672,9 @@ dependencies = [
679672"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
680673"checksum backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b5b493b66e03090ebc4343eb02f94ff944e0cbc9ac6571491d170ba026741eb5"
681674"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
682−"checksum bumpalo 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "38da2bd7a4195f03c07e3cb21d7963729722541c6b94ec245ac818eab48b2ff7"
683675"checksum cc 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4a8b715cb4597106ea87c7c84b2f1d452c7492033765df7f32651e66fcf749"
684676"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
685677"checksum compiletest_rs 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0d76d4322a40f6b0db7259d4f2c8a65ed8b0d84fce0bbc61b98cf47f8ec6eec3"
686−"checksum console_error_panic_hook 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6c5dd2c094474ec60a6acaf31780af270275e3153bafff2db5995b715295762e"
687678"checksum console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e7871d2947441b0fdd8e2bd1ce2a2f75304f896582c0d572162d48290683c48"
688679"checksum diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3c2b69f912779fbb121ceb775d74d51e915af17aaebc38d28a592843a2dd0a3a"
689680"checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
@@ -695,7 +686,6 @@ dependencies = [
695686"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
696687"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
697688"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
698−"checksum js-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)" = "f0edfcbe54ba2071053f2e67f5dfbcea344b69aedfb371b76e27392427a5750f"
699689"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
700690"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1"
701691"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047"
@@ -703,7 +693,7 @@ dependencies = [
703693"checksum memchr 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e1dd4eaac298c32ce07eb6ed9242eda7d82955b9170b7d6db59b2e02cc63fcb8"
704694"checksum miow 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "396aa0f2003d7df8395cb93e09871561ccc3e785f0acb369170e8cc74ddf9226"
705695"checksum nom 4.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b30adc557058ce00c9d0d7cb3c6e0b5bc6f36e2e2eabe74b0ba726d194abd588"
706−"checksum proc-macro2 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)" = "38fddd23d98b2144d197c0eca5705632d4fe2667d14a6be5df8934f8d74f1978"
696+"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
707697"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
708698"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
709699"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85"
@@ -716,7 +706,7 @@ dependencies = [
716706"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
717707"checksum serde 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)" = "534b8b91a95e0f71bca3ed5824752d558da048d4248c91af873b63bd60519752"
718708"checksum serde_derive 1.0.85 (registry+https://github.com/rust-lang/crates.io-index)" = "a915306b0f1ac5607797697148c223bedeaa36bcc2e28a01441cd638cc6567b4"
719−"checksum serde_json 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "4b90a9fbe1211e57d3e1c15670f1cb00802988fb23a1a4aad7a2b63544f1920e"
709+"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9"
720710"checksum socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c4d11a52082057d87cb5caa31ad812f4504b97ab44732cd8359df2e9ff9f48e7"
721711"checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3"
722712"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9"
@@ -732,16 +722,6 @@ dependencies = [
732722"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
733723"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
734724"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
735−"checksum wasm-bindgen 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "2d8c1eb210a0e91e24feb8ccd6f7484a5d442bfdf2ff179204b3a1d16e1029cc"
736−"checksum wasm-bindgen-backend 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "57ab9a5e88bf5dea5be82bb5f8b68c0f8e550675796ac88570ea4c4e89923413"
737−"checksum wasm-bindgen-futures 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d35a917b7c857bf8bce3e47d51d7d74a074d467aa2b750857fc7d9f878483124"
738−"checksum wasm-bindgen-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "52909345426e198a0d34f63526f9f4d86ca50c24b4a22a019d0bc86570ffe1e3"
739−"checksum wasm-bindgen-macro-support 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "2a130a5906bd540390cda0a28e7f8a2d450222461beb92a5eb5c6a33b8b8bc2a"
740−"checksum wasm-bindgen-shared 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "9c406cd5b42c36db6f76f17d28bcd26187fd90cda686231f3d04de07a716fc3a"
741−"checksum wasm-bindgen-test 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "edda7c4a76a8b2c1852e2bf707c3c537ae1a910fe5324bda77b10d249b6afd51"
742−"checksum wasm-bindgen-test-macro 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "1a97eada8f48c14b098237f45332b3b9d88bbc15ee9f93bf2478853fae85ebf2"
743−"checksum wasm-bindgen-webidl 0.2.27 (registry+https://github.com/rust-lang/crates.io-index)" = "068ab8c6b5682c5ac2aa7c2178ecdfc91143e3d62da458997ee1299c3fbf0e85"
744−"checksum web-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)" = "53890ae11f09f4f08f8dc0d76d03cedfb4aa1f667b17ee1b9d6aa23b314cb940"
745725"checksum weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26a4c67f132386d965390b8a734d5d10adbcd30eb5cc74bd9229af8b83f10044"
746726"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
747727"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
Cargo.toml+18−4
@@ -9,16 +9,19 @@ default = []
99
1010[dependencies]
1111bumpalo = "1.0.2"
12−wasm-bindgen = "0.2.32"
13−js-sys = "0.3.9"
12+futures = "0.1"
13+wasm-bindgen = "0.2.33"
14+wasm-bindgen-futures = "0.3.10"
15+js-sys = "0.3.10"
1416log = "0.4.6"
1517
1618[dependencies.web-sys]
17−version = "0.3.9"
19+version = "0.3.10"
1820features = [
1921 "console",
2022 "Document",
2123 "Element",
24+ "Event",
2225 "Node",
2326 "Window",
2427]
@@ -28,9 +31,11 @@ wasm-bindgen-test = "0.2"
2831console_log = "0.1.2"
2932
3033[dev-dependencies.web-sys]
31−version = "0.3.9"
34+version = "0.3.10"
3235features = [
3336 "Attr",
37+ "EventTarget",
38+ "HtmlElement",
3439 "NamedNodeMap",
3540 "NodeList",
3641]
@@ -46,3 +51,12 @@ members = [
4651 "./examples/hello-world/crate",
4752 "./examples/input-form/crate",
4853]
54+
55+[patch.crates-io]
56+wasm-bindgen = { path = "../wasm-bindgen" }
57+web-sys = { path = "../wasm-bindgen/crates/web-sys" }
58+js-sys = { path = "../wasm-bindgen/crates/js-sys" }
59+wasm-bindgen-futures = { path = "../wasm-bindgen/crates/futures" }
60+wasm-bindgen-test = { path = "../wasm-bindgen/crates/test" }
61+bumpalo = { path = "../bumpalo" }
62+console_error_panic_hook = { path = "../console_error_panic_hook" }
examples/counter/crate/Cargo.toml+3−1
@@ -10,9 +10,11 @@ crate-type = ["cdylib", "rlib"]
1010[features]
1111
1212[dependencies]
13+console_error_panic_hook = "0.1.1"
14+console_log = "0.1.2"
1315dodrio = { path = "../../.." }
16+log = "0.4.6"
1417wasm-bindgen = "0.2"
15−console_error_panic_hook = "0.1.1"
1618
1719[dependencies.web-sys]
1820version = "0.3.8"
examples/counter/crate/src/lib.rs+32−38
@@ -1,7 +1,7 @@
11use dodrio::bumpalo::{self, Bump};
2−use dodrio::{Attribute, Node, Render};
2+use dodrio::{on, Node, Render};
3+use log::*;
34use wasm_bindgen::prelude::*;
4−use wasm_bindgen::JsCast;
55
66struct Counter {
77 val: isize,
@@ -28,31 +28,34 @@ impl Render for Counter {
2828 {
2929 let val = bumpalo::format!(in bump, "{}", self.val);
3030
31− let increment = Node::element(
32− bump,
33− "button",
34− [Attribute {
35− name: "data-action",
36− value: "increment",
37− }],
38− [Node::text("+")],
39− );
40−
41− let decrement = Node::element(
42− bump,
43− "button",
44− [Attribute {
45− name: "data-action",
46− value: "decrement",
47− }],
48− [Node::text("-")],
49− );
50−
5131 Node::element(
5232 bump,
5333 "div",
5434 [],
55− [increment, Node::text(val.into_bump_str()), decrement],
35+ [],
36+ [
37+ Node::element(
38+ bump,
39+ "button",
40+ [on(bump, "click", |root, vdom, _event| {
41+ root.unwrap_mut::<Counter>().increment();
42+ vdom.schedule_render();
43+ })],
44+ [],
45+ [Node::text("+")],
46+ ),
47+ Node::text(val.into_bump_str()),
48+ Node::element(
49+ bump,
50+ "button",
51+ [on(bump, "click", |root, vdom, _event| {
52+ root.unwrap_mut::<Counter>().decrement();
53+ vdom.schedule_render();
54+ })],
55+ [],
56+ [Node::text("-")],
57+ ),
58+ ],
5659 )
5760 }
5861}
@@ -60,27 +63,18 @@ impl Render for Counter {
6063#[wasm_bindgen]
6164pub fn run() {
6265 console_error_panic_hook::set_once();
66+ console_log::init_with_level(Level::Trace).expect("should initialize logging OK");
6367
6468 let window = web_sys::window().unwrap();
6569 let document = window.document().unwrap();
6670 let body = document.body().unwrap();
6771
72+ // Construct a new counter component.
6873 let counter = Counter::new();
69− let mut vdom = dodrio::Vdom::new(&body, counter);
7074
71− let on_click = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
72− match event
73− .target()
74− .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
75− .and_then(|e| e.get_attribute("data-action"))
76− {
77− Some(ref s) if s == "increment" => vdom.component_mut().increment(),
78− Some(ref s) if s == "decrement" => vdom.component_mut().decrement(),
79− _ => {}
80− }
81− vdom.render();
82− }) as Box<FnMut(_)>);
75+ // Mount our counter component to the `<body>`.
76+ let vdom = dodrio::Vdom::new(&body, counter);
8377
84− let _ = body.add_event_listener_with_callback("click", on_click.as_ref().unchecked_ref());
85− on_click.forget();
78+ // Run the virtual DOM and its listeners forever.
79+ vdom.forget();
8680}
examples/hello-world/.bin/create-rust-webpack.jsdeleted−38
@@ -1,38 +0,0 @@
1−#!/usr/bin/env node
2−
3−const { spawn } = require("child_process");
4−const fs = require("fs");
5−
6−let folderName = '.';
7−
8−if (process.argv.length >= 3) {
9− folderName = process.argv[2];
10− if (!fs.existsSync(folderName)) {
11− fs.mkdirSync(folderName);
12− }
13−}
14−
15−const clone = spawn("git", ["clone", "https://github.com/rustwasm/rust-webpack-template.git", folderName]);
16−
17−clone.on("close", (code) => {
18− if (code !== 0) {
19− handleError("install", code);
20− } else {
21− console.log(" 🦀 Rust + 🕸 WebAssembly + Webpack = ❤️ ");
22−
23− const install = spawn('npm', ['install'], { cwd: folderName });
24− install.on("close", (code) => {
25− if (code !== 0) {
26− handleError("install", code);
27− } else {
28− console.log(" Installed dependencies ✅ ");
29− }
30− });
31− }
32−});
33−
34−function handleError(type, errCode) {
35− // TODO(sven): handle error here
36− console.error()
37− process.exit(errCode);
38−}
examples/hello-world/crate/src/lib.rs+21−15
@@ -1,16 +1,18 @@
11use dodrio::bumpalo::Bump;
2−use dodrio::{Node, Render};
2+use dodrio::{Node, Render, Vdom};
33use wasm_bindgen::prelude::*;
44
5−struct HelloWorld<'a>(&'a str);
5+struct Hello<'who> {
6+ who: &'who str,
7+}
68
7−impl<'who> HelloWorld<'who> {
8− fn new(who: &str) -> HelloWorld {
9− HelloWorld(who)
9+impl<'who> Hello<'who> {
10+ fn new(who: &str) -> Hello {
11+ Hello { who }
1012 }
1113}
1214
13−impl<'who> Render for HelloWorld<'who> {
15+impl<'who> Render for Hello<'who> {
1416 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
1517 where
1618 'a: 'bump,
@@ -19,7 +21,8 @@ impl<'who> Render for HelloWorld<'who> {
1921 bump,
2022 "p",
2123 [],
22− [Node::text("Hello, "), Node::text(self.0), Node::text("!")],
24+ [],
25+ [Node::text("Hello, "), Node::text(self.who), Node::text("!")],
2326 )
2427 }
2528}
@@ -28,14 +31,17 @@ impl<'who> Render for HelloWorld<'who> {
2831pub fn run() {
2932 console_error_panic_hook::set_once();
3033
31− let window = web_sys::window().unwrap();
32− let document = window.document().unwrap();
33− let body = document.body().unwrap();
34+ let window = web_sys::window().unwrap_throw();
35+ let document = window.document().unwrap_throw();
36+ let body = document.body().unwrap_throw();
37+
38+ // Create a new `Hello` render component.
39+ let component = Hello::new("World");
3440
35− // Create a new dodrio vdom contained in the body, with an initial virtual
36− // dom.
37− let vdom = dodrio::Vdom::new(body.as_ref(), HelloWorld::new("World"));
41+ // Create a virtual DOM and mount it and the `Hello` render component to the
42+ // `<body>`.
43+ let vdom = Vdom::new(body.as_ref(), component);
3844
39− // Render a new node tree into the virtual dom.
40− vdom.render_component(HelloWorld::new("Dodrio"));
45+ // Run the virtual DOM forever and don't unmount it.
46+ vdom.forget();
4147}
examples/input-form/crate/src/lib.rs+18−17
@@ -1,5 +1,5 @@
11use dodrio::bumpalo::{self, Bump};
2−use dodrio::{Attribute, Node, Render};
2+use dodrio::{on, Attribute, Node, Render};
33use wasm_bindgen::prelude::*;
44use wasm_bindgen::JsCast;
55
@@ -26,6 +26,18 @@ impl Render for SayHelloTo {
2626 let input = Node::element(
2727 bump,
2828 "input",
29+ [on(bump, "input", |root, vdom, event| {
30+ let input = match event
31+ .target()
32+ .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
33+ {
34+ Some(input) => input,
35+ None => return,
36+ };
37+
38+ root.unwrap_mut::<SayHelloTo>().set_who(input.value());
39+ vdom.schedule_render();
40+ })],
2941 [
3042 Attribute {
3143 name: "type",
@@ -42,7 +54,7 @@ impl Render for SayHelloTo {
4254 let hello = bumpalo::format!(in bump, "Hello, {}!", self.who);
4355 let hello = Node::text(hello.into_bump_str());
4456
45− Node::element(bump, "div", [], [input, hello])
57+ Node::element(bump, "div", [], [], [input, hello])
4658 }
4759}
4860
@@ -55,21 +67,10 @@ pub fn run() {
5567 let body = document.body().unwrap();
5668
5769 let say_hello = SayHelloTo::new("World");
58− let mut vdom = dodrio::Vdom::new(&body, say_hello);
59−
60− let on_input = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
61− let input = match event
62− .target()
63− .and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
64− {
65− Some(input) => input,
66− None => return,
67− };
6870
69− vdom.component_mut().set_who(input.value());
70− vdom.render();
71− }) as Box<FnMut(_)>);
71+ // Mount the component to the `<body>`.
72+ let vdom = dodrio::Vdom::new(&body, say_hello);
7273
73− let _ = body.add_event_listener_with_callback("input", on_input.as_ref().unchecked_ref());
74− on_input.forget();
74+ // Run the component forever.
75+ vdom.forget();
7576}
js/change-list.js+93−12
@@ -11,7 +11,7 @@ function string(mem, pointer, length) {
1111
1212const OP_TABLE = [
1313 // 0
14− function setText(stack, mem8, mem32, i) {
14+ function setText(stack, eventsTrampoline, listeners, mem8, mem32, i) {
1515 // console.log("setText");
1616 const pointer = mem32[i++];
1717 const length = mem32[i++];
@@ -23,7 +23,7 @@ const OP_TABLE = [
2323 },
2424
2525 // 1
26− function removeSelfAndNextSiblings(stack, mem8, mem32, i) {
26+ function removeSelfAndNextSiblings(stack, eventsTrampoline, listeners, mem8, mem32, i) {
2727 // console.log("removeSelfAndNextSiblings");
2828 const node = stack.pop();
2929 // console.log(" top(stack) =", node);
@@ -38,7 +38,7 @@ const OP_TABLE = [
3838 },
3939
4040 // 2
41− function replaceWith(stack, mem8, mem32, i) {
41+ function replaceWith(stack, eventsTrampoline, listeners, mem8, mem32, i) {
4242 // console.log("replaceWith");
4343 const newNode = stack.pop();
4444 // console.log(" newNode =", newNode);
@@ -50,7 +50,7 @@ const OP_TABLE = [
5050 },
5151
5252 // 3
53− function setAttribute(stack, mem8, mem32, i) {
53+ function setAttribute(stack, eventsTrampoline, listeners, mem8, mem32, i) {
5454 // console.log("setAttribute");
5555 const pointer1 = mem32[i++];
5656 const length1 = mem32[i++];
@@ -66,7 +66,7 @@ const OP_TABLE = [
6666 },
6767
6868 // 4
69− function removeAttribute(stack, mem8, mem32, i) {
69+ function removeAttribute(stack, eventsTrampoline, listeners, mem8, mem32, i) {
7070 // console.log("removeAttribute");
7171 const pointer = mem32[i++];
7272 const length = mem32[i++];
@@ -77,7 +77,7 @@ const OP_TABLE = [
7777 },
7878
7979 // 5
80− function pushFirstChild(stack, mem8, mem32, i) {
80+ function pushFirstChild(stack, eventsTrampoline, listeners, mem8, mem32, i) {
8181 // console.log("pushFirstChild");
8282 // console.log(" top(stack) =", top(stack));
8383 stack.push(top(stack).firstChild);
@@ -85,7 +85,7 @@ const OP_TABLE = [
8585 },
8686
8787 // 6
88− function popPushNextSibling(stack, mem8, mem32, i) {
88+ function popPushNextSibling(stack, eventsTrampoline, listeners, mem8, mem32, i) {
8989 // console.log("popPushNextSibling");
9090 // console.log(" top(stack) =", top(stack));
9191 const node = stack.pop();
@@ -94,7 +94,7 @@ const OP_TABLE = [
9494 },
9595
9696 // 7
97− function pop(stack, mem8, mem32, i) {
97+ function pop(stack, eventsTrampoline, listeners, mem8, mem32, i) {
9898 // console.log("pop");
9999 // console.log(" top(stack) =", top(stack));
100100 stack.pop();
@@ -102,7 +102,7 @@ const OP_TABLE = [
102102 },
103103
104104 // 8
105− function appendChild(stack, mem8, mem32, i) {
105+ function appendChild(stack, eventsTrampoline, listeners, mem8, mem32, i) {
106106 // console.log("appendChild");
107107 // console.log(" top(stack) =", top(stack));
108108 const child = stack.pop();
@@ -111,7 +111,7 @@ const OP_TABLE = [
111111 },
112112
113113 // 9
114− function createTextNode(stack, mem8, mem32, i) {
114+ function createTextNode(stack, eventsTrampoline, listeners, mem8, mem32, i) {
115115 // console.log("appendChild");
116116 const pointer = mem32[i++];
117117 const length = mem32[i++];
@@ -123,7 +123,7 @@ const OP_TABLE = [
123123 },
124124
125125 // 10
126− function createElement(stack, mem8, mem32, i) {
126+ function createElement(stack, eventsTrampoline, listeners, mem8, mem32, i) {
127127 // console.log("createElement");
128128 const pointer = mem32[i++];
129129 const length = mem32[i++];
@@ -132,17 +132,92 @@ const OP_TABLE = [
132132 // console.log(" top(stack) =", top(stack));
133133 stack.push(document.createElement(tagName));
134134 return i;
135+ },
136+
137+ // 11
138+ function newEventListener(stack, eventsTrampoline, listeners, mem8, mem32, i) {
139+ const pointer = mem32[i++];
140+ const length = mem32[i++];
141+ const eventType = string(mem8, pointer, length);
142+ const a = mem32[i++];
143+ const b = mem32[i++];
144+ const el = top(stack);
145+ const listener = new Listener(a, b, eventType, eventsTrampoline, el);
146+ listeners.add(listener);
147+ el.addEventListener(eventType, listener.callback);
148+ el[`dodrio-${eventType}`] = listener;
149+ return i;
150+ },
151+
152+ // 12
153+ function updateEventListener(stack, eventsTrampoline, listeners, mem8, mem32, i) {
154+ const pointer = mem32[i++];
155+ const length = mem32[i++];
156+ const eventType = string(mem8, pointer, length);
157+ const el = top(stack);
158+ const listener = el[`dodrio-${eventType}`];
159+ listener.a = mem32[i++];
160+ listener.b = mem32[i++];
161+ return i;
162+ },
163+
164+ // 13
165+ function removeEventListener(stack, eventsTrampoline, listeners, mem8, mem32, i) {
166+ const pointer = mem32[i++];
167+ const length = mem32[i++];
168+ const eventType = string(mem8, pointer, length);
169+ const el = top(stack);
170+ const listener = el[`dodrio-${eventType}`];
171+ el.removeEventListener(eventType, listener.callback);
172+ listeners.delete(listener);
173+ return i;
135174 }
136175];
137176
177+class Listener {
178+ constructor(a, b, eventType, trampoline, el) {
179+ this.a = a;
180+ this.b = b;
181+ this.eventType = eventType;
182+ this.trampoline = trampoline;
183+ this.el = el;
184+ this.callback = this.callback.bind(this);
185+ }
186+
187+ callback(event) {
188+ this.trampoline(event, this.a, this.b);
189+ }
190+}
191+
138192// export
139193class ChangeList {
140194 constructor(container) {
195+ this.listeners = new Set();
196+ this.trampoline = null;
141197 this.container = container;
142198 this.ranges = [];
143199 this.stack = [];
144200 }
145201
202+ unmount() {
203+ for (const listener of this.listeners) {
204+ listener.el.removeEventListener(listener.eventType, listener.callback);
205+ listener.trampoline = () => {
206+ throw new Error("invocation of listener after it has been removed");
207+ };
208+ listener.a = 0;
209+ listener.b = 0;
210+ }
211+
212+ // Null out all of our properties just to ensure that if we mistakenly ever
213+ // call a method on this instance again, it will throw.
214+ this.listeners = null;
215+ this.trampoline = null;
216+ this.container = null;
217+ this.ranges = null;
218+ this.stack = null;
219+ }
220+
146221 addChangeListRange(start, len) {
147222 this.ranges.push(start);
148223 this.ranges.push(len);
@@ -175,8 +250,14 @@ class ChangeList {
175250 // console.log();
176251 // console.log(OP_TABLE[op].name);
177252 // console.log(this.stack);
178− i = OP_TABLE[op](this.stack, mem8, mem32, i);
253+ i = OP_TABLE[op](this.stack, this.eventsTrampoline, this.listeners, mem8, mem32, i);
179254 }
180255 }
256+
257+ initEventsTrampoline(trampoline) {
258+ this.eventsTrampoline = (...args) => {
259+ trampoline(...args);
260+ };
261+ }
181262}
182263window.ChangeList = ChangeList;
src/change_list.rs+110−2
@@ -1,5 +1,7 @@
1+use crate::Listener;
12use bumpalo::{Bump, BumpAllocSafe};
23use log::*;
4+use std::fmt;
35use std::sync::Once;
46use wasm_bindgen::prelude::*;
57
@@ -14,17 +16,44 @@ pub mod js {
1416 #[wasm_bindgen(constructor)]
1517 pub fn new(container: &web_sys::Node) -> ChangeList;
1618
19+ #[wasm_bindgen(structural, method)]
20+ pub fn unmount(this: &ChangeList);
21+
1722 #[wasm_bindgen(structural, method, js_name = addChangeListRange)]
1823 pub fn add_change_list_range(this: &ChangeList, start: usize, len: usize);
1924
2025 #[wasm_bindgen(structural, method, js_name = applyChanges)]
2126 pub fn apply_changes(this: &ChangeList, memory: JsValue);
27+
28+ #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)]
29+ pub fn init_events_trampoline(
30+ this: &ChangeList,
31+ trampoline: &Closure<Fn(web_sys::Event, u32, u32)>,
32+ );
2233 }
2334}
2435
2536pub(crate) struct ChangeList {
2637 bump: Bump,
2738 js: js::ChangeList,
39+ events_trampoline: Option<Closure<Fn(web_sys::Event, u32, u32)>>,
40+}
41+
42+impl Drop for ChangeList {
43+ fn drop(&mut self) {
44+ debug!("Dropping ChangeList");
45+ self.js.unmount();
46+ }
47+}
48+
49+impl fmt::Debug for ChangeList {
50+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51+ f.debug_struct("ChangeList")
52+ .field("bump", &self.bump)
53+ .field("js", &self.js)
54+ .field("events_trampoline", &"..")
55+ .finish()
56+ }
2857}
2958
3059impl ChangeList {
@@ -42,7 +71,11 @@ impl ChangeList {
4271
4372 let bump = Bump::new();
4473 let js = js::ChangeList::new(container);
45− ChangeList { bump, js }
74+ ChangeList {
75+ bump,
76+ js,
77+ events_trampoline: None,
78+ }
4679 }
4780
4881 pub(crate) fn apply_changes(&mut self) {
@@ -55,6 +88,15 @@ impl ChangeList {
5588 js.apply_changes(wasm_bindgen::memory());
5689 self.bump.reset();
5790 }
91+
92+ pub(crate) fn init_events_trampoline(
93+ &mut self,
94+ trampoline: Closure<Fn(web_sys::Event, u32, u32)>,
95+ ) {
96+ debug_assert!(self.events_trampoline.is_none());
97+ self.js.init_events_trampoline(&trampoline);
98+ self.events_trampoline = Some(trampoline);
99+ }
58100}
59101
60102#[wasm_bindgen]
@@ -166,6 +208,37 @@ pub enum ChangeDiscriminant {
166208 /// stack.push(createElement(String(pointer, length))
167209 /// ```
168210 CreateElement = 10,
211+
212+ /// Immediate: `(pointer, length, A, B)`
213+ ///
214+ /// Stack: `[... Node] -> [... Node]`
215+ ///
216+ /// ```text
217+ /// event = String(pointer, length)
218+ /// callback = ProxyToRustCallback(A, B);
219+ /// stack.top().addEventListener(event, callback);
220+ /// ```
221+ NewEventListener = 11,
222+
223+ /// Immediate: `(pointer, length, A, B)`
224+ ///
225+ /// Stack: `[... Node] -> [... Node]`
226+ ///
227+ /// ```text
228+ /// event = String(pointer, length)
229+ /// new_callback = ProxyToRustCallback(A, B);
230+ /// stack.top().updateEventlistener(new_callback)
231+ /// ```
232+ UpdateEventListener = 12,
233+
234+ /// Immediates: `(pointer, length)`
235+ ///
236+ /// Stack: `[... Node] -> [... Node]`
237+ ///
238+ /// ```text
239+ /// stack.top().removeEventListener(String(pointer, length));
240+ /// ```
241+ RemoveEventListener = 13,
169242}
170243
171244impl BumpAllocSafe for ChangeDiscriminant {}
@@ -186,7 +259,7 @@ impl ChangeList {
186259 self.bump.alloc([discriminant as u32, a, b]);
187260 }
188261
189− // Note: no 3-immediates opcodes at this time.
262+ // Note: no 3-immediate opcodes at this time.
190263
191264 // Allocate an opcode with four immediates.
192265 fn op4(&self, discriminant: ChangeDiscriminant, a: u32, b: u32, c: u32, d: u32) {
@@ -271,4 +344,39 @@ impl ChangeList {
271344 tag_name.len() as u32,
272345 );
273346 }
347+
348+ pub(crate) fn emit_new_event_listener(&self, listener: &Listener) {
349+ debug!("emit_new_event_listener({:?})", listener);
350+ let (a, b) = listener.get_callback_parts();
351+ debug_assert!(a != 0);
352+ self.op4(
353+ ChangeDiscriminant::NewEventListener,
354+ listener.event.as_ptr() as u32,
355+ listener.event.len() as u32,
356+ a,
357+ b,
358+ );
359+ }
360+
361+ pub(crate) fn emit_update_event_listener(&self, listener: &Listener) {
362+ debug!("emit_update_event_listener({:?})", listener);
363+ let (a, b) = listener.get_callback_parts();
364+ debug_assert!(a != 0);
365+ self.op4(
366+ ChangeDiscriminant::UpdateEventListener,
367+ listener.event.as_ptr() as u32,
368+ listener.event.len() as u32,
369+ a,
370+ b,
371+ );
372+ }
373+
374+ pub(crate) fn emit_remove_event_listener(&self, event: &str) {
375+ debug!("emit_remove_event_listener({:?})", event);
376+ self.op2(
377+ ChangeDiscriminant::RemoveEventListener,
378+ event.as_ptr() as u32,
379+ event.len() as u32,
380+ );
381+ }
274382}
src/events.rsnew file+99
@@ -0,0 +1,99 @@
1+use crate::vdom::{VdomInner, VdomWeak};
2+use crate::{Listener, ListenerCallback};
3+use log::*;
4+use std::cell::RefCell;
5+use std::collections::HashMap;
6+use std::fmt;
7+use std::mem;
8+use std::rc::{Rc, Weak};
9+use wasm_bindgen::closure::Closure;
10+use wasm_bindgen::prelude::*;
11+
12+/// The events registry manages event listeners for a virtual DOM.
13+///
14+/// The events registry is persistent across virtual DOM rendering and double
15+/// buffering.
16+pub(crate) struct EventsRegistry {
17+ vdom: Weak<VdomInner>,
18+ active: HashMap<(u32, u32), ListenerCallback<'static>>,
19+}
20+
21+impl fmt::Debug for EventsRegistry {
22+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
23+ f.debug_struct("EventsRegistry")
24+ .field("active", &self.active.keys().collect::<Vec<_>>())
25+ .finish()
26+ }
27+}
28+
29+impl EventsRegistry {
30+ /// Construct a new events registry and JS function trampoline that weakly
31+ /// holds the new registry and can be used by JS to invoke listeners on the
32+ /// Rust side.
33+ pub(crate) fn new(
34+ vdom: Weak<VdomInner>,
35+ ) -> (
36+ Rc<RefCell<EventsRegistry>>,
37+ Closure<Fn(web_sys::Event, u32, u32)>,
38+ ) {
39+ let registry = Rc::new(RefCell::new(EventsRegistry {
40+ vdom,
41+ active: HashMap::new(),
42+ }));
43+
44+ let weak_registry = Rc::downgrade(&registry);
45+ let closure = Closure::wrap(Box::new(move |event, a, b| {
46+ debug!("Events trampoline invoked with (0x{:x}, 0x{:x})", a, b);
47+ debug_assert!(a != 0);
48+
49+ // if the VdomInnerExclusive is keeping this closure alive, then the
50+ // VdomInnerExclusive should also be keeping the registry alive
51+ let registry = weak_registry.upgrade().unwrap_throw();
52+ let registry = registry.borrow();
53+
54+ match registry.active.get(&(a, b)) {
55+ None => warn!(
56+ "EventsRegistry closure invoked with unknown listener parts: \
57+ (0x{:x}, 0x{:x})",
58+ a, b
59+ ),
60+ Some(callback) => {
61+ let vdom = registry.vdom.upgrade().expect_throw(
62+ "if the registry is still around, then the vdom should still be around",
63+ );
64+ let vdom_weak = VdomWeak::new(&vdom);
65+ let mut vdom = vdom.exclusive.borrow_mut();
66+ let component = vdom.component_raw_mut();
67+ callback(component, vdom_weak, event);
68+ }
69+ }
70+ }) as Box<Fn(web_sys::Event, u32, u32)>);
71+
72+ (registry, closure)
73+ }
74+
75+ /// Add an event listener to the registry, exposing to JS.
76+ ///
77+ /// # Unsafety
78+ ///
79+ /// The listener's lifetime is extended to `'static` and it is the caller's
80+ /// responsibility to ensure that the listener is not kept in the registry
81+ /// after it is dropped. This is typically done via clearing the whole
82+ /// registry on each render and repopulating it during diffing.
83+ pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
84+ let id = listener.get_callback_parts();
85+ debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
86+ debug_assert!(id.0 != 0);
87+
88+ let callback =
89+ mem::transmute::<ListenerCallback<'a>, ListenerCallback<'static>>(listener.callback);
90+ let old = self.active.insert(id, callback);
91+ debug_assert!(old.is_none());
92+ }
93+
94+ /// Clear all event listeners from the registry.
95+ pub(crate) fn clear_active_listeners(&mut self) {
96+ debug!("EventsRegistry::clear_active_listeners()");
97+ self.active.clear();
98+ }
99+}
src/lib.rs+143−3
@@ -1,22 +1,78 @@
1−//! TODO
1+//! The `dodrio` virtual DOM.
2+//!
3+//! ## Example
4+//!
5+//! ```no_run
6+//! /// A component that greets someone.
7+//! pub struct Hello<'who> {
8+//! who: &'who str,
9+//! }
10+//!
11+//! impl<'who> Hello<'who> {
12+//! /// Construct a new `Hello` component that greets the given `who`.
13+//! pub fn new(who: &str) -> Hello {
14+//! Hello { who }
15+//! }
16+//! }
17+//!
18+//! impl<'who> Render for Hello<'who> {
19+//! fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
20+//! where
21+//! 'a: 'bump,
22+//! {
23+//! let id = bumpalo::format!(in bump, "hello-{}", self.who);
24+//! Node::element(
25+//! bump,
26+//! // The element's tag name. In this case a `<p>` element.
27+//! "p",
28+//! // Listeners.
29+//! [on(bump, "click", |root, _vdom, _event| {
30+//! let hello = root.unwrap_mut::<Hello>();
31+//! let window = web_sys::window().expect_throw("should have a `Window` on the Web");
32+//! window.alert(hello.who);
33+//! })],
34+//! // Attributes.
35+//! [Attribute { name: "id", value: &id }],
36+//! // Child nodes.
37+//! [
38+//! Node::text("Hello, "),
39+//! Node::element(
40+//! bump,
41+//! "strong",
42+//! [],
43+//! [],
44+//! [
45+//! Node::text(self.who),
46+//! Node::text("!"),
47+//! ],
48+//! ),
49+//! ],
50+//! )
51+//! }
52+//! }
53+//! ```
254
355// Re-export the `bumpalo` crate.
456pub use bumpalo;
557
658use bumpalo::Bump;
59+use std::any::Any;
60+use std::rc::Rc;
61+use wasm_bindgen::UnwrapThrowExt;
762
863// Only `pub` so that the wasm-bindgen bindings work.
964#[doc(hidden)]
1065pub mod change_list;
1166
1267mod cached;
68+mod events;
1369mod node;
1470mod vdom;
1571
1672// Re-export items at the top level.
1773pub use self::cached::Cached;
18−pub use self::node::{Attribute, ElementNode, Node, TextNode};
19−pub use self::vdom::Vdom;
74+pub use self::node::{on, Attribute, ElementNode, Listener, ListenerCallback, Node, TextNode};
75+pub use self::vdom::{Vdom, VdomWeak};
2076
2177pub trait Render {
2278 fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
@@ -35,3 +91,87 @@ where
3591 (**self).render(bump)
3692 }
3793}
94+
95+impl<R> Render for Rc<R>
96+where
97+ R: Render,
98+{
99+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
100+ where
101+ 'a: 'bump,
102+ {
103+ (**self).render(bump)
104+ }
105+}
106+
107+/// A `RootRender` is a render component that can be the root rendering component
108+/// mounted to a virtual DOM.
109+///
110+/// In addition to rendering, it must also be `'static` so that it can be owned
111+/// by the virtual DOM and `Any` so that it can be downcast to its concrete type
112+/// by event listener callbacks.
113+///
114+/// You do not need to implement this trait by hand: there is a blanket
115+/// implementation for all `Render` types that fulfill the `RootRender`
116+/// requirements.
117+pub trait RootRender: Any + Render {
118+ /// Get this `&RootRender` trait object as an `&Any` trait object reference.
119+ fn as_any(&self) -> &Any;
120+
121+ /// Get this `&mut RootRender` trait object as an `&mut Any` trait object
122+ /// reference.
123+ fn as_any_mut(&mut self) -> &mut Any;
124+}
125+
126+impl<T: Any + Render> RootRender for T {
127+ fn as_any(&self) -> &Any {
128+ self
129+ }
130+
131+ fn as_any_mut(&mut self) -> &mut Any {
132+ self
133+ }
134+}
135+
136+impl dyn RootRender {
137+ /// Downcast this shared `&dyn RootRender` trait object reference to its
138+ /// underlying concrete type.
139+ ///
140+ /// # Panics
141+ ///
142+ /// Panics if this virtual DOM's root rendering component is not an `R`
143+ /// instance.
144+ pub fn unwrap_ref<R: RootRender>(&self) -> &R {
145+ self.as_any()
146+ .downcast_ref::<R>()
147+ .expect_throw("bad `RootRender::unwrap_ref` call")
148+ }
149+
150+ /// Downcast this exclusive `&mut dyn RootRender` trait object reference to
151+ /// its underlying concrete type.
152+ ///
153+ /// # Panics
154+ ///
155+ /// Panics if this virtual DOM's root rendering component is not an `R`
156+ /// instance.
157+ pub fn unwrap_mut<R: RootRender>(&mut self) -> &mut R {
158+ self.as_any_mut()
159+ .downcast_mut::<R>()
160+ .expect_throw("bad `RootRender::unwrap_ref` call")
161+ }
162+}
163+
164+#[cfg(test)]
165+mod tests {
166+ #[test]
167+ fn render_is_object_safe() {
168+ #[allow(dead_code)]
169+ fn takes_dyn_render(_: &dyn super::Render) {}
170+ }
171+
172+ #[test]
173+ fn root_render_is_object_safe() {
174+ #[allow(dead_code)]
175+ fn takes_dyn_render(_: &dyn super::RootRender) {}
176+ }
177+}
src/node.rs+108−29
@@ -1,4 +1,7 @@
1+use crate::{RootRender, VdomWeak};
12use bumpalo::{Bump, BumpAllocSafe};
3+use std::fmt;
4+use std::mem;
25
36/// A node is either a text node or an element.
47#[derive(Debug, Clone)]
@@ -22,17 +25,83 @@ pub struct TextNode<'a> {
2225#[derive(Debug, Clone)]
2326pub struct ElementNode<'a> {
2427 pub(crate) tag_name: &'a str,
28+ pub(crate) listeners: &'a [Listener<'a>],
2529 pub(crate) attributes: &'a [Attribute<'a>],
2630 pub(crate) children: &'a [Node<'a>],
2731}
2832
33+/// An event listener callback function.
34+///
35+/// It takes three parameters:
36+///
37+/// 1. The virtual DOM's root rendering component.
38+/// 2. A capability to scheduler virtual DOM re-rendering.
39+/// 3. The event that ocurred.
40+pub type ListenerCallback<'a> =
41+ &'a (dyn Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static);
42+
43+/// An event listener.
44+pub struct Listener<'a> {
45+ /// The type of event to listen for.
46+ pub event: &'a str,
47+ /// The callback to invoke when the event happens.
48+ pub callback: ListenerCallback<'a>,
49+}
50+
2951#[derive(Clone, Debug)]
3052pub struct Attribute<'a> {
3153 pub name: &'a str,
3254 pub value: &'a str,
3355}
3456
57+impl<'a> BumpAllocSafe for Node<'a> {}
58+impl<'a> BumpAllocSafe for Listener<'a> {}
59+impl<'a> BumpAllocSafe for Attribute<'a> {}
60+
61+impl fmt::Debug for Listener<'_> {
62+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63+ let (a, b) = self.get_callback_parts();
64+ let a = a as *mut u32;
65+ let b = b as *mut u32;
66+ f.debug_struct("Listener")
67+ .field("event", &self.event)
68+ .field("callback", &(a, b))
69+ .finish()
70+ }
71+}
72+
3573impl<'a> Node<'a> {
74+ /// Construct a new element node with the given tag name and children.
75+ #[inline]
76+ pub fn element<Listeners, Attributes, Children>(
77+ bump: &'a Bump,
78+ tag_name: &'a str,
79+ listeners: Listeners,
80+ attributes: Attributes,
81+ children: Children,
82+ ) -> Node<'a>
83+ where
84+ Listeners: 'a + BumpAllocSafe + AsRef<[Listener<'a>]>,
85+ Attributes: 'a + BumpAllocSafe + AsRef<[Attribute<'a>]>,
86+ Children: 'a + BumpAllocSafe + AsRef<[Node<'a>]>,
87+ {
88+ let children: &'a Children = bump.alloc(children);
89+ let children: &'a [Node<'a>] = children.as_ref();
90+
91+ let listeners: &'a Listeners = bump.alloc(listeners);
92+ let listeners: &'a [Listener<'a>] = listeners.as_ref();
93+
94+ let attributes: &'a Attributes = bump.alloc(attributes);
95+ let attributes: &'a [Attribute<'a>] = attributes.as_ref();
96+
97+ Node::Element(ElementNode {
98+ tag_name,
99+ listeners,
100+ attributes,
101+ children,
102+ })
103+ }
104+
36105 /// Is this node a text node?
37106 pub fn is_text(&self) -> bool {
38107 match self {
@@ -48,6 +117,12 @@ impl<'a> Node<'a> {
48117 _ => false,
49118 }
50119 }
120+
121+ /// Construct a new text node with the given text.
122+ #[inline]
123+ pub fn text(text: &'a str) -> Node<'a> {
124+ Node::Text(TextNode { text })
125+ }
51126}
52127
53128impl<'a> TextNode<'a> {
@@ -74,38 +149,42 @@ impl<'a> ElementNode<'a> {
74149 }
75150}
76151
77−impl<'a> BumpAllocSafe for Node<'a> {}
78−impl<'a> BumpAllocSafe for Attribute<'a> {}
152+union CallbackFatPtr<'a> {
153+ callback: ListenerCallback<'a>,
154+ parts: (u32, u32),
155+}
79156
80−impl<'a> Node<'a> {
81− /// Construct a new text node with the given text.
157+impl Listener<'_> {
82158 #[inline]
83− pub fn text(text: &'a str) -> Node<'a> {
84− Node::Text(TextNode { text })
159+ pub(crate) fn get_callback_parts(&self) -> (u32, u32) {
160+ assert_eq!(
161+ mem::size_of::<ListenerCallback>(),
162+ mem::size_of::<CallbackFatPtr>()
163+ );
164+
165+ unsafe {
166+ let fat = CallbackFatPtr {
167+ callback: self.callback,
168+ };
169+ let (a, b) = fat.parts;
170+ debug_assert!(a != 0);
171+ (a, b)
172+ }
85173 }
174+}
86175
87− /// Construct a new element node with the given tag name and children.
88− #[inline]
89− pub fn element<Attributes, Children>(
90− bump: &'a Bump,
91− tag_name: &'a str,
92− attributes: Attributes,
93− children: Children,
94− ) -> Node<'a>
95− where
96− Attributes: 'a + BumpAllocSafe + AsRef<[Attribute<'a>]>,
97− Children: 'a + BumpAllocSafe + AsRef<[Node<'a>]>,
98− {
99− let children: &'a Children = bump.alloc(children);
100− let children: &'a [Node<'a>] = children.as_ref();
101−
102− let attributes: &'a Attributes = bump.alloc(attributes);
103− let attributes: &'a [Attribute<'a>] = attributes.as_ref();
104−
105− Node::Element(ElementNode {
106− tag_name,
107− attributes,
108− children,
109− })
176+/// Utility function for creating event listeners and downcasting the component
177+/// to its `RootRender` concrete type.
178+pub fn on<'a, F>(bump: &'a Bump, event: &'a str, callback: F) -> Listener<'a>
179+where
180+ F: Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static,
181+{
182+ Listener {
183+ event,
184+ callback: bump.alloc(
185+ move |component: &mut dyn RootRender, vdom: VdomWeak, event: web_sys::Event| {
186+ callback(component, vdom, event);
187+ },
188+ ),
110189 }
111190}
src/vdom.rs+417−69
@@ -1,16 +1,63 @@
11use super::change_list::ChangeList;
2−use super::node::{Attribute, ElementNode, Node, TextNode};
3−use super::Render;
2+use super::node::{Attribute, ElementNode, Listener, Node, TextNode};
3+use super::RootRender;
4+use crate::events::EventsRegistry;
45use bumpalo::Bump;
6+use futures::future::Future;
57use log::*;
8+use std::cell::Cell;
9+use std::cell::RefCell;
610use std::cmp;
11+use std::fmt;
712use std::mem;
13+use std::mem::ManuallyDrop;
14+use std::rc::{Rc, Weak};
15+use wasm_bindgen::prelude::*;
16+use wasm_bindgen::JsCast;
17+use wasm_bindgen_futures::JsFuture;
18+
19+/// A strong handle to a mounted virtual DOM.
20+///
21+/// When this handle is dropped, the virtual DOM is unmounted and its listeners
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 \
24+ dropped. If you want a `Vdom` to run forever, call `Vdom::forget`."]
25+pub struct Vdom {
26+ inner: Rc<VdomInner>,
27+}
28+
29+/// A weak handle to a virtual DOM.
30+///
31+/// Does not prevent the virtual DOM from being unmounted: only keeping the
32+/// original `Vdom` alive guarantees that.
33+///
34+/// A `VdomWeak` also gives you the capability to scheduling re-rendering (say
35+/// after mutating the render component state).
36+#[derive(Clone, Debug)]
37+pub struct VdomWeak {
38+ inner: Weak<VdomInner>,
39+}
40+
41+#[derive(Debug)]
42+pub(crate) struct VdomInner {
43+ pub(crate) shared: VdomInnerShared,
44+ pub(crate) exclusive: RefCell<VdomInnerExclusive>,
45+}
46+
47+pub(crate) struct VdomInnerShared {
48+ pub(crate) render_scheduled: Cell<Option<js_sys::Promise>>,
49+}
50+
51+pub(crate) struct VdomInnerExclusive {
52+ // Always `Some` except just before we drop. Just an option so that
53+ // `unmount` can take the component out but we can still have a Drop
54+ // implementation.
55+ component: Option<Box<RootRender>>,
856
9−pub struct Vdom<R> {
10− component: R,
1157 dom_buffers: [Bump; 2],
12− change_list: ChangeList,
58+ change_list: ManuallyDrop<ChangeList>,
1359 container: web_sys::Element,
60+ events_registry: Option<Rc<RefCell<EventsRegistry>>>,
1461
1562 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
1663 // caching renders, into `self.component`'s bump.
@@ -21,19 +68,77 @@ unsafe fn extend_node_lifetime<'a>(node: Node<'a>) -> Node<'static> {
2168 mem::transmute(node)
2269}
2370
24−impl<R> Vdom<R>
25−where
26− R: Render,
27−{
28− pub fn new(container: &web_sys::Element, component: R) -> Vdom<R> {
71+impl fmt::Debug for VdomInnerShared {
72+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73+ let render_scheduled = Cell::new(None);
74+ self.render_scheduled.swap(&render_scheduled);
75+ let render_scheduled = render_scheduled.into_inner();
76+ let r = f
77+ .debug_struct("VdomInnerShared")
78+ .field("render_scheduled", &render_scheduled)
79+ .finish();
80+ self.render_scheduled.set(render_scheduled);
81+ r
82+ }
83+}
84+
85+impl fmt::Debug for VdomInnerExclusive {
86+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87+ f.debug_struct("VdomInnerExclusive")
88+ .field("component", &"..")
89+ .field("dom_buffers", &self.dom_buffers)
90+ .field("change_list", &self.change_list)
91+ .field("container", &self.container)
92+ .field("events_registry", &self.events_registry)
93+ .field("current_root", &self.current_root)
94+ .finish()
95+ }
96+}
97+
98+impl Drop for VdomInnerExclusive {
99+ fn drop(&mut self) {
100+ debug!("Dropping VdomInnerExclusive");
101+
102+ // Make sure that we clean up our JS listeners and all that before we
103+ // empty the container.
104+ unsafe {
105+ ManuallyDrop::drop(&mut self.change_list);
106+ }
107+
108+ let registry = self.events_registry.take().unwrap_throw();
109+ let mut registry = registry.borrow_mut();
110+ registry.clear_active_listeners();
111+
112+ self.container.set_inner_html("");
113+ }
114+}
115+
116+impl Vdom {
117+ /// Mount a new `Vdom` in the given container element with the given root
118+ /// rendering component.
119+ ///
120+ /// This will box the given component into trait object.
121+ pub fn new<R>(container: &web_sys::Element, component: R) -> Vdom
122+ where
123+ R: RootRender,
124+ {
125+ Self::with_boxed_root_render(container, Box::new(component) as Box<RootRender>)
126+ }
127+
128+ /// Construct a `Vdom` with the already-boxed-as-a-trait-object root
129+ /// rendering component.
130+ pub fn with_boxed_root_render(
131+ container: &web_sys::Element,
132+ component: Box<RootRender>,
133+ ) -> Vdom {
29134 let dom_buffers = [Bump::new(), Bump::new()];
30− let change_list = ChangeList::new(container);
135+ let change_list = ManuallyDrop::new(ChangeList::new(container));
31136
32137 // Ensure that the container is empty.
33138 container.set_inner_html("");
34139
35140 // Create a dummy `<div/>` in our container.
36− let current_root = Node::element(&dom_buffers[0], "div", [], []);
141+ let current_root = Node::element(&dom_buffers[0], "div", [], [], []);
37142 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
38143 let window = web_sys::window().expect("should have acess to the Window");
39144 let document = window
@@ -48,76 +153,114 @@ where
48153 )
49154 .expect("should append child OK");
50155
51− // Diff and apply the `contents` against our dummy `<div/>`.
52156 let container = container.clone();
53− let mut vdom = Vdom {
54− component,
55− dom_buffers,
56− change_list,
57− container,
58− current_root,
59− };
60− vdom.render();
61− vdom
157+ let inner = Rc::new(VdomInner {
158+ shared: VdomInnerShared {
159+ render_scheduled: Cell::new(None),
160+ },
161+ exclusive: RefCell::new(VdomInnerExclusive {
162+ component: Some(component),
163+ dom_buffers,
164+ change_list,
165+ container,
166+ current_root,
167+ events_registry: None,
168+ }),
169+ });
170+
171+ let (events_registry, events_trampoline) = EventsRegistry::new(Rc::downgrade(&inner));
172+
173+ {
174+ let mut inner = inner.exclusive.borrow_mut();
175+ inner.events_registry = Some(events_registry);
176+ inner.change_list.init_events_trampoline(events_trampoline);
177+
178+ // Diff and apply the `contents` against our dummy `<div/>`.
179+ inner.render();
180+ }
181+
182+ Vdom { inner }
62183 }
63184
64− /// Get a shared reference to the underlying render component.
185+ /// Run this virtual DOM and its listeners forever and never unmount it.
65186 #[inline]
66− pub fn component(&self) -> &R {
67− &self.component
187+ pub fn forget(self) {
188+ mem::forget(self);
68189 }
69190
70− /// Get an exclusive reference to the underlying render component.
191+ /// Get a weak handle to this virtual DOM.
71192 #[inline]
72− pub fn component_mut(&mut self) -> &mut R {
73− &mut self.component
193+ pub fn weak(&self) -> VdomWeak {
194+ VdomWeak::new(&self.inner)
74195 }
75196
76− /// Take the render component out of this virtual DOM.
197+ /// Unmount this virtual DOM, unregister its event listeners, and return its
198+ /// root render component.
77199 #[inline]
78− pub fn into_component(self) -> R {
79− self.component
200+ pub fn unmount(self) -> Box<RootRender> {
201+ Rc::try_unwrap(self.inner.clone())
202+ .map_err(|_| ())
203+ .unwrap_throw()
204+ .exclusive
205+ .into_inner()
206+ .component
207+ .take()
208+ .unwrap_throw()
80209 }
210+}
81211
82− /// Get a reference to the physical DOM node that reflects this virtual DOM
83− /// tree.
84− pub fn container(&self) -> &web_sys::Element {
85− &self.container
212+impl VdomInnerExclusive {
213+ /// Get an exclusive reference to the underlying render component as a raw
214+ /// trait object.
215+ #[inline]
216+ pub(crate) fn component_raw_mut(&mut self) -> &mut dyn RootRender {
217+ &mut **self.component.as_mut().unwrap_throw()
86218 }
87219
88220 /// Re-render this virtual dom's current component.
89− pub fn render(&mut self) {
221+ pub(crate) fn render(&mut self) {
90222 unsafe {
91− self.dom_buffers[1].reset();
92− let new_contents = self.component.render(&self.dom_buffers[1]);
93− let new_contents = extend_node_lifetime(new_contents);
223+ let events_registry = self.events_registry.take().unwrap();
224+
225+ {
226+ // All the old listeners are no longer active. We will build a new
227+ // set of active listeners when diffing.
228+ //
229+ // NB: if we end up avoiding diffing cached renders (instead of just
230+ // avoiding re-rendering them) then we will need to maintain cached
231+ // active listeners, and can't just clear all active listeners and
232+ // rebuild them here.
233+ let mut registry = events_registry.borrow_mut();
234+ registry.clear_active_listeners();
235+
236+ // Reset the inactive bump arena's pointer.
237+ self.dom_buffers[1].reset();
94238
95− let old_contents = self.current_root.take().unwrap();
96− self.diff(old_contents, new_contents.clone());
239+ // Render the new current contents into the inactive bump arena.
240+ let new_contents = self
241+ .component
242+ .as_ref()
243+ .unwrap_throw()
244+ .render(&self.dom_buffers[1]);
245+ let new_contents = extend_node_lifetime(new_contents);
246+
247+ // Diff the old contents with the new contents.
248+ let old_contents = self.current_root.take().unwrap();
249+ self.diff(&mut registry, old_contents, new_contents.clone());
250+
251+ // Swap the buffers to make the bump arena with the new contents the
252+ // active arena, and the old one into the inactive arena.
253+ self.swap_buffers();
254+ self.set_current_root(new_contents);
255+ }
97256
98− self.swap_buffers();
99− self.set_current_root(new_contents);
257+ self.events_registry = Some(events_registry);
100258
259+ // Tell JS to apply our diff-generated changes to the physical DOM!
101260 self.change_list.apply_changes();
102261 }
103262 }
104263
105− /// Render a new component.
106− pub fn render_component<S>(self, component: S) -> Vdom<S>
107− where
108− S: Render,
109− {
110− let mut vdom = Vdom {
111− component,
112− dom_buffers: self.dom_buffers,
113− change_list: self.change_list,
114− container: self.container,
115− current_root: self.current_root,
116− };
117− vdom.render();
118− vdom
119− }
120−
121264 fn swap_buffers(&mut self) {
122265 let (first, second) = self.dom_buffers.as_mut().split_at_mut(1);
123266 mem::swap(&mut first[0], &mut second[0]);
@@ -128,7 +271,7 @@ where
128271 self.current_root = Some(current);
129272 }
130273
131− fn diff(&self, old: Node, new: Node) {
274+ fn diff<'a>(&self, registry: &mut EventsRegistry, old: Node<'a>, new: Node<'a>) {
132275 // debug!("---------------------------------------------------------");
133276 // debug!("dodrio::Vdom::diff");
134277 // debug!(" old = {:#?}", old);
@@ -143,22 +286,24 @@ where
143286 }
144287 (&Node::Text(TextNode { .. }), Node::Element(ElementNode { .. })) => {
145288 debug!(" replacing a text node with an element");
146− self.create(new);
289+ self.create(registry, new);
147290 self.change_list.emit_replace_with();
148291 }
149292 (&Node::Element(ElementNode { .. }), Node::Text(TextNode { .. })) => {
150293 debug!(" replacing an element with a text node");
151− self.create(new);
294+ self.create(registry, new);
152295 self.change_list.emit_replace_with();
153296 }
154297 (
155298 &Node::Element(ElementNode {
156299 tag_name: new_tag_name,
300+ listeners: new_listeners,
157301 attributes: new_attributes,
158302 children: new_children,
159303 }),
160304 Node::Element(ElementNode {
161305 tag_name: old_tag_name,
306+ listeners: old_listeners,
162307 attributes: old_attributes,
163308 children: old_children,
164309 }),
@@ -166,16 +311,48 @@ where
166311 debug!(" updating an element");
167312 if new_tag_name != old_tag_name {
168313 debug!(" different tag names; creating new element and replacing old element");
169− self.create(new);
314+ self.create(registry, new);
170315 self.change_list.emit_replace_with();
171316 return;
172317 }
318+ self.diff_listeners(registry, old_listeners, new_listeners);
173319 self.diff_attributes(old_attributes, new_attributes);
174− self.diff_children(old_children, new_children);
320+ self.diff_children(registry, old_children, new_children);
175321 }
176322 }
177323 }
178324
325+ fn diff_listeners<'a>(
326+ &self,
327+ registry: &mut EventsRegistry,
328+ old: &'a [Listener<'a>],
329+ new: &'a [Listener<'a>],
330+ ) {
331+ debug!(" updating event listeners");
332+
333+ 'outer1: for new_l in new {
334+ unsafe {
335+ registry.add(new_l);
336+ }
337+ for old_l in old {
338+ if new_l.event == old_l.event {
339+ self.change_list.emit_update_event_listener(new_l);
340+ continue 'outer1;
341+ }
342+ }
343+ self.change_list.emit_new_event_listener(new_l);
344+ }
345+
346+ 'outer2: for old_l in old {
347+ for new_l in new {
348+ if new_l.event == old_l.event {
349+ continue 'outer2;
350+ }
351+ }
352+ self.change_list.emit_remove_event_listener(old_l.event);
353+ }
354+ }
355+
179356 fn diff_attributes(&self, old: &[Attribute], new: &[Attribute]) {
180357 debug!(" updating attributes");
181358
@@ -205,7 +382,12 @@ where
205382 }
206383 }
207384
208− fn diff_children(&self, old: &[Node], new: &[Node]) {
385+ fn diff_children<'a>(
386+ &self,
387+ registry: &mut EventsRegistry,
388+ old: &'a [Node<'a>],
389+ new: &'a [Node<'a>],
390+ ) {
209391 debug!(" updating children shared by old and new");
210392
211393 let num_children_to_diff = cmp::min(new.len(), old.len());
@@ -227,7 +409,7 @@ where
227409 self.change_list.emit_pop_push_next_sibling();
228410 }
229411
230− self.diff(old_child.clone(), new_child.clone());
412+ self.diff(registry, old_child.clone(), new_child.clone());
231413 }
232414
233415 if old_children.next().is_some() {
@@ -247,7 +429,7 @@ where
247429 self.change_list.emit_pop();
248430 pushed = false;
249431 }
250− self.create(new_child.clone());
432+ self.create(registry, new_child.clone());
251433 self.change_list.emit_append_child();
252434 }
253435 }
@@ -258,25 +440,191 @@ where
258440 }
259441 }
260442
261− fn create(&self, node: Node) {
443+ fn create<'a>(&self, registry: &mut EventsRegistry, node: Node<'a>) {
262444 match node {
263445 Node::Text(TextNode { text }) => {
264446 self.change_list.emit_create_text_node(text);
265447 }
266448 Node::Element(ElementNode {
267449 tag_name,
450+ listeners,
268451 attributes,
269452 children,
270453 }) => {
271454 self.change_list.emit_create_element(tag_name);
455+ for l in listeners {
456+ unsafe {
457+ registry.add(l);
458+ }
459+ self.change_list.emit_new_event_listener(l);
460+ }
272461 for attr in attributes {
273462 self.change_list.emit_set_attribute(&attr.name, &attr.value);
274463 }
275464 for child in children {
276− self.create(child.clone());
465+ self.create(registry, child.clone());
277466 self.change_list.emit_append_child();
278467 }
279468 }
280469 }
281470 }
282471}
472+
473+fn request_animation_frame(f: &Closure<FnMut()>) {
474+ web_sys::window()
475+ .expect_throw("should have a window")
476+ .request_animation_frame(f.as_ref().unchecked_ref())
477+ .expect_throw("should register `requestAnimationFrame` OK");
478+}
479+
480+fn with_animation_frame<F>(mut f: F)
481+where
482+ F: 'static + FnMut(),
483+{
484+ let g = Rc::new(RefCell::new(None));
485+ let h = g.clone();
486+
487+ let f = Closure::wrap(Box::new(move || {
488+ *g.borrow_mut() = None;
489+ f();
490+ }) as Box<FnMut()>);
491+ request_animation_frame(&f);
492+
493+ *h.borrow_mut() = Some(f);
494+}
495+
496+trait CellOptionExt<T> {
497+ fn take(&self) -> Option<T>;
498+}
499+
500+impl<T> CellOptionExt<T> for Cell<Option<T>> {
501+ fn take(&self) -> Option<T> {
502+ let temp = Cell::new(None);
503+ self.swap(&temp);
504+ temp.into_inner()
505+ }
506+}
507+
508+/// An operation failed because the virtual DOM was already dropped and
509+/// unmounted.
510+#[derive(Debug)]
511+pub struct VdomDroppedError {}
512+
513+impl fmt::Display for VdomDroppedError {
514+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
515+ write!(f, "The virtual DOM was dropped.")
516+ }
517+}
518+
519+impl std::error::Error for VdomDroppedError {}
520+
521+impl VdomWeak {
522+ /// Construct a new weak handle to the given virtual DOM.
523+ #[inline]
524+ pub(crate) fn new(inner: &Rc<VdomInner>) -> VdomWeak {
525+ VdomWeak {
526+ inner: Rc::downgrade(inner),
527+ }
528+ }
529+
530+ /// TODO
531+ pub fn set_component(
532+ self,
533+ root: Box<dyn RootRender>,
534+ ) -> impl Future<Item = Box<dyn RootRender + 'static>, Error = VdomDroppedError> {
535+ futures::future::ok(self.inner.upgrade())
536+ .and_then(|inner| inner.ok_or(()))
537+ .map_err(|_| VdomDroppedError {})
538+ .and_then(|inner| {
539+ let promise = js_sys::Promise::resolve(&JsValue::null());
540+ JsFuture::from(promise)
541+ .map_err(|_| VdomDroppedError {})
542+ .and_then(move |_| {
543+ let old = {
544+ let mut exclusive = inner.exclusive.borrow_mut();
545+ mem::replace(&mut *exclusive.component.as_mut().unwrap_throw(), root)
546+ };
547+ VdomWeak::new(&inner).render().map(|_| old)
548+ })
549+ })
550+ }
551+
552+ /// Execute `f` with a reference to this virtual DOM's root rendering
553+ /// component.
554+ ///
555+ /// To ensure exclusive access to the root rendering component, the
556+ /// invocation takes place on a new tick of the micro-task queue.
557+ pub fn with_component<F, T>(&self, f: F) -> impl Future<Item = T, Error = VdomDroppedError>
558+ where
559+ F: 'static + FnOnce(&mut dyn RootRender) -> T,
560+ {
561+ futures::future::ok(self.inner.upgrade())
562+ .and_then(|inner| inner.ok_or(()))
563+ .map_err(|_| VdomDroppedError {})
564+ .and_then(|inner| {
565+ let mut f = Some(f);
566+ let promise = js_sys::Promise::resolve(&JsValue::null());
567+ JsFuture::from(promise)
568+ .map_err(|_| VdomDroppedError {})
569+ .map(move |_| {
570+ let f = f.take().unwrap_throw();
571+ let mut exclusive = inner.exclusive.borrow_mut();
572+ f(exclusive.component_raw_mut())
573+ })
574+ })
575+ }
576+
577+ /// Schedule a render to occur during the next animation frame.
578+ ///
579+ /// If you want a future that resolves after the render has finished, use
580+ /// `render` instead.
581+ pub fn schedule_render(&self) {
582+ debug!("VdomWeak::schedule_render");
583+ wasm_bindgen_futures::spawn_local(self.render().map_err(|_| ()));
584+ }
585+
586+ /// Schedule a render to occur during the next animation frame and return a
587+ /// future that will complete once the render has finished.
588+ ///
589+ /// If you don't want to do more things after the render completes, then use
590+ /// `schedule_render` instead of `render`.
591+ pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
592+ debug!("VdomWeak::render: initiating render in new animation frame");
593+ futures::future::ok(self.inner.upgrade())
594+ .and_then(|inner| inner.ok_or(()))
595+ .map_err(|_| VdomDroppedError {})
596+ .and_then(|inner| {
597+ let promise = inner.shared.render_scheduled.take().unwrap_or_else(|| {
598+ js_sys::Promise::new(&mut |resolve, reject| {
599+ let vdom = VdomWeak {
600+ inner: Rc::downgrade(&inner),
601+ };
602+ with_animation_frame(move || match vdom.inner.upgrade() {
603+ None => {
604+ warn!("VdomWeak::render: vdom unmounted before we could render");
605+ let r = reject.call0(&JsValue::null());
606+ debug_assert!(r.is_ok());
607+ }
608+ Some(inner) => {
609+ let mut exclusive = inner.exclusive.borrow_mut();
610+ exclusive.render();
611+
612+ // We did the render, so take the promise away
613+ // and let future `render` calls request new
614+ // animation frames.
615+ let _ = inner.shared.render_scheduled.take();
616+
617+ debug!("VdomWeak::render: finished rendering");
618+ let r = resolve.call0(&JsValue::null());
619+ debug_assert!(r.is_ok());
620+ }
621+ });
622+ })
623+ });
624+ inner.shared.render_scheduled.set(Some(promise.clone()));
625+ JsFuture::from(promise)
626+ .map(|_| ())
627+ .map_err(|_| VdomDroppedError {})
628+ })
629+ }
630+}
tests/web/cached.rs+65−17
@@ -1,7 +1,9 @@
11use super::{assert_rendered, create_element, RenderFn};
2−use bumpalo::Bump;
3−use dodrio::{Cached, Node, Render, Vdom};
2+use dodrio::{bumpalo::Bump, Cached, Node, Render, Vdom};
3+use futures::prelude::*;
44use std::cell::Cell;
5+use std::rc::Rc;
6+use wasm_bindgen::*;
57use wasm_bindgen_test::*;
68
79pub struct CountRenders {
@@ -29,23 +31,69 @@ impl Render for CountRenders {
2931 }
3032}
3133
32−#[wasm_bindgen_test]
33−fn uses_cached_render() {
34− let container = create_element("div");
34+#[wasm_bindgen_test(async)]
35+fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
3536 let cached = Cached::new(CountRenders::new());
36− let mut vdom = Vdom::new(&container, cached);
3737
38− for _ in 0..10 {
39− vdom.render();
40− assert_eq!(vdom.component().render_count.get(), 1);
41− assert_rendered(&container, &RenderFn(|_| Node::text("1")))
42− }
38+ let container0 = create_element("div");
39+ let container1 = container0.clone();
40+ let container2 = container0.clone();
41+ let container3 = container0.clone();
42+ let container4 = container0.clone();
4343
44− Cached::invalidate(vdom.component_mut());
44+ let vdom0 = Rc::new(Vdom::new(&container0, cached));
45+ let vdom1 = vdom0.clone();
46+ let vdom2 = vdom0.clone();
47+ let vdom3 = vdom0.clone();
48+ let vdom4 = vdom0.clone();
49+ let vdom5 = vdom0.clone();
50+ let vdom6 = vdom0.clone();
51+ let vdom7 = vdom0.clone();
4552
46− for _ in 0..10 {
47− vdom.render();
48− assert_eq!(vdom.component().render_count.get(), 2);
49− assert_rendered(&container, &RenderFn(|_| Node::text("2")))
50− }
53+ vdom0
54+ .weak()
55+ // We render, populate the cache, and get "1".
56+ .render()
57+ .and_then(move |_| {
58+ vdom0.weak().with_component(move |comp| {
59+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
60+ assert_eq!(comp.render_count.get(), 1);
61+ assert_rendered(&container1, &RenderFn(|_| Node::text("1")));
62+ })
63+ })
64+ // We re-render, re-use the cached node, and get "1" again.
65+ .and_then(move |_| vdom1.weak().render())
66+ .and_then(move |_| {
67+ vdom2.weak().with_component(move |comp| {
68+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
69+ assert_eq!(comp.render_count.get(), 1);
70+ assert_rendered(&container2, &RenderFn(|_| Node::text("1")));
71+ })
72+ })
73+ // We invalidate the cache, re-render, re-populate the cache, and should
74+ // now have "2".
75+ .and_then(move |_| {
76+ vdom3.weak().with_component(move |comp| {
77+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
78+ Cached::invalidate(comp);
79+ })
80+ })
81+ .and_then(move |_| vdom4.weak().render())
82+ .and_then(move |_| {
83+ vdom5.weak().with_component(move |comp| {
84+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
85+ assert_eq!(comp.render_count.get(), 2);
86+ assert_rendered(&container3, &RenderFn(|_| Node::text("2")));
87+ })
88+ })
89+ // We re-render, re-use the cached node, and get "2" again.
90+ .and_then(move |_| vdom6.weak().render())
91+ .and_then(move |_| {
92+ vdom7.weak().with_component(move |comp| {
93+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
94+ assert_eq!(comp.render_count.get(), 2);
95+ assert_rendered(&container4, &RenderFn(|_| Node::text("2")));
96+ })
97+ })
98+ .map_err(|e| JsValue::from(e.to_string()))
5199}
tests/web/events.rsnew file+371
@@ -0,0 +1,371 @@
1+use super::create_element;
2+use bumpalo::Bump;
3+use dodrio::{on, Attribute, Node, Render, Vdom};
4+use futures::Future;
5+use std::cell::{Cell, RefCell};
6+use std::rc::Rc;
7+use wasm_bindgen::prelude::*;
8+use wasm_bindgen::JsCast;
9+use wasm_bindgen_test::*;
10+
11+struct EventContainer {
12+ event: &'static str,
13+ on_event: Box<FnMut()>,
14+}
15+
16+impl EventContainer {
17+ fn new<F>(event: &'static str, on_event: F) -> EventContainer
18+ where
19+ F: 'static + FnMut(),
20+ {
21+ EventContainer {
22+ event,
23+ on_event: Box::new(on_event),
24+ }
25+ }
26+}
27+
28+impl Render for EventContainer {
29+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
30+ where
31+ 'a: 'bump,
32+ {
33+ Node::element(
34+ bump,
35+ "div",
36+ [on(bump, self.event, |root, _scheduler, _event| {
37+ (root.unwrap_mut::<EventContainer>().on_event)();
38+ })],
39+ [Attribute {
40+ name: "id",
41+ value: "target",
42+ }],
43+ [],
44+ )
45+ }
46+}
47+
48+fn target(container: &web_sys::Element) -> web_sys::HtmlElement {
49+ container
50+ .query_selector("#target")
51+ .expect_throw("should querySelector OK")
52+ .expect_throw("should find `#target` in container")
53+ .unchecked_into()
54+}
55+
56+#[wasm_bindgen_test(async)]
57+fn click() -> impl Future<Item = (), Error = JsValue> {
58+ let container = create_element("div");
59+
60+ let (sender, receiver) = futures::sync::oneshot::channel();
61+ let mut sender = Some(sender);
62+
63+ let vdom = Vdom::new(
64+ &container,
65+ EventContainer::new("click", move || {
66+ sender
67+ .take()
68+ .expect_throw("should only call listener once")
69+ .send(())
70+ .expect_throw("should not have dropped the receiver");
71+ }),
72+ );
73+
74+ target(&container).click();
75+
76+ receiver
77+ .map(|_| {
78+ drop(vdom);
79+ })
80+ .map_err(|e| JsValue::from(e.to_string()))
81+}
82+
83+#[wasm_bindgen_test(async)]
84+fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
85+ let container = create_element("div");
86+
87+ let (first_sender, first_receiver) = futures::sync::oneshot::channel();
88+ let mut first_sender = Some(first_sender);
89+
90+ let vdom = Vdom::new(
91+ &container,
92+ EventContainer::new("click", move || {
93+ first_sender
94+ .take()
95+ .expect_throw("should only call listener once")
96+ .send("first")
97+ .expect_throw("should not have dropped the receiver");
98+ }),
99+ );
100+
101+ let (second_sender, second_receiver) = futures::sync::oneshot::channel();
102+ let mut second_sender = Some(second_sender);
103+
104+ vdom.weak()
105+ .set_component(Box::new(EventContainer::new("click", move || {
106+ second_sender
107+ .take()
108+ .expect_throw("should only call listener once")
109+ .send("second")
110+ .expect_throw("should not have dropped the receiver");
111+ })))
112+ .map_err(|e| JsValue::from(e.to_string()))
113+ .and_then(move |_| {
114+ target(&container).click();
115+
116+ first_receiver
117+ .select(second_receiver)
118+ .map(|(which, _other)| which)
119+ .or_else(|(_error, other)| other)
120+ .map(|which| {
121+ assert_eq!(which, "second");
122+ drop(vdom);
123+ })
124+ .map_err(|e| JsValue::from(e.to_string()))
125+ })
126+}
127+
128+struct ListensOnlyOnFirstRender {
129+ count: Cell<usize>,
130+ callback: Box<FnMut()>,
131+}
132+
133+impl ListensOnlyOnFirstRender {
134+ fn new<F>(callback: F) -> ListensOnlyOnFirstRender
135+ where
136+ F: 'static + FnMut(),
137+ {
138+ ListensOnlyOnFirstRender {
139+ count: Cell::new(0),
140+ callback: Box::new(callback),
141+ }
142+ }
143+}
144+
145+impl Render for ListensOnlyOnFirstRender {
146+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
147+ where
148+ 'a: 'bump,
149+ {
150+ let count = self.count.get();
151+ self.count.set(count + 1);
152+ if count == 0 {
153+ Node::element(
154+ bump,
155+ "div",
156+ [on(bump, "click", |root, _scheduler, _event| {
157+ (root.unwrap_mut::<ListensOnlyOnFirstRender>().callback)();
158+ })],
159+ [Attribute {
160+ name: "id",
161+ value: "target",
162+ }],
163+ [],
164+ )
165+ } else {
166+ Node::element(
167+ bump,
168+ "div",
169+ [],
170+ [Attribute {
171+ name: "id",
172+ value: "target",
173+ }],
174+ [],
175+ )
176+ }
177+ }
178+}
179+
180+#[wasm_bindgen_test(async)]
181+fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
182+ let container = create_element("div");
183+
184+ let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
185+ let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
186+ let outer_listener = Closure::wrap(Box::new(move |_| {
187+ outer_sender
188+ .borrow_mut()
189+ .take()
190+ .expect_throw("should only invoke outer_listener once")
191+ .send("outer")
192+ .expect_throw("should not have dropped receiver");
193+ }) as Box<FnMut(web_sys::Event)>);
194+
195+ container
196+ .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
197+ .unwrap();
198+
199+ let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
200+ let mut vdom_sender = Some(vdom_sender);
201+
202+ // Render a vdom with a listener into container.
203+ let vdom = Vdom::new(
204+ &container,
205+ ListensOnlyOnFirstRender::new(move || {
206+ vdom_sender
207+ .take()
208+ .expect_throw("should not invoke vdom listener more than once")
209+ .send("inner")
210+ .expect_throw("should not have dropped the receiver");
211+ }),
212+ );
213+
214+ vdom.weak()
215+ // Re-render, so we aren't listening anymore.
216+ .render()
217+ .map_err(|e| JsValue::from(e.to_string()))
218+ .and_then(move |_| {
219+ target(&container).click();
220+
221+ // We should get our container's event handler fired, and not the unmounted
222+ // vdom's event handler.
223+ outer_receiver
224+ .select(vdom_receiver)
225+ .map(|(which, _other)| which)
226+ .or_else(|(_error, other)| other)
227+ .map(|which| {
228+ assert_eq!(which, "outer");
229+ drop(outer_listener);
230+ drop(vdom)
231+ })
232+ .map_err(|e| JsValue::from(e.to_string()))
233+ })
234+}
235+
236+#[wasm_bindgen_test(async)]
237+fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsValue> {
238+ let container = create_element("div");
239+
240+ let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
241+ let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
242+ let outer_listener = Closure::wrap(Box::new(move |_| {
243+ outer_sender
244+ .borrow_mut()
245+ .take()
246+ .expect_throw("should only invoke outer_listener once")
247+ .send("outer")
248+ .expect_throw("should not have dropped receiver");
249+ }) as Box<FnMut(web_sys::Event)>);
250+
251+ container
252+ .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
253+ .unwrap();
254+
255+ let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
256+ let mut vdom_sender = Some(vdom_sender);
257+
258+ // Render a vdom with a listener into container.
259+ let vdom = Vdom::new(
260+ &container,
261+ EventContainer::new("click", move || {
262+ vdom_sender
263+ .take()
264+ .expect_throw("should not invoke vdom listener more than once")
265+ .send("vdom")
266+ .expect_throw("should not have dropped the receiver");
267+ }),
268+ );
269+
270+ // Grab a reference to the target before dropping clears the DOM.
271+ let target = target(&container);
272+
273+ // Unmount the vdom.
274+ drop(vdom);
275+
276+ // The target is no longer a child of the container, so re-append it so that
277+ // the click will bubble.
278+ container.append_child(&target).unwrap();
279+
280+ // Send a click to the target.
281+ target.click();
282+
283+ // We should get our container's event handler fired, and not the unmounted
284+ // vdom's event handler.
285+ outer_receiver
286+ .select(vdom_receiver)
287+ .map(|(which, _other)| which)
288+ .or_else(|(_error, other)| other)
289+ .map(|which| {
290+ assert_eq!(which, "outer");
291+ drop(outer_listener);
292+ })
293+ .map_err(|e| JsValue::from(e.to_string()))
294+}
295+
296+struct CountRendersAndListen {
297+ count: Cell<usize>,
298+ callback: Box<FnMut(usize)>,
299+}
300+
301+impl CountRendersAndListen {
302+ fn new<F>(callback: F) -> CountRendersAndListen
303+ where
304+ F: 'static + FnMut(usize),
305+ {
306+ CountRendersAndListen {
307+ count: Cell::new(0),
308+ callback: Box::new(callback),
309+ }
310+ }
311+}
312+
313+impl Render for CountRendersAndListen {
314+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
315+ where
316+ 'a: 'bump,
317+ {
318+ let count = self.count.get() + 1;
319+ self.count.set(count);
320+ Node::element(
321+ bump,
322+ "button",
323+ [on(bump, "click", |comp, _vdom, _event| {
324+ let comp: &mut _ = comp.unwrap_mut::<dodrio::Cached<CountRendersAndListen>>();
325+ let comp: &mut CountRendersAndListen = comp;
326+ let n = comp.count.get();
327+ (comp.callback)(n);
328+ })],
329+ [Attribute {
330+ name: "id",
331+ value: "target",
332+ }],
333+ [],
334+ )
335+ }
336+}
337+
338+#[wasm_bindgen_test(async)]
339+fn cached_listeners_work_correctly() -> impl Future<Item = (), Error = JsValue> {
340+ let container = create_element("div");
341+
342+ let (sender, receiver) = futures::sync::oneshot::channel();
343+ let mut sender = Some(sender);
344+ let vdom = Vdom::new(
345+ &container,
346+ dodrio::Cached::new(CountRendersAndListen::new(move |n| {
347+ sender.take().unwrap().send(n).unwrap();
348+ })),
349+ );
350+
351+ let weak0 = vdom.weak();
352+ let weak1 = vdom.weak();
353+ let weak2 = vdom.weak();
354+ vdom.weak()
355+ // Re-render a few times, so we are using the cached version.
356+ .render()
357+ .and_then(move |_| weak0.render())
358+ .and_then(move |_| weak1.render())
359+ .and_then(move |_| weak2.render())
360+ .map_err(|e| JsValue::from(e.to_string()))
361+ .and_then(move |_| {
362+ target(&container).click();
363+
364+ receiver
365+ .map(|n| {
366+ assert_eq!(n, 1);
367+ drop(vdom);
368+ })
369+ .map_err(|e| JsValue::from(e.to_string()))
370+ })
371+}
tests/web/main.rs+25−10
@@ -4,13 +4,17 @@
44
55use bumpalo::Bump;
66use dodrio::{Attribute, Node, Render, Vdom};
7+use futures::prelude::*;
78use log::*;
9+use std::rc::Rc;
10+use wasm_bindgen::prelude::*;
811use wasm_bindgen::JsCast;
912use wasm_bindgen_test::*;
1013
1114wasm_bindgen_test_configure!(run_in_browser);
1215
1316pub mod cached;
17+pub mod events;
1418pub mod render;
1519
1620pub fn window() -> web_sys::Window {
@@ -24,6 +28,7 @@ pub fn document() -> web_sys::Document {
2428}
2529
2630pub fn create_element(tag: &str) -> web_sys::Element {
31+ init_logging();
2732 document()
2833 .create_element(tag)
2934 .expect("should create element OK")
@@ -142,22 +147,32 @@ where
142147/// Assert that if we start by rendering the `before` virtual DOM tree into a
143148/// physical DOM tree, and then diff it with the `after` virtual DOM tree, then
144149/// the physical DOM tree correctly matches `after`.
145−pub fn assert_before_after<R, S>(before: R, after: S)
150+pub fn assert_before_after<R, S>(before: R, after: S) -> impl Future<Item = (), Error = JsValue>
146151where
147− R: Render,
148− S: Render,
152+ R: 'static + Render,
153+ S: 'static + Render,
149154{
150155 let container = create_element("div");
151156
157+ let before = Rc::new(before);
158+ let after = Rc::new(after);
159+
152160 debug!("====== Rendering the *before* DOM into the physical DOM ======");
153− let vdom = Vdom::new(&container, &before);
161+ let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
162+ let vdom2 = vdom1.clone();
163+
154164 debug!("====== Checking the *before* DOM against the physical DOM ======");
155165 assert_rendered(&container, &before);
156166
157167 debug!("====== Rendering the *after* DOM into the physical DOM ======");
158− vdom.render_component(&after);
159− debug!("====== Checking the *after* DOM against the physical DOM ======");
160− assert_rendered(&container, &after);
168+ let weak = vdom1.weak();
169+ weak.set_component(Box::new(after.clone()))
170+ .map(move |_| {
171+ debug!("====== Checking the *after* DOM against the physical DOM ======");
172+ assert_rendered(&container, &after);
173+ drop(vdom2);
174+ })
175+ .map_err(|e| JsValue::from(e.to_string()))
161176}
162177
163178/// A helper macro for decalaring a bunch of `assert_before_after` tests.
@@ -174,13 +189,13 @@ macro_rules! before_after {
174189 }
175190 )* ) => {
176191 $(
177− #[wasm_bindgen_test]
178− fn $name() {
192+ #[wasm_bindgen_test(async)]
193+ fn $name() -> impl Future<Item = (), Error = wasm_bindgen::JsValue> {
179194 use crate::{assert_before_after, RenderFn};
180195 assert_before_after(
181196 RenderFn(|$before_bump| { $( $before )* }),
182197 RenderFn(|$after_bump| { $( $after )* })
183− );
198+ )
184199 }
185200 )*
186201 }
tests/web/render.rs+59−48
@@ -1,38 +1,49 @@
11use super::{assert_rendered, before_after, create_element, RenderFn};
22use dodrio::{Attribute, Node, Vdom};
3+use futures::prelude::*;
4+use std::rc::Rc;
35use wasm_bindgen_test::*;
46
57#[wasm_bindgen_test]
68fn render_initial_text() {
7− let hello = RenderFn(|_bump| Node::text("hello"));
9+ let hello = Rc::new(RenderFn(|_bump| Node::text("hello")));
810
911 let container = create_element("div");
10− Vdom::new(&container, &hello);
12+ let _vdom = Vdom::new(&container, hello.clone());
1113 assert_rendered(&container, &hello);
1214}
1315
1416#[wasm_bindgen_test]
1517fn render_initial_node() {
16− let hello = RenderFn(|bump| {
18+ let hello = Rc::new(RenderFn(|bump| {
1719 Node::element(
1820 bump,
1921 "div",
22+ [],
2023 [Attribute {
2124 name: "id",
2225 value: "hello-world",
2326 }],
2427 [
2528 Node::text("Hello "),
26− Node::element(bump, "span", [], [Node::text("World!")]),
29+ Node::element(bump, "span", [], [], [Node::text("World!")]),
2730 ],
2831 )
29− });
32+ }));
3033
3134 let container = create_element("div");
32− Vdom::new(&container, &hello);
35+ let _vdom = Vdom::new(&container, hello.clone());
3336 assert_rendered(&container, &hello);
3437}
3538
39+#[wasm_bindgen_test]
40+fn container_is_emptied_upon_drop() {
41+ let container = create_element("div");
42+ let vdom = Vdom::new(&container, RenderFn(|_bump| Node::text("blah")));
43+ drop(vdom);
44+ assert!(container.first_child().is_none());
45+}
46+
3647before_after! {
3748 same_text {
3849 before(_bump) {
@@ -57,13 +68,13 @@ before_after! {
5768 Node::text("before")
5869 }
5970 after(bump) {
60− Node::element(bump, "div", [], [])
71+ Node::element(bump, "div", [], [], [])
6172 }
6273 }
6374
6475 replace_elem_with_text {
6576 before(bump) {
66− Node::element(bump, "div", [], [])
77+ Node::element(bump, "div", [], [], [])
6778 }
6879 after(_bump) {
6980 Node::text("before")
@@ -72,52 +83,52 @@ before_after! {
7283
7384 same_elem {
7485 before(bump) {
75− Node::element(bump, "div", [], [])
86+ Node::element(bump, "div", [], [], [])
7687 }
7788 after(bump) {
78− Node::element(bump, "div", [], [])
89+ Node::element(bump, "div", [], [], [])
7990 }
8091 }
8192
8293 elems_with_different_tag_names {
8394 before(bump) {
84− Node::element(bump, "span", [], [])
95+ Node::element(bump, "span", [], [], [])
8596 }
8697 after(bump) {
87− Node::element(bump, "div", [], [])
98+ Node::element(bump, "div", [], [], [])
8899 }
89100 }
90101
91102 same_tag_name_update_attribute {
92103 before(bump) {
93− Node::element(bump, "div", [Attribute { name: "value", value: "1" }], [])
104+ Node::element(bump, "div", [], [Attribute { name: "value", value: "1" }], [])
94105 }
95106 after(bump) {
96− Node::element(bump, "div", [Attribute { name: "value", value: "2" }], [])
107+ Node::element(bump, "div", [], [Attribute { name: "value", value: "2" }], [])
97108 }
98109 }
99110
100111 same_tag_name_remove_attribute {
101112 before(bump) {
102− Node::element(bump, "div", [Attribute { name: "value", value: "1" }], [])
113+ Node::element(bump, "div", [], [Attribute { name: "value", value: "1" }], [])
103114 }
104115 after(bump) {
105− Node::element(bump, "div", [], [])
116+ Node::element(bump, "div", [], [], [])
106117 }
107118 }
108119
109120 same_tag_name_add_attribute {
110121 before(bump) {
111− Node::element(bump, "div", [], [])
122+ Node::element(bump, "div", [], [], [])
112123 }
113124 after(bump) {
114− Node::element(bump, "div", [Attribute { name: "value", value: "2" }], [])
125+ Node::element(bump, "div", [], [Attribute { name: "value", value: "2" }], [])
115126 }
116127 }
117128
118129 same_tag_name_many_attributes {
119130 before(bump) {
120− Node::element(bump, "div", [
131+ Node::element(bump, "div", [], [
121132 Attribute { name: "before-1", value: "1" },
122133 Attribute { name: "shared-1", value: "1" },
123134 Attribute { name: "modified-1", value: "1" },
@@ -130,7 +141,7 @@ before_after! {
130141 ], [])
131142 }
132143 after(bump) {
133− Node::element(bump, "div", [
144+ Node::element(bump, "div", [], [
134145 Attribute { name: "after-1", value: "1" },
135146 Attribute { name: "shared-1", value: "1" },
136147 Attribute { name: "modified-1", value: "100" },
@@ -146,12 +157,12 @@ before_after! {
146157
147158 same_tag_same_children {
148159 before(bump) {
149− Node::element(bump, "div", [], [
160+ Node::element(bump, "div", [], [], [
150161 Node::text("child")
151162 ])
152163 }
153164 after(bump) {
154− Node::element(bump, "div", [], [
165+ Node::element(bump, "div", [], [], [
155166 Node::text("child")
156167 ])
157168 }
@@ -159,12 +170,12 @@ before_after! {
159170
160171 same_tag_update_child {
161172 before(bump) {
162− Node::element(bump, "div", [], [
173+ Node::element(bump, "div", [], [], [
163174 Node::text("before")
164175 ])
165176 }
166177 after(bump) {
167− Node::element(bump, "div", [], [
178+ Node::element(bump, "div", [], [], [
168179 Node::text("after")
169180 ])
170181 }
@@ -172,10 +183,10 @@ before_after! {
172183
173184 same_tag_add_child {
174185 before(bump) {
175− Node::element(bump, "div", [], [])
186+ Node::element(bump, "div", [], [], [])
176187 }
177188 after(bump) {
178− Node::element(bump, "div", [], [
189+ Node::element(bump, "div", [], [], [
179190 Node::text("child")
180191 ])
181192 }
@@ -183,58 +194,58 @@ before_after! {
183194
184195 same_tag_remove_child {
185196 before(bump) {
186− Node::element(bump, "div", [], [
197+ Node::element(bump, "div", [], [], [
187198 Node::text("child")
188199 ])
189200 }
190201 after(bump) {
191− Node::element(bump, "div", [], [])
202+ Node::element(bump, "div", [], [], [])
192203 }
193204 }
194205
195206 same_tag_update_many_children {
196207 before(bump) {
197− Node::element(bump, "div", [], [
198− Node::element(bump, "div", [], []),
199− Node::element(bump, "span", [], []),
200− Node::element(bump, "p", [], []),
208+ Node::element(bump, "div", [], [], [
209+ Node::element(bump, "div", [], [], []),
210+ Node::element(bump, "span", [], [], []),
211+ Node::element(bump, "p", [], [], []),
201212 ])
202213 }
203214 after(bump) {
204− Node::element(bump, "div", [], [
205− Node::element(bump, "span", [], []),
206− Node::element(bump, "p", [], []),
207− Node::element(bump, "div", [], []),
215+ Node::element(bump, "div", [], [], [
216+ Node::element(bump, "span", [], [], []),
217+ Node::element(bump, "p", [], [], []),
218+ Node::element(bump, "div", [], [], []),
208219 ])
209220 }
210221 }
211222
212223 same_tag_remove_many_children {
213224 before(bump) {
214− Node::element(bump, "div", [], [
215− Node::element(bump, "div", [], []),
216− Node::element(bump, "span", [], []),
217− Node::element(bump, "p", [], []),
225+ Node::element(bump, "div", [], [], [
226+ Node::element(bump, "div", [], [], []),
227+ Node::element(bump, "span", [], [], []),
228+ Node::element(bump, "p", [], [], []),
218229 ])
219230 }
220231 after(bump) {
221− Node::element(bump, "div", [], [
222− Node::element(bump, "div", [], []),
232+ Node::element(bump, "div", [], [], [
233+ Node::element(bump, "div", [], [], []),
223234 ])
224235 }
225236 }
226237
227238 same_tag_add_many_children {
228239 before(bump) {
229− Node::element(bump, "div", [], [
230− Node::element(bump, "div", [], []),
240+ Node::element(bump, "div", [], [], [
241+ Node::element(bump, "div", [], [], []),
231242 ])
232243 }
233244 after(bump) {
234− Node::element(bump, "div", [], [
235− Node::element(bump, "div", [], []),
236− Node::element(bump, "span", [], []),
237− Node::element(bump, "p", [], []),
245+ Node::element(bump, "div", [], [], [
246+ Node::element(bump, "div", [], [], []),
247+ Node::element(bump, "span", [], [], []),
248+ Node::element(bump, "p", [], [], []),
238249 ])
239250 }
240251 }