From 5ce4df6dcac880be5edaa33ef3587303e78bdf28 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 14 Mar 2022 18:57:11 +0900 Subject: [PATCH 01/36] Bring changes to this branch. --- .github/workflows/main-checks.yml | 17 +- examples/agents/Cargo.toml | 2 +- examples/agents/src/bin/app.rs | 2 +- examples/boids/Cargo.toml | 2 +- examples/boids/src/main.rs | 2 +- examples/contexts/Cargo.toml | 2 +- examples/contexts/src/main.rs | 2 +- examples/counter/Cargo.toml | 2 +- examples/counter/src/main.rs | 2 +- examples/dyn_create_destroy_apps/Cargo.toml | 2 +- examples/dyn_create_destroy_apps/src/main.rs | 7 +- examples/file_upload/Cargo.toml | 2 +- examples/file_upload/src/main.rs | 2 +- examples/function_memory_game/Cargo.toml | 2 +- examples/function_memory_game/src/main.rs | 2 +- examples/function_router/Cargo.toml | 3 + examples/function_router/index.html | 1 + examples/function_router/src/main.rs | 3 +- examples/function_todomvc/Cargo.toml | 2 +- examples/function_todomvc/src/main.rs | 2 +- examples/futures/Cargo.toml | 2 +- examples/futures/src/main.rs | 2 +- examples/game_of_life/Cargo.toml | 2 +- examples/game_of_life/src/main.rs | 2 +- examples/inner_html/Cargo.toml | 2 +- examples/inner_html/src/main.rs | 2 +- examples/js_callback/Cargo.toml | 2 +- examples/js_callback/src/main.rs | 2 +- examples/keyed_list/Cargo.toml | 2 +- examples/keyed_list/src/main.rs | 2 +- examples/mount_point/Cargo.toml | 2 +- examples/mount_point/src/main.rs | 2 +- examples/nested_list/Cargo.toml | 2 +- examples/nested_list/src/main.rs | 2 +- examples/node_refs/Cargo.toml | 2 +- examples/node_refs/src/main.rs | 2 +- examples/password_strength/Cargo.toml | 2 +- examples/password_strength/src/main.rs | 2 +- examples/portals/Cargo.toml | 2 +- examples/portals/src/main.rs | 39 +- examples/router/Cargo.toml | 2 +- examples/router/src/main.rs | 2 +- examples/suspense/Cargo.toml | 2 +- examples/suspense/src/main.rs | 2 +- examples/timer/Cargo.toml | 2 +- examples/timer/src/main.rs | 2 +- examples/todomvc/Cargo.toml | 2 +- examples/todomvc/src/main.rs | 2 +- examples/two_apps/Cargo.toml | 2 +- examples/two_apps/src/main.rs | 2 +- examples/web_worker_fib/Cargo.toml | 2 +- examples/web_worker_fib/src/lib.rs | 2 +- examples/webgl/Cargo.toml | 2 +- examples/webgl/src/main.rs | 2 +- .../tests/html_macro/element-fail.stderr | 2 +- packages/yew-router/Cargo.toml | 1 + packages/yew-router/tests/basename.rs | 3 +- packages/yew-router/tests/browser_router.rs | 3 +- packages/yew-router/tests/hash_router.rs | 3 +- packages/yew-router/tests/link.rs | 6 +- packages/yew/Cargo.toml | 5 +- packages/yew/Makefile.toml | 18 + .../yew/src/{dom_bundle => }/app_handle.rs | 9 +- packages/yew/src/dom_bundle/bcomp.rs | 198 +------ packages/yew/src/dom_bundle/blist.rs | 6 +- packages/yew/src/dom_bundle/bnode.rs | 4 +- packages/yew/src/dom_bundle/bportal.rs | 4 +- packages/yew/src/dom_bundle/bsuspense.rs | 23 +- .../yew/src/dom_bundle/btag/attributes.rs | 2 +- packages/yew/src/dom_bundle/btag/listeners.rs | 5 +- packages/yew/src/dom_bundle/btag/mod.rs | 10 +- packages/yew/src/dom_bundle/btext.rs | 4 +- packages/yew/src/dom_bundle/mod.rs | 167 ++---- packages/yew/src/dom_bundle/tests/mod.rs | 26 - packages/yew/src/dom_bundle/traits.rs | 88 +++ packages/yew/src/dom_bundle/utils.rs | 28 + packages/yew/src/html/component/lifecycle.rs | 408 +++++++++----- packages/yew/src/html/component/mod.rs | 87 ++- packages/yew/src/html/component/scope.rs | 519 +++++++++++------- packages/yew/src/html/mod.rs | 72 ++- packages/yew/src/lib.rs | 118 +--- packages/yew/src/portal.rs | 53 ++ packages/yew/src/renderer.rs | 94 ++++ packages/yew/src/scheduler.rs | 237 ++++---- packages/yew/src/server_renderer.rs | 20 +- packages/yew/src/suspense/component.rs | 186 ++++--- packages/yew/src/suspense/mod.rs | 2 + .../{dom_bundle => }/tests/layout_tests.rs | 31 +- packages/yew/src/tests/mod.rs | 1 + packages/yew/src/virtual_dom/vcomp.rs | 104 +++- packages/yew/src/virtual_dom/vlist.rs | 15 +- packages/yew/src/virtual_dom/vnode.rs | 19 +- packages/yew/src/virtual_dom/vsuspense.rs | 34 +- packages/yew/src/virtual_dom/vtag.rs | 46 +- packages/yew/src/virtual_dom/vtext.rs | 23 +- .../base_component_impl-fail.stderr | 20 +- packages/yew/tests/mod.rs | 7 +- packages/yew/tests/suspense.rs | 16 +- packages/yew/tests/use_context.rs | 17 +- packages/yew/tests/use_effect.rs | 22 +- packages/yew/tests/use_memo.rs | 7 +- packages/yew/tests/use_reducer.rs | 12 +- packages/yew/tests/use_ref.rs | 7 +- packages/yew/tests/use_state.rs | 17 +- tools/website-test/Cargo.toml | 2 +- website/docs/advanced-topics/portals.mdx | 22 +- .../getting-started/build-a-sample-app.mdx | 21 +- .../yew/from-0_19_0-to-0_20_0.mdx | 6 + website/docs/tutorial/index.mdx | 17 +- 109 files changed, 1788 insertions(+), 1255 deletions(-) rename packages/yew/src/{dom_bundle => }/app_handle.rs (85%) delete mode 100644 packages/yew/src/dom_bundle/tests/mod.rs create mode 100644 packages/yew/src/dom_bundle/traits.rs create mode 100644 packages/yew/src/dom_bundle/utils.rs create mode 100644 packages/yew/src/portal.rs create mode 100644 packages/yew/src/renderer.rs rename packages/yew/src/{dom_bundle => }/tests/layout_tests.rs (85%) create mode 100644 packages/yew/src/tests/mod.rs diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index db27c709143..ae24820550d 100644 --- a/.github/workflows/main-checks.yml +++ b/.github/workflows/main-checks.yml @@ -27,6 +27,14 @@ jobs: command: clippy args: --all-targets -- -D warnings + - name: Lint feature soundness + run: | + cargo clippy -- --deny=warnings + cargo clippy --features=ssr -- --deny=warnings + cargo clippy --features=render -- --deny=warnings + cargo clippy --all-features --all-targets -- --deny=warnings + working-directory: packages/yew + clippy-release: name: Clippy on release profile @@ -49,6 +57,14 @@ jobs: command: clippy args: --all-targets --release -- -D warnings + - name: Lint feature soundness + run: | + cargo clippy --release -- --deny=warnings + cargo clippy --release --features=ssr -- --deny=warnings + cargo clippy --release --features=render -- --deny=warnings + cargo clippy --release --all-features --all-targets -- --deny=warnings + working-directory: packages/yew + doc_tests: name: Documentation Tests @@ -180,4 +196,3 @@ jobs: with: command: test args: -p yew-macro test_html_lints --features lints - diff --git a/examples/agents/Cargo.toml b/examples/agents/Cargo.toml index 2181b30c7eb..ddd23bcc21e 100644 --- a/examples/agents/Cargo.toml +++ b/examples/agents/Cargo.toml @@ -9,6 +9,6 @@ license = "MIT OR Apache-2.0" log = "0.4" serde = { version = "1.0", features = ["derive"] } wasm-logger = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } yew-agent = { path = "../../packages/yew-agent" } gloo-timers = "0.2" diff --git a/examples/agents/src/bin/app.rs b/examples/agents/src/bin/app.rs index 5b11b4fb854..1d6a6ef9518 100644 --- a/examples/agents/src/bin/app.rs +++ b/examples/agents/src/bin/app.rs @@ -1,4 +1,4 @@ fn main() { wasm_logger::init(wasm_logger::Config::default()); - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/boids/Cargo.toml b/examples/boids/Cargo.toml index 3e44001d44d..c23a7e666fd 100644 --- a/examples/boids/Cargo.toml +++ b/examples/boids/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0" getrandom = { version = "0.2", features = ["js"] } rand = "0.8" serde = { version = "1.0", features = ["derive"] } -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo = "0.6" [dependencies.web-sys] diff --git a/examples/boids/src/main.rs b/examples/boids/src/main.rs index ae1f2da6d81..28307bc8bfa 100644 --- a/examples/boids/src/main.rs +++ b/examples/boids/src/main.rs @@ -162,5 +162,5 @@ impl App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/contexts/Cargo.toml b/examples/contexts/Cargo.toml index 7deb6ccffcf..68206e297c7 100644 --- a/examples/contexts/Cargo.toml +++ b/examples/contexts/Cargo.toml @@ -6,5 +6,5 @@ license = "MIT OR Apache-2.0" [dependencies] serde = { version = "1.0", features = ["derive"] } -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } yew-agent = { path = "../../packages/yew-agent" } diff --git a/examples/contexts/src/main.rs b/examples/contexts/src/main.rs index 4f09126e462..5df627b94e7 100644 --- a/examples/contexts/src/main.rs +++ b/examples/contexts/src/main.rs @@ -19,5 +19,5 @@ pub fn App() -> Html { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 403a8299268..3ad4bf2b0ee 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -8,5 +8,5 @@ license = "MIT OR Apache-2.0" [dependencies] gloo-console = "0.2" js-sys = "0.3" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } wasm-bindgen = "0.2" diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index d863bd894ac..d240ef192f5 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -72,5 +72,5 @@ impl Component for App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/dyn_create_destroy_apps/Cargo.toml b/examples/dyn_create_destroy_apps/Cargo.toml index 2cc223d0b39..3ad361931ea 100644 --- a/examples/dyn_create_destroy_apps/Cargo.toml +++ b/examples/dyn_create_destroy_apps/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" [dependencies] js-sys = "0.3" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } slab = "0.4.3" gloo = "0.6" wasm-bindgen = "0.2" diff --git a/examples/dyn_create_destroy_apps/src/main.rs b/examples/dyn_create_destroy_apps/src/main.rs index 431f0407367..658edbfac71 100644 --- a/examples/dyn_create_destroy_apps/src/main.rs +++ b/examples/dyn_create_destroy_apps/src/main.rs @@ -55,14 +55,15 @@ impl Component for App { // Get the key for the entry and create and mount a new CounterModel app // with a callback that destroys the app when emitted let app_key = app_entry.key(); - let new_counter_app = yew::start_app_with_props_in_element( + let new_counter_app = yew::Renderer::::with_root_and_props( app_div.clone(), CounterProps { destroy_callback: ctx .link() .callback(move |_| Msg::DestroyCounterApp(app_key)), }, - ); + ) + .render(); // Insert the app and the app div to our app collection app_entry.insert((app_div, new_counter_app)); @@ -107,5 +108,5 @@ impl Component for App { fn main() { // Start main app - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/file_upload/Cargo.toml b/examples/file_upload/Cargo.toml index 4425b4ce29e..98249727367 100644 --- a/examples/file_upload/Cargo.toml +++ b/examples/file_upload/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" [dependencies] js-sys = "0.3" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo-file = "0.2" [dependencies.web-sys] diff --git a/examples/file_upload/src/main.rs b/examples/file_upload/src/main.rs index 18562cb0850..88584007aa0 100644 --- a/examples/file_upload/src/main.rs +++ b/examples/file_upload/src/main.rs @@ -124,5 +124,5 @@ impl App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/function_memory_game/Cargo.toml b/examples/function_memory_game/Cargo.toml index 3c73774c93f..9846c2f3ef0 100644 --- a/examples/function_memory_game/Cargo.toml +++ b/examples/function_memory_game/Cargo.toml @@ -13,7 +13,7 @@ gloo = "0.4" nanoid = "0.4" rand = "0.8" getrandom = { version = "0.2", features = ["js"] } -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } [dependencies.web-sys] version = "0.3" diff --git a/examples/function_memory_game/src/main.rs b/examples/function_memory_game/src/main.rs index 995fd2787be..dd70adcb37d 100644 --- a/examples/function_memory_game/src/main.rs +++ b/examples/function_memory_game/src/main.rs @@ -6,5 +6,5 @@ mod state; use crate::components::app::App; fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/function_router/Cargo.toml b/examples/function_router/Cargo.toml index 0593e73ca34..c09cbfabbd1 100644 --- a/examples/function_router/Cargo.toml +++ b/examples/function_router/Cargo.toml @@ -21,3 +21,6 @@ wasm-logger = "0.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] instant = { version = "0.1" } + +[features] +render = ["yew/render"] diff --git a/examples/function_router/index.html b/examples/function_router/index.html index d7101222d8b..34950cc7611 100644 --- a/examples/function_router/index.html +++ b/examples/function_router/index.html @@ -11,6 +11,7 @@ href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css" /> + diff --git a/examples/function_router/src/main.rs b/examples/function_router/src/main.rs index 1b16d4978ff..ee2be1e7df0 100644 --- a/examples/function_router/src/main.rs +++ b/examples/function_router/src/main.rs @@ -9,5 +9,6 @@ pub use app::*; fn main() { #[cfg(target_arch = "wasm32")] wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); - yew::start_app::(); + #[cfg(feature = "render")] + yew::Renderer::::new().render(); } diff --git a/examples/function_todomvc/Cargo.toml b/examples/function_todomvc/Cargo.toml index a56da0cb76b..a8ab9181292 100644 --- a/examples/function_todomvc/Cargo.toml +++ b/examples/function_todomvc/Cargo.toml @@ -10,7 +10,7 @@ serde = { version = "1.0", features = ["derive"] } strum = "0.24" strum_macros = "0.24" gloo = "0.6" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } [dependencies.web-sys] version = "0.3" diff --git a/examples/function_todomvc/src/main.rs b/examples/function_todomvc/src/main.rs index 434f35caea7..acfda1d23f3 100644 --- a/examples/function_todomvc/src/main.rs +++ b/examples/function_todomvc/src/main.rs @@ -145,5 +145,5 @@ fn app() -> Html { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/futures/Cargo.toml b/examples/futures/Cargo.toml index 7e473a68579..3f862e56b03 100644 --- a/examples/futures/Cargo.toml +++ b/examples/futures/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" pulldown-cmark = { version = "0.9", default-features = false } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" -yew = { path = "../../packages/yew", features = ["tokio"] } +yew = { path = "../../packages/yew", features = ["tokio", "render"] } gloo-utils = "0.1" [dependencies.web-sys] diff --git a/examples/futures/src/main.rs b/examples/futures/src/main.rs index 56ded191690..1f2866d1851 100644 --- a/examples/futures/src/main.rs +++ b/examples/futures/src/main.rs @@ -128,5 +128,5 @@ impl Component for App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index bb7c8fe1bd2..157a414691e 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -14,5 +14,5 @@ getrandom = { version = "0.2", features = ["js"] } log = "0.4" rand = "0.8" wasm-logger = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo-timers = "0.2" diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index cbea803745d..5699d785c6f 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -226,5 +226,5 @@ fn wrap(coord: isize, range: isize) -> usize { fn main() { wasm_logger::init(wasm_logger::Config::default()); log::trace!("Initializing yew..."); - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/inner_html/Cargo.toml b/examples/inner_html/Cargo.toml index e8c10c2be3d..d164676976c 100644 --- a/examples/inner_html/Cargo.toml +++ b/examples/inner_html/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo-utils = "0.1" [dependencies.web-sys] diff --git a/examples/inner_html/src/main.rs b/examples/inner_html/src/main.rs index 4e825a4ca8d..1bd7fa8f752 100644 --- a/examples/inner_html/src/main.rs +++ b/examples/inner_html/src/main.rs @@ -26,5 +26,5 @@ impl Component for App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/js_callback/Cargo.toml b/examples/js_callback/Cargo.toml index 4d111bf0623..8413fa356ab 100644 --- a/examples/js_callback/Cargo.toml +++ b/examples/js_callback/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" [dependencies] wasm-bindgen = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } [dependencies.web-sys] version = "0.3" diff --git a/examples/js_callback/src/main.rs b/examples/js_callback/src/main.rs index b69e8cbd182..b5f4e06acee 100644 --- a/examples/js_callback/src/main.rs +++ b/examples/js_callback/src/main.rs @@ -73,5 +73,5 @@ impl Component for App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/keyed_list/Cargo.toml b/examples/keyed_list/Cargo.toml index 21be5cfdc5a..0bd0355a0fd 100644 --- a/examples/keyed_list/Cargo.toml +++ b/examples/keyed_list/Cargo.toml @@ -12,7 +12,7 @@ instant = { version = "0.1", features = ["wasm-bindgen"] } log = "0.4" rand = "0.8" wasm-logger = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } [dependencies.web-sys] version = "0.3" diff --git a/examples/keyed_list/src/main.rs b/examples/keyed_list/src/main.rs index 1e9ae0d1dd9..7b87283b49b 100644 --- a/examples/keyed_list/src/main.rs +++ b/examples/keyed_list/src/main.rs @@ -279,5 +279,5 @@ impl App { fn main() { wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/mount_point/Cargo.toml b/examples/mount_point/Cargo.toml index b62137a85ea..33c24f20545 100644 --- a/examples/mount_point/Cargo.toml +++ b/examples/mount_point/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" [dependencies] wasm-bindgen = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo-utils = "0.1" [dependencies.web-sys] diff --git a/examples/mount_point/src/main.rs b/examples/mount_point/src/main.rs index 983e64dbca0..3354caebe55 100644 --- a/examples/mount_point/src/main.rs +++ b/examples/mount_point/src/main.rs @@ -73,5 +73,5 @@ fn main() { body.append_child(&mount_point).unwrap(); - yew::start_app_in_element::(mount_point); + yew::Renderer::::with_root(mount_point).render(); } diff --git a/examples/nested_list/Cargo.toml b/examples/nested_list/Cargo.toml index 9c3678922a6..da944d582f1 100644 --- a/examples/nested_list/Cargo.toml +++ b/examples/nested_list/Cargo.toml @@ -8,4 +8,4 @@ license = "MIT OR Apache-2.0" [dependencies] log = "0.4" wasm-logger = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } diff --git a/examples/nested_list/src/main.rs b/examples/nested_list/src/main.rs index c30277d4330..ea54369f213 100644 --- a/examples/nested_list/src/main.rs +++ b/examples/nested_list/src/main.rs @@ -63,5 +63,5 @@ impl fmt::Display for Hovered { fn main() { wasm_logger::init(wasm_logger::Config::default()); - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/node_refs/Cargo.toml b/examples/node_refs/Cargo.toml index 23fa0e436c7..9d6cd3fbf09 100644 --- a/examples/node_refs/Cargo.toml +++ b/examples/node_refs/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } web-sys = { version = "0.3", features = ["HtmlElement", "HtmlInputElement", "Node"] } diff --git a/examples/node_refs/src/main.rs b/examples/node_refs/src/main.rs index 3f8f329526f..8edfb015d50 100644 --- a/examples/node_refs/src/main.rs +++ b/examples/node_refs/src/main.rs @@ -77,5 +77,5 @@ impl Component for App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/password_strength/Cargo.toml b/examples/password_strength/Cargo.toml index c73f7ae6b62..f3ff7f8f4ef 100644 --- a/examples/password_strength/Cargo.toml +++ b/examples/password_strength/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } zxcvbn = "2.1.2" js-sys = "0.3.46" web-sys = { version = "0.3", features = ["Event","EventTarget","InputEvent"] } diff --git a/examples/password_strength/src/main.rs b/examples/password_strength/src/main.rs index 19ed552dc96..b845bdcdfe2 100644 --- a/examples/password_strength/src/main.rs +++ b/examples/password_strength/src/main.rs @@ -8,5 +8,5 @@ mod password; use app::App; fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/portals/Cargo.toml b/examples/portals/Cargo.toml index abf2f445939..24fc7f04d1f 100644 --- a/examples/portals/Cargo.toml +++ b/examples/portals/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo-utils = "0.1" wasm-bindgen = "0.2" diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 7c16713a894..c4886ff25a4 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -1,6 +1,6 @@ use wasm_bindgen::JsCast; use web_sys::{Element, ShadowRootInit, ShadowRootMode}; -use yew::{create_portal, html, Children, Component, Context, Html, NodeRef, Properties}; +use yew::prelude::*; #[derive(Properties, PartialEq)] pub struct ShadowDOMProps { @@ -49,15 +49,16 @@ impl Component for ShadowDOMHost { } fn view(&self, ctx: &Context) -> Html { - let contents = if let Some(ref inner_host) = self.inner_host { - create_portal( + let contents = match self.inner_host { + Some(ref m) => { + let children = ctx.props().children.clone(); html! { - {for ctx.props().children.iter()} - }, - inner_host.clone(), - ) - } else { - html! { <> } + + {children} + + } + } + None => Html::default(), }; html! {
@@ -76,15 +77,17 @@ impl Component for App { type Properties = (); fn create(_ctx: &Context) -> Self { - let document_head = gloo_utils::document() + let document_head: Element = gloo_utils::document() .head() - .expect("head element to be present"); - let style_html = create_portal( - html! { - - }, - document_head.into(), - ); + .expect("head element to be present") + .into(); + let style_html = html! { + + + + }; Self { style_html } } @@ -102,5 +105,5 @@ impl Component for App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/router/Cargo.toml b/examples/router/Cargo.toml index 598e8f84902..f9a0990a3a1 100644 --- a/examples/router/Cargo.toml +++ b/examples/router/Cargo.toml @@ -11,7 +11,7 @@ log = "0.4" getrandom = { version = "0.2", features = ["js"] } rand = { version = "0.8", features = ["small_rng"] } wasm-logger = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } yew-router = { path = "../../packages/yew-router" } serde = { version = "1.0", features = ["derive"] } lazy_static = "1.4.0" diff --git a/examples/router/src/main.rs b/examples/router/src/main.rs index 55c424857db..14280b1897d 100644 --- a/examples/router/src/main.rs +++ b/examples/router/src/main.rs @@ -147,5 +147,5 @@ fn switch(routes: &Route) -> Html { fn main() { wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/suspense/Cargo.toml b/examples/suspense/Cargo.toml index 1dc2b395a3a..602bb5d4d86 100644 --- a/examples/suspense/Cargo.toml +++ b/examples/suspense/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -yew = { path = "../../packages/yew", features = ["tokio"] } +yew = { path = "../../packages/yew", features = ["tokio", "render"] } gloo-timers = { version = "0.2.2", features = ["futures"] } wasm-bindgen-futures = "0.4" wasm-bindgen = "0.2" diff --git a/examples/suspense/src/main.rs b/examples/suspense/src/main.rs index 31f0a3d3591..496429fd6b8 100644 --- a/examples/suspense/src/main.rs +++ b/examples/suspense/src/main.rs @@ -56,5 +56,5 @@ fn app() -> Html { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/timer/Cargo.toml b/examples/timer/Cargo.toml index e91b9d5cc16..b383172e89d 100644 --- a/examples/timer/Cargo.toml +++ b/examples/timer/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } js-sys = "0.3" gloo = "0.6" wasm-bindgen = "0.2" diff --git a/examples/timer/src/main.rs b/examples/timer/src/main.rs index db9bf8374c1..4f2ce8dada5 100644 --- a/examples/timer/src/main.rs +++ b/examples/timer/src/main.rs @@ -150,5 +150,5 @@ impl Component for App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml index f933eb86a4f..d0a06e92edd 100644 --- a/examples/todomvc/Cargo.toml +++ b/examples/todomvc/Cargo.toml @@ -10,7 +10,7 @@ strum = "0.24" strum_macros = "0.24" serde = "1" serde_derive = "1" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo = "0.6" [dependencies.web-sys] diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index aca6fdc43ec..1ea9dc2b6de 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -245,5 +245,5 @@ impl App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/examples/two_apps/Cargo.toml b/examples/two_apps/Cargo.toml index af49ad8c276..216a38845a5 100644 --- a/examples/two_apps/Cargo.toml +++ b/examples/two_apps/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo-utils = "0.1" diff --git a/examples/two_apps/src/main.rs b/examples/two_apps/src/main.rs index 6990c019904..ef91b2dacb1 100644 --- a/examples/two_apps/src/main.rs +++ b/examples/two_apps/src/main.rs @@ -72,7 +72,7 @@ impl Component for App { fn mount_app(selector: &'static str) -> AppHandle { let document = gloo_utils::document(); let element = document.query_selector(selector).unwrap().unwrap(); - yew::start_app_in_element(element) + yew::Renderer::::with_root(element).render() } fn main() { diff --git a/examples/web_worker_fib/Cargo.toml b/examples/web_worker_fib/Cargo.toml index ed6511de40a..8a3bc84644f 100644 --- a/examples/web_worker_fib/Cargo.toml +++ b/examples/web_worker_fib/Cargo.toml @@ -8,7 +8,7 @@ authors = ["Shrey Somaiya", "Zac Kologlu"] crate-type = ["cdylib"] [dependencies] -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } yew-agent = { path = "../../packages/yew-agent" } wasm-bindgen = "0.2" js-sys = "0.3" diff --git a/examples/web_worker_fib/src/lib.rs b/examples/web_worker_fib/src/lib.rs index eb1a45a2833..7701d304a42 100644 --- a/examples/web_worker_fib/src/lib.rs +++ b/examples/web_worker_fib/src/lib.rs @@ -12,7 +12,7 @@ pub fn start() { use js_sys::{global, Reflect}; if Reflect::has(&global(), &JsValue::from_str("window")).unwrap() { - yew::start_app::(); + yew::Renderer::::new().render(); } else { agent::Worker::register(); } diff --git a/examples/webgl/Cargo.toml b/examples/webgl/Cargo.toml index 32328eb99b5..492cd91eb5e 100644 --- a/examples/webgl/Cargo.toml +++ b/examples/webgl/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [dependencies] js-sys = "0.3" wasm-bindgen = "0.2" -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["render"] } gloo-render = "0.1" [dependencies.web-sys] diff --git a/examples/webgl/src/main.rs b/examples/webgl/src/main.rs index 19a47fd4fdf..00f6ac21e0b 100644 --- a/examples/webgl/src/main.rs +++ b/examples/webgl/src/main.rs @@ -134,5 +134,5 @@ impl App { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } diff --git a/packages/yew-macro/tests/html_macro/element-fail.stderr b/packages/yew-macro/tests/html_macro/element-fail.stderr index 0ef61f0b6e5..5c2652e2680 100644 --- a/packages/yew-macro/tests/html_macro/element-fail.stderr +++ b/packages/yew-macro/tests/html_macro/element-fail.stderr @@ -483,6 +483,6 @@ error[E0277]: the trait bound `Cow<'static, str>: From<{integer}>` is not satisf as From<&'a CString>> as From> as From<&'a OsStr>> - and 14 others + and 11 others = note: required because of the requirements on the impl of `Into>` for `{integer}` note: required by `into` diff --git a/packages/yew-router/Cargo.toml b/packages/yew-router/Cargo.toml index 3aab33adb4c..6bb8e4e2b83 100644 --- a/packages/yew-router/Cargo.toml +++ b/packages/yew-router/Cargo.toml @@ -36,6 +36,7 @@ features = [ [dev-dependencies] wasm-bindgen-test = "0.3" serde = { version = "1", features = ["derive"] } +yew = { version = "0.19.3", path = "../yew", features = ["render"] } [dev-dependencies.web-sys] version = "0.3" diff --git a/packages/yew-router/tests/basename.rs b/packages/yew-router/tests/basename.rs index 97d65320eac..a22687718ad 100644 --- a/packages/yew-router/tests/basename.rs +++ b/packages/yew-router/tests/basename.rs @@ -115,7 +115,8 @@ fn root() -> Html { // - 404 redirects #[test] async fn router_works() { - yew::start_app_in_element::(gloo::utils::document().get_element_by_id("output").unwrap()); + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew-router/tests/browser_router.rs b/packages/yew-router/tests/browser_router.rs index d720e2546ca..ae43f640882 100644 --- a/packages/yew-router/tests/browser_router.rs +++ b/packages/yew-router/tests/browser_router.rs @@ -115,7 +115,8 @@ fn root() -> Html { // - 404 redirects #[test] async fn router_works() { - yew::start_app_in_element::(gloo::utils::document().get_element_by_id("output").unwrap()); + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew-router/tests/hash_router.rs b/packages/yew-router/tests/hash_router.rs index 1e75396cfdd..cb5130abab8 100644 --- a/packages/yew-router/tests/hash_router.rs +++ b/packages/yew-router/tests/hash_router.rs @@ -115,7 +115,8 @@ fn root() -> Html { // - 404 redirects #[test] async fn router_works() { - yew::start_app_in_element::(gloo::utils::document().get_element_by_id("output").unwrap()); + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew-router/tests/link.rs b/packages/yew-router/tests/link.rs index 1d9c0afa746..718ce3f13d1 100644 --- a/packages/yew-router/tests/link.rs +++ b/packages/yew-router/tests/link.rs @@ -93,7 +93,7 @@ async fn link_in_browser_router() { let div = gloo::utils::document().create_element("div").unwrap(); let _ = div.set_attribute("id", "browser-router"); let _ = gloo::utils::body().append_child(&div); - yew::start_app_in_element::(div); + yew::Renderer::::with_root(div).render(); sleep(Duration::ZERO).await; @@ -128,7 +128,7 @@ async fn link_with_basename() { let div = gloo::utils::document().create_element("div").unwrap(); let _ = div.set_attribute("id", "with-basename"); let _ = gloo::utils::body().append_child(&div); - yew::start_app_in_element::(div); + yew::Renderer::::with_root(div).render(); sleep(Duration::ZERO).await; @@ -166,7 +166,7 @@ async fn link_in_hash_router() { let div = gloo::utils::document().create_element("div").unwrap(); let _ = div.set_attribute("id", "hash-router"); let _ = gloo::utils::body().append_child(&div); - yew::start_app_in_element::(div); + yew::Renderer::::with_root(div).render(); sleep(Duration::ZERO).await; diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index d4bb8dec3d0..f32804bf63b 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -77,9 +77,10 @@ rustversion = "1" trybuild = "1" [features] -doc_test = [] -wasm_test = [] ssr = ["futures", "html-escape"] +render = [] +doc_test = ["render"] +wasm_test = ["render"] default = [] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index 6cb68f638c4..fede2657c13 100644 --- a/packages/yew/Makefile.toml +++ b/packages/yew/Makefile.toml @@ -31,3 +31,21 @@ args = [ [tasks.ssr-test] command = "cargo" args = ["test", "ssr_tests", "--features", "ssr"] + +[tasks.clippy-feature-soundness] +script = ''' +#!/usr/bin/env bash +set -ex +cargo clippy -- --deny=warnings +cargo clippy --features=ssr -- --deny=warnings +cargo clippy --features=render -- --deny=warnings +cargo clippy --all-features --all-targets -- --deny=warnings + +cargo clippy --release -- --deny=warnings +cargo clippy --release --features=ssr -- --deny=warnings +cargo clippy --release --features=render -- --deny=warnings +cargo clippy --release --all-features --all-targets -- --deny=warnings +''' + +[tasks.lint-flow] +dependencies = ["clippy-feature-soundness"] diff --git a/packages/yew/src/dom_bundle/app_handle.rs b/packages/yew/src/app_handle.rs similarity index 85% rename from packages/yew/src/dom_bundle/app_handle.rs rename to packages/yew/src/app_handle.rs index 7cd49e94ee1..597c431c534 100644 --- a/packages/yew/src/dom_bundle/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -1,6 +1,6 @@ //! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. -use super::{ComponentRenderState, Scoped}; +use crate::html::Scoped; use crate::html::{IntoComponent, NodeRef, Scope}; use std::ops::Deref; use std::rc::Rc; @@ -8,6 +8,7 @@ use web_sys::Element; /// An instance of an application. #[derive(Debug)] +#[cfg_attr(documenting, doc(cfg(feature = "render")))] pub struct AppHandle { /// `Scope` holder pub(crate) scope: Scope<::Component>, @@ -26,11 +27,9 @@ where let app = Self { scope: Scope::new(None), }; - let node_ref = NodeRef::default(); - let initial_render_state = - ComponentRenderState::new(element, NodeRef::default(), &node_ref); + app.scope - .mount_in_place(initial_render_state, node_ref, props); + .mount_in_place(element, NodeRef::default(), NodeRef::default(), props); app } diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index c68db83af9a..d427dfc0049 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -1,21 +1,16 @@ //! This module contains the bundle implementation of a virtual component [BComp]. -use super::{insert_node, BNode, DomBundle, Reconcilable}; -use crate::html::{AnyScope, BaseComponent, Scope}; -use crate::virtual_dom::{Key, VComp, VNode}; +use super::{BNode, DomBundle, Reconcilable}; +use crate::html::AnyScope; +use crate::html::Scoped; +use crate::virtual_dom::{Key, VComp}; use crate::NodeRef; -#[cfg(feature = "ssr")] -use futures::channel::oneshot; -#[cfg(feature = "ssr")] -use futures::future::{FutureExt, LocalBoxFuture}; -use gloo_utils::document; -use std::cell::Ref; +use std::fmt; use std::{any::TypeId, borrow::Borrow}; -use std::{fmt, rc::Rc}; -use web_sys::{Element, Node}; +use web_sys::Element; /// A virtual component. Compare with [VComp]. -pub struct BComp { +pub(super) struct BComp { type_id: TypeId, scope: Box, node_ref: NodeRef, @@ -24,18 +19,16 @@ pub struct BComp { impl BComp { /// Get the key of the underlying component - pub(super) fn key(&self) -> Option<&Key> { + pub fn key(&self) -> Option<&Key> { self.key.as_ref() } } impl fmt::Debug for BComp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "BComp {{ root: {:?} }}", - self.scope.as_ref().render_state(), - ) + f.debug_struct("BComp") + .field("root", &self.scope.as_ref().render_state()) + .finish() } } @@ -123,169 +116,7 @@ impl Reconcilable for VComp { } } -pub trait Mountable { - fn copy(&self) -> Box; - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box; - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); - - #[cfg(feature = "ssr")] - fn render_to_string<'a>( - &'a self, - w: &'a mut String, - parent_scope: &'a AnyScope, - ) -> LocalBoxFuture<'a, ()>; -} - -pub struct PropsWrapper { - props: Rc, -} - -impl PropsWrapper { - pub fn new(props: Rc) -> Self { - Self { props } - } -} - -impl Mountable for PropsWrapper { - fn copy(&self) -> Box { - let wrapper: PropsWrapper = PropsWrapper { - props: Rc::clone(&self.props), - }; - Box::new(wrapper) - } - - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - let initial_render_state = ComponentRenderState::new(parent, next_sibling, &node_ref); - scope.mount_in_place(initial_render_state, node_ref, self.props); - - Box::new(scope) - } - - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { - let scope: Scope = scope.to_any().downcast::(); - scope.reuse(self.props, node_ref, next_sibling); - } - - #[cfg(feature = "ssr")] - fn render_to_string<'a>( - &'a self, - w: &'a mut String, - parent_scope: &'a AnyScope, - ) -> LocalBoxFuture<'a, ()> { - async move { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.render_to_string(w, self.props.clone()).await; - } - .boxed_local() - } -} - -pub struct ComponentRenderState { - root_node: BNode, - /// When a component has no parent, it means that it should not be rendered. - parent: Option, - next_sibling: NodeRef, - - #[cfg(feature = "ssr")] - html_sender: Option>, -} - -impl std::fmt::Debug for ComponentRenderState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.root_node.fmt(f) - } -} - -impl ComponentRenderState { - /// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component - pub(crate) fn new(parent: Element, next_sibling: NodeRef, node_ref: &NodeRef) -> Self { - let placeholder = { - let placeholder: Node = document().create_text_node("").into(); - insert_node(&placeholder, &parent, next_sibling.get().as_ref()); - node_ref.set(Some(placeholder.clone())); - BNode::Ref(placeholder) - }; - Self { - root_node: placeholder, - parent: Some(parent), - next_sibling, - #[cfg(feature = "ssr")] - html_sender: None, - } - } - /// Set up server-side rendering of a component - #[cfg(feature = "ssr")] - pub(crate) fn new_ssr(tx: oneshot::Sender) -> Self { - use super::blist::BList; - - Self { - root_node: BNode::List(BList::new()), - parent: None, - next_sibling: NodeRef::default(), - html_sender: Some(tx), - } - } - /// Reuse the render state, asserting a new next_sibling - pub(crate) fn reuse(&mut self, next_sibling: NodeRef) { - self.next_sibling = next_sibling; - } - /// Shift the rendered content to a new DOM position - pub(crate) fn shift(&mut self, new_parent: Element, next_sibling: NodeRef) { - self.root_node.shift(&new_parent, next_sibling.clone()); - - self.parent = Some(new_parent); - self.next_sibling = next_sibling; - } - /// Reconcile the rendered content with a new [VNode] - pub(crate) fn reconcile(&mut self, root: VNode, scope: &AnyScope) -> NodeRef { - if let Some(ref parent) = self.parent { - let next_sibling = self.next_sibling.clone(); - - root.reconcile_node(scope, parent, next_sibling, &mut self.root_node) - } else { - #[cfg(feature = "ssr")] - if let Some(tx) = self.html_sender.take() { - tx.send(root).unwrap(); - } - NodeRef::default() - } - } - /// Detach the rendered content from the DOM - pub(crate) fn detach(self, parent_to_detach: bool) { - if let Some(ref m) = self.parent { - self.root_node.detach(m, parent_to_detach); - } - } - - pub(crate) fn should_trigger_rendered(&self) -> bool { - self.parent.is_some() - } -} - -pub trait Scoped { - fn to_any(&self) -> AnyScope; - /// Get the render state if it hasn't already been destroyed - fn render_state(&self) -> Option>; - /// Shift the node associated with this scope to a new place - fn shift_node(&self, parent: Element, next_sibling: NodeRef); - /// Process an event to destroy a component - fn destroy(self, parent_to_detach: bool); - fn destroy_boxed(self: Box, parent_to_detach: bool); -} - +#[cfg(feature = "wasm_test")] #[cfg(test)] mod tests { use super::*; @@ -301,10 +132,8 @@ mod tests { use web_sys::Element; use web_sys::Node; - #[cfg(feature = "wasm_test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); struct Comp; @@ -576,6 +405,7 @@ mod tests { } } +#[cfg(feature = "wasm_test")] #[cfg(test)] mod layout_tests { extern crate self as yew; @@ -585,10 +415,8 @@ mod layout_tests { use crate::{Children, Component, Context, Html, Properties}; use std::marker::PhantomData; - #[cfg(feature = "wasm_test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); struct Comp { diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 21e66fc60ec..389fb17f2b5 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -12,7 +12,7 @@ use web_sys::Element; /// This struct represents a mounted [VList] #[derive(Debug)] -pub struct BList { +pub(super) struct BList { /// The reverse (render order) list of child [BNode]s rev_children: Vec, /// All [BNode]s in the BList have keys @@ -120,7 +120,7 @@ impl BNode { impl BList { /// Create a new empty [BList] - pub(super) const fn new() -> BList { + pub const fn new() -> BList { BList { rev_children: vec![], fully_keyed: true, @@ -129,7 +129,7 @@ impl BList { } /// Get the key of the underlying fragment - pub(super) fn key(&self) -> Option<&Key> { + pub fn key(&self) -> Option<&Key> { self.key.as_ref() } diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 0e80563fd30..d0cdd318bba 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -9,7 +9,7 @@ use std::fmt; use web_sys::{Element, Node}; /// The bundle implementation to [VNode]. -pub enum BNode { +pub(super) enum BNode { /// A bind between `VTag` and `Element`. Tag(Box), /// A bind between `VText` and `TextNode`. @@ -28,7 +28,7 @@ pub enum BNode { impl BNode { /// Get the key of the underlying node - pub(super) fn key(&self) -> Option<&Key> { + pub fn key(&self) -> Option<&Key> { match self { Self::Comp(bsusp) => bsusp.key(), Self::List(blist) => blist.key(), diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index a5c6d769181..7bd1a806595 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -10,7 +10,7 @@ use web_sys::Element; /// The bundle implementation to [VPortal]. #[derive(Debug)] -pub struct BPortal { +pub(super) struct BPortal { /// The element under which the content is inserted. host: Element, /// The next sibling after the inserted content @@ -99,7 +99,7 @@ impl Reconcilable for VPortal { impl BPortal { /// Get the key of the underlying portal - pub(super) fn key(&self) -> Option<&Key> { + pub fn key(&self) -> Option<&Key> { self.node.key() } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 0781b512e7d..d008ddbe474 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -4,11 +4,12 @@ use super::{BNode, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; use crate::NodeRef; +use gloo::utils::document; use web_sys::Element; /// The bundle implementation to [VSuspense] #[derive(Debug)] -pub struct BSuspense { +pub(super) struct BSuspense { children_bundle: BNode, /// The supsense is suspended if fallback contains [Some] bundle fallback_bundle: Option, @@ -18,7 +19,7 @@ pub struct BSuspense { impl BSuspense { /// Get the key of the underlying suspense - pub(super) fn key(&self) -> Option<&Key> { + pub fn key(&self) -> Option<&Key> { self.key.as_ref() } /// Get the bundle node that actually shows up in the dom @@ -56,11 +57,12 @@ impl Reconcilable for VSuspense { let VSuspense { children, fallback, - detached_parent, suspended, key, } = self; - let detached_parent = detached_parent.expect("no detached parent?"); + let detached_parent = document() + .create_element("div") + .expect("failed to create detached element"); // When it's suspended, we render children into an element that is detached from the dom // tree while rendering fallback UI into the original place where children resides in. @@ -100,10 +102,7 @@ impl Reconcilable for VSuspense { ) -> NodeRef { match bundle { // We only preserve the child state if they are the same suspense. - BNode::Suspense(m) - if m.key == self.key - && self.detached_parent.as_ref() == Some(&m.detached_parent) => - { + BNode::Suspense(m) if m.key == self.key => { self.reconcile(parent_scope, parent, next_sibling, m) } _ => self.replace(parent_scope, parent, next_sibling, bundle), @@ -120,11 +119,9 @@ impl Reconcilable for VSuspense { let VSuspense { children, fallback, - detached_parent, suspended, key: _, } = self; - let detached_parent = detached_parent.expect("no detached parent?"); let children_bundle = &mut suspense.children_bundle; // no need to update key & detached_parent @@ -136,7 +133,7 @@ impl Reconcilable for VSuspense { (true, Some(fallback_bundle)) => { children.reconcile_node( parent_scope, - &detached_parent, + &suspense.detached_parent, NodeRef::default(), children_bundle, ); @@ -149,11 +146,11 @@ impl Reconcilable for VSuspense { } // Freshly suspended. Shift children into the detached parent, then add fallback to the DOM (true, None) => { - children_bundle.shift(&detached_parent, NodeRef::default()); + children_bundle.shift(&suspense.detached_parent, NodeRef::default()); children.reconcile_node( parent_scope, - &detached_parent, + &suspense.detached_parent, NodeRef::default(), children_bundle, ); diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs index cdec0630e6b..761d74986ed 100644 --- a/packages/yew/src/dom_bundle/btag/attributes.rs +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -53,7 +53,7 @@ macro_rules! impl_access_value { impl_access_value! {InputElement TextAreaElement} /// Able to have its value read or set -pub trait AccessValue { +pub(super) trait AccessValue { fn value(&self) -> String; fn set_value(&self, v: &str); } diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 66f14363b3f..687e92106ef 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -34,6 +34,7 @@ static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); /// handler has no effect. /// /// This function should be called before any component is mounted. +#[cfg_attr(documenting, doc(cfg(feature = "render")))] pub fn set_event_bubbling(bubble: bool) { BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); } @@ -105,7 +106,7 @@ impl ListenerRegistration { } /// Remove any registered event listeners from the global registry - pub(super) fn unregister(&self) { + pub fn unregister(&self) { if let Self::Registered(id) = self { Registry::with(|r| r.unregister(id)); } @@ -406,7 +407,7 @@ mod tests { let root = document().create_element("div").unwrap(); document().body().unwrap().append_child(&root).unwrap(); - let app = crate::start_app_in_element::>(root); + let app = crate::Renderer::>::with_root(root).render(); scheduler::start_now(); (app, get_el_by_tag(tag)) diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 1d172c1f87d..46a995608ab 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -56,7 +56,7 @@ enum BTagInner { /// The bundle implementation to [VTag] #[derive(Debug)] -pub struct BTag { +pub(super) struct BTag { /// [BTag] fields that are specific to different [BTag] kinds. inner: BTagInner, listeners: ListenerRegistration, @@ -247,15 +247,17 @@ impl VTag { impl BTag { /// Get the key of the underlying tag - pub(super) fn key(&self) -> Option<&Key> { + pub fn key(&self) -> Option<&Key> { self.key.as_ref() } + #[cfg(feature = "wasm_test")] #[cfg(test)] fn reference(&self) -> &Element { &self.reference } + #[cfg(feature = "wasm_test")] #[cfg(test)] fn children(&self) -> &[BNode] { match &self.inner { @@ -264,6 +266,7 @@ impl BTag { } } + #[cfg(feature = "wasm_test")] #[cfg(test)] fn tag(&self) -> &str { match &self.inner { @@ -274,6 +277,7 @@ impl BTag { } } +#[cfg(feature = "wasm_test")] #[cfg(test)] mod tests { use super::*; @@ -287,10 +291,8 @@ mod tests { use wasm_bindgen::JsCast; use web_sys::HtmlInputElement as InputElement; - #[cfg(feature = "wasm_test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); fn test_scope() -> AnyScope { diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index af152955daf..7d4007cbdc1 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -9,7 +9,7 @@ use gloo_utils::document; use web_sys::{Element, Text as TextNode}; /// The bundle implementation to [VText] -pub struct BText { +pub(super) struct BText { text: AttrValue, text_node: TextNode, } @@ -81,7 +81,7 @@ impl Reconcilable for VText { impl std::fmt::Debug for BText { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "BText {{ text: \"{}\" }}", self.text) + f.debug_struct("BText").field("text", &self.text).finish() } } diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index a1f0b596a14..6b07cc32d2b 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -5,7 +5,6 @@ //! In order to efficiently implement updates, and diffing, additional information has to be //! kept around. This information is carried in the bundle. -mod app_handle; mod bcomp; mod blist; mod bnode; @@ -13,139 +12,65 @@ mod bportal; mod bsuspense; mod btag; mod btext; +mod traits; +mod utils; -#[cfg(test)] -mod tests; +use gloo::utils::document; +use web_sys::{Element, Node}; -use self::bcomp::BComp; -use self::blist::BList; -use self::bnode::BNode; -use self::bportal::BPortal; -use self::bsuspense::BSuspense; -use self::btag::BTag; -use self::btext::BText; +use crate::html::AnyScope; +use crate::html::NodeRef; +use crate::virtual_dom::VNode; -pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped}; +use bcomp::BComp; +use blist::BList; +use bnode::BNode; +use bportal::BPortal; +use bsuspense::BSuspense; +use btag::BTag; +use btext::BText; +use traits::{DomBundle, Reconcilable}; +use utils::{insert_node, test_log}; -#[doc(hidden)] // Publically exported from crate::app_handle -pub use self::app_handle::AppHandle; #[doc(hidden)] // Publically exported from crate::events pub use self::btag::set_event_bubbling; -#[cfg(test)] -#[doc(hidden)] // Publically exported from crate::tests -pub use self::tests::layout_tests; -use crate::html::AnyScope; -use crate::NodeRef; -use web_sys::{Element, Node}; - -trait DomBundle { - /// Remove self from parent. - /// - /// Parent to detach is `true` if the parent element will also be detached. - fn detach(self, parent: &Element, parent_to_detach: bool); - - /// Move elements from one parent to another parent. - /// This is for example used by `VSuspense` to preserve component state without detaching - /// (which destroys component state). - fn shift(&self, next_parent: &Element, next_sibling: NodeRef); -} +/// A Bundle. +/// +/// Each component holds a bundle that represents a realised layout, designated by a VNode. +/// +/// This is not to be confused with [BComp], which represents a component in the position of a +/// bundle layout. +#[derive(Debug)] +pub(crate) struct Bundle(BNode); -/// This trait provides features to update a tree by calculating a difference against another tree. -trait Reconcilable { - type Bundle: DomBundle; - - /// Attach a virtual node to the DOM tree. - /// - /// Parameters: - /// - `parent_scope`: the parent `Scope` used for passing messages to the - /// parent `Component`. - /// - `parent`: the parent node in the DOM. - /// - `next_sibling`: to find where to put the node. - /// - /// Returns a reference to the newly inserted element. - fn attach( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle); - - /// Scoped diff apply to other tree. - /// - /// Virtual rendering for the node. It uses parent node and existing - /// children (virtual and DOM) to check the difference and apply patches to - /// the actual DOM representation. - /// - /// Parameters: - /// - `parent_scope`: the parent `Scope` used for passing messages to the - /// parent `Component`. - /// - `parent`: the parent node in the DOM. - /// - `next_sibling`: the next sibling, used to efficiently find where to - /// put the node. - /// - `bundle`: the node that this node will be replacing in the DOM. This - /// method will remove the `bundle` from the `parent` if it is of the wrong - /// kind, and otherwise reuse it. - /// - /// Returns a reference to the newly inserted element. - fn reconcile_node( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut BNode, - ) -> NodeRef; +impl Bundle { + /// Creates a new bundle. + pub fn new(parent: &Element, next_sibling: &NodeRef, node_ref: &NodeRef) -> Self { + let placeholder: Node = document().create_text_node("").into(); + insert_node(&placeholder, parent, next_sibling.get().as_ref()); + node_ref.set(Some(placeholder.clone())); + Self(BNode::Ref(placeholder)) + } - fn reconcile( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut Self::Bundle, - ) -> NodeRef; + /// Shifts the bundle into a different position. + pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.0.shift(next_parent, next_sibling); + } - /// Replace an existing bundle by attaching self and detaching the existing one - fn replace( - self, + /// Applies a virtual dom layout to current bundle. + pub fn reconcile( + &mut self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - bundle: &mut BNode, - ) -> NodeRef - where - Self: Sized, - Self::Bundle: Into, - { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - let ancestor = std::mem::replace(bundle, self_.into()); - ancestor.detach(parent, false); - self_ref + next_node: VNode, + ) -> NodeRef { + next_node.reconcile_node(parent_scope, parent, next_sibling, &mut self.0) } -} - -/// Insert a concrete [Node] into the DOM -fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { - match next_sibling { - Some(next_sibling) => parent - .insert_before(node, Some(next_sibling)) - .expect("failed to insert tag before next sibling"), - None => parent.append_child(node).expect("failed to append child"), - }; -} -#[cfg(all(test, feature = "wasm_test", verbose_tests))] -macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { - ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); - }; -} -#[cfg(not(all(test, feature = "wasm_test", verbose_tests)))] -macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { - // Only type-check the format expression, do not run any side effects - let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); }; - }; + /// Detaches current bundle. + pub fn detach(self, parent: &Element, parent_to_detach: bool) { + self.0.detach(parent, parent_to_detach); + } } -/// Log an operation during tests for debugging purposes -/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. -pub(self) use test_log; diff --git a/packages/yew/src/dom_bundle/tests/mod.rs b/packages/yew/src/dom_bundle/tests/mod.rs deleted file mode 100644 index 1208f4409c5..00000000000 --- a/packages/yew/src/dom_bundle/tests/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -pub mod layout_tests; - -use super::Reconcilable; - -use crate::virtual_dom::VNode; -use crate::{dom_bundle::BNode, html::AnyScope, NodeRef}; -use web_sys::Element; - -impl VNode { - fn reconcile_sequentially( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut Option, - ) -> NodeRef { - match bundle { - None => { - let (self_ref, node) = self.attach(parent_scope, parent, next_sibling); - *bundle = Some(node); - self_ref - } - Some(bundle) => self.reconcile_node(parent_scope, parent, next_sibling, bundle), - } - } -} diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs new file mode 100644 index 00000000000..cc6d93f88aa --- /dev/null +++ b/packages/yew/src/dom_bundle/traits.rs @@ -0,0 +1,88 @@ +use super::BNode; +use crate::html::AnyScope; +use crate::html::NodeRef; +use web_sys::Element; + +pub(super) trait DomBundle { + /// Remove self from parent. + /// + /// Parent to detach is `true` if the parent element will also be detached. + fn detach(self, parent: &Element, parent_to_detach: bool); + + /// Move elements from one parent to another parent. + /// This is for example used by `VSuspense` to preserve component state without detaching + /// (which destroys component state). + fn shift(&self, next_parent: &Element, next_sibling: NodeRef); +} + +/// This trait provides features to update a tree by calculating a difference against another tree. +pub(super) trait Reconcilable { + type Bundle: DomBundle; + + /// Attach a virtual node to the DOM tree. + /// + /// Parameters: + /// - `parent_scope`: the parent `Scope` used for passing messages to the + /// parent `Component`. + /// - `parent`: the parent node in the DOM. + /// - `next_sibling`: to find where to put the node. + /// + /// Returns a reference to the newly inserted element. + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle); + + /// Scoped diff apply to other tree. + /// + /// Virtual rendering for the node. It uses parent node and existing + /// children (virtual and DOM) to check the difference and apply patches to + /// the actual DOM representation. + /// + /// Parameters: + /// - `parent_scope`: the parent `Scope` used for passing messages to the + /// parent `Component`. + /// - `parent`: the parent node in the DOM. + /// - `next_sibling`: the next sibling, used to efficiently find where to + /// put the node. + /// - `bundle`: the node that this node will be replacing in the DOM. This + /// method will remove the `bundle` from the `parent` if it is of the wrong + /// kind, and otherwise reuse it. + /// + /// Returns a reference to the newly inserted element. + fn reconcile_node( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef; + + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut Self::Bundle, + ) -> NodeRef; + + /// Replace an existing bundle by attaching self and detaching the existing one + fn replace( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef + where + Self: Sized, + Self::Bundle: Into, + { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + let ancestor = std::mem::replace(bundle, self_.into()); + ancestor.detach(parent, false); + self_ref + } +} diff --git a/packages/yew/src/dom_bundle/utils.rs b/packages/yew/src/dom_bundle/utils.rs new file mode 100644 index 00000000000..e1385ad5a51 --- /dev/null +++ b/packages/yew/src/dom_bundle/utils.rs @@ -0,0 +1,28 @@ +use web_sys::{Element, Node}; + +/// Insert a concrete [Node] into the DOM +pub(super) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { + match next_sibling { + Some(next_sibling) => parent + .insert_before(node, Some(next_sibling)) + .expect("failed to insert tag before next sibling"), + None => parent.append_child(node).expect("failed to append child"), + }; +} + +#[cfg(all(test, feature = "wasm_test", verbose_tests))] +macro_rules! test_log { + ($fmt:literal, $($arg:expr),* $(,)?) => { + ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); + }; +} +#[cfg(not(all(test, feature = "wasm_test", verbose_tests)))] +macro_rules! test_log { + ($fmt:literal, $($arg:expr),* $(,)?) => { + // Only type-check the format expression, do not run any side effects + let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); }; + }; +} +/// Log an operation during tests for debugging purposes +/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. +pub(super) use test_log; diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index ed833c4d46e..e2e44885d74 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,16 +1,69 @@ //! Component lifecycle module use super::scope::{AnyScope, Scope}; -use super::BaseComponent; -use crate::dom_bundle::ComponentRenderState; -use crate::html::RenderError; +use super::{BaseComponent, ComponentId}; +use crate::html::{Html, RenderError}; use crate::scheduler::{self, Runnable, Shared}; -use crate::suspense::{Suspense, Suspension}; -use crate::{Callback, Context, HtmlResult, NodeRef}; +use crate::suspense::{BaseSuspense, Suspension}; +use crate::{Callback, Context, HtmlResult}; use std::any::Any; use std::rc::Rc; -pub struct CompStateInner +#[cfg(feature = "render")] +use crate::dom_bundle::Bundle; +#[cfg(feature = "render")] +use crate::html::NodeRef; +#[cfg(feature = "render")] +use web_sys::Element; + +pub(crate) enum ComponentRenderState { + #[cfg(feature = "render")] + Render { + bundle: Bundle, + parent: web_sys::Element, + next_sibling: NodeRef, + node_ref: NodeRef, + }, + + #[cfg(feature = "ssr")] + Ssr { + sender: Option>, + }, +} + +impl std::fmt::Debug for ComponentRenderState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(feature = "render")] + Self::Render { + ref bundle, + ref parent, + ref next_sibling, + ref node_ref, + } => f + .debug_struct("ComponentRenderState::Render") + .field("bundle", bundle) + .field("parent", parent) + .field("next_sibling", next_sibling) + .field("node_ref", node_ref) + .finish(), + + #[cfg(feature = "ssr")] + Self::Ssr { ref sender } => { + let sender_repr = match sender { + Some(_) => "Some(_)", + None => "None", + }; + + f.debug_struct("ComponentRenderState::Ssr") + .field("sender", &sender_repr) + .finish() + } + } + } +} + +struct CompStateInner where COMP: BaseComponent, { @@ -23,7 +76,7 @@ where /// /// Mostly a thin wrapper that passes the context to a component's lifecycle /// methods. -pub trait Stateful { +pub(crate) trait Stateful { fn view(&self) -> HtmlResult; fn rendered(&mut self, first_render: bool); fn destroy(&mut self); @@ -90,29 +143,27 @@ where } } -pub struct ComponentState { +pub(crate) struct ComponentState { pub(super) inner: Box, pub(super) render_state: ComponentRenderState, - node_ref: NodeRef, + + #[cfg(feature = "render")] has_rendered: bool, suspension: Option, - // Used for debug logging - #[cfg(debug_assertions)] - pub(crate) vcomp_id: usize, + pub(crate) comp_id: ComponentId, } impl ComponentState { pub(crate) fn new( initial_render_state: ComponentRenderState, - node_ref: NodeRef, scope: Scope, props: Rc, ) -> Self { #[cfg(debug_assertions)] - let vcomp_id = scope.vcomp_id; + let comp_id = scope.id; let context = Context { scope, props }; let inner = Box::new(CompStateInner { @@ -123,19 +174,28 @@ impl ComponentState { Self { inner, render_state: initial_render_state, - node_ref, suspension: None, + + #[cfg(feature = "render")] has_rendered: false, - #[cfg(debug_assertions)] - vcomp_id, + comp_id, } } + + pub(crate) fn downcast_comp_ref(&self) -> Option<&COMP> + where + COMP: BaseComponent + 'static, + { + self.inner + .as_any() + .downcast_ref::>() + .map(|m| &m.component) + } } -pub struct CreateRunner { +pub(crate) struct CreateRunner { pub initial_render_state: ComponentRenderState, - pub node_ref: NodeRef, pub props: Rc, pub scope: Scope, } @@ -145,11 +205,10 @@ impl Runnable for CreateRunner { let mut current_state = self.scope.state.borrow_mut(); if current_state.is_none() { #[cfg(debug_assertions)] - super::log_event(self.scope.vcomp_id, "create"); + super::log_event(self.scope.id, "create"); *current_state = Some(ComponentState::new( self.initial_render_state, - self.node_ref, self.scope.clone(), self.props, )); @@ -157,43 +216,91 @@ impl Runnable for CreateRunner { } } -pub enum UpdateEvent { +pub(crate) enum UpdateEvent { /// Drain messages for a component. Message, - /// Wraps properties, node ref, and next sibling for a component. + /// Wraps properties, node ref, and next sibling for a component + #[cfg(feature = "render")] Properties(Rc, NodeRef, NodeRef), + /// Shift Scope. + #[cfg(feature = "render")] + Shift(Element, NodeRef), } -pub struct UpdateRunner { +pub(crate) struct UpdateRunner { pub state: Shared>, pub event: UpdateEvent, } impl Runnable for UpdateRunner { fn run(self: Box) { - if let Some(mut state) = self.state.borrow_mut().as_mut() { + if let Some(state) = self.state.borrow_mut().as_mut() { let schedule_render = match self.event { UpdateEvent::Message => state.inner.flush_messages(), - UpdateEvent::Properties(props, node_ref, next_sibling) => { - // When components are updated, a new node ref could have been passed in - state.node_ref = node_ref; - // When components are updated, their siblings were likely also updated - state.render_state.reuse(next_sibling); - // Only trigger changed if props were changed - - state.inner.props_changed(props) + #[cfg(feature = "render")] + UpdateEvent::Properties(props, next_node_ref, next_sibling) => { + match state.render_state { + #[cfg(feature = "render")] + ComponentRenderState::Render { + ref mut node_ref, + next_sibling: ref mut current_next_sibling, + .. + } => { + // When components are updated, a new node ref could have been passed in + *node_ref = next_node_ref; + // When components are updated, their siblings were likely also updated + *current_next_sibling = next_sibling; + // Only trigger changed if props were changed + state.inner.props_changed(props) + } + + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { .. } => { + #[cfg(debug_assertions)] + panic!("properties do not change during SSR"); + + #[cfg(not(debug_assertions))] + false + } + } + } + + #[cfg(feature = "render")] + UpdateEvent::Shift(next_parent, next_sibling) => { + match state.render_state { + ComponentRenderState::Render { + ref bundle, + ref mut parent, + next_sibling: ref mut current_next_sibling, + .. + } => { + bundle.shift(&next_parent, next_sibling.clone()); + + *parent = next_parent; + *current_next_sibling = next_sibling; + } + + // Shifting is not possible during SSR. + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { .. } => { + #[cfg(debug_assertions)] + panic!("shifting is not possible during SSR"); + } + } + + false } }; #[cfg(debug_assertions)] super::log_event( - state.vcomp_id, + state.comp_id, format!("update(schedule_render={})", schedule_render), ); if schedule_render { scheduler::push_component_render( - self.state.as_ptr() as usize, + state.comp_id, RenderRunner { state: self.state.clone(), }, @@ -204,8 +311,10 @@ impl Runnable for UpdateRunner { } } -pub struct DestroyRunner { +pub(crate) struct DestroyRunner { pub state: Shared>, + + #[cfg(feature = "render")] pub parent_to_detach: bool, } @@ -213,16 +322,31 @@ impl Runnable for DestroyRunner { fn run(self: Box) { if let Some(mut state) = self.state.borrow_mut().take() { #[cfg(debug_assertions)] - super::log_event(state.vcomp_id, "destroy"); + super::log_event(state.comp_id, "destroy"); state.inner.destroy(); - state.render_state.detach(self.parent_to_detach); - state.node_ref.set(None); + + match state.render_state { + #[cfg(feature = "render")] + ComponentRenderState::Render { + bundle, + ref parent, + ref node_ref, + .. + } => { + bundle.detach(parent, self.parent_to_detach); + + node_ref.set(None); + } + + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { .. } => {} + } } } } -pub struct RenderRunner { +pub(crate) struct RenderRunner { pub state: Shared>, } @@ -230,123 +354,152 @@ impl Runnable for RenderRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] - super::log_event(state.vcomp_id, "render"); + super::log_event(state.comp_id, "render"); match state.inner.view() { - Ok(root) => { - // Currently not suspended, we remove any previous suspension and update - // normally. - if let Some(m) = state.suspension.take() { - let comp_scope = state.inner.any_scope(); + Ok(m) => self.render(state, m), + Err(RenderError::Suspended(m)) => self.suspend(state, m), + }; + } + } +} - let suspense_scope = comp_scope.find_parent_scope::().unwrap(); - let suspense = suspense_scope.get_component().unwrap(); +impl RenderRunner { + fn suspend(&self, state: &mut ComponentState, suspension: Suspension) { + // Currently suspended, we re-use previous root node and send + // suspension to parent element. + let shared_state = self.state.clone(); - suspense.resume(m); - } + let comp_id = state.comp_id; - let scope = state.inner.any_scope(); - let node = state.render_state.reconcile(root, &scope); - state.node_ref.link(node); - - if state.render_state.should_trigger_rendered() { - let first_render = !state.has_rendered; - state.has_rendered = true; - - scheduler::push_component_rendered( - self.state.as_ptr() as usize, - RenderedRunner { - state: self.state.clone(), - first_render, - }, - first_render, - ); - } - } + if suspension.resumed() { + // schedule a render immediately if suspension is resumed. - Err(RenderError::Suspended(m)) => { - // Currently suspended, we re-use previous root node and send - // suspension to parent element. - let shared_state = self.state.clone(); - - if m.resumed() { - // schedule a render immediately if suspension is resumed. - - scheduler::push_component_render( - shared_state.as_ptr() as usize, - RenderRunner { - state: shared_state.clone(), - }, - ); - } else { - // We schedule a render after current suspension is resumed. - - let comp_scope = state.inner.any_scope(); - - let suspense_scope = comp_scope - .find_parent_scope::() - .expect("To suspend rendering, a component is required."); - let suspense = suspense_scope.get_component().unwrap(); - - m.listen(Callback::from(move |_| { - scheduler::push_component_render( - shared_state.as_ptr() as usize, - RenderRunner { - state: shared_state.clone(), - }, - ); - scheduler::start(); - })); - - if let Some(ref last_m) = state.suspension { - if &m != last_m { - // We remove previous suspension from the suspense. - suspense.resume(last_m.clone()); - } - } - state.suspension = Some(m.clone()); + scheduler::push_component_render( + state.comp_id, + RenderRunner { + state: shared_state, + }, + ); + } else { + // We schedule a render after current suspension is resumed. + let comp_scope = state.inner.any_scope(); - suspense.suspend(m); - } + let suspense_scope = comp_scope + .find_parent_scope::() + .expect("To suspend rendering, a component is required."); + let suspense = suspense_scope.get_component().unwrap(); + + suspension.listen(Callback::from(move |_| { + scheduler::push_component_render( + comp_id, + RenderRunner { + state: shared_state.clone(), + }, + ); + scheduler::start(); + })); + + if let Some(ref last_suspension) = state.suspension { + if &suspension != last_suspension { + // We remove previous suspension from the suspense. + suspense.resume(last_suspension.clone()); } - }; + } + state.suspension = Some(suspension.clone()); + + suspense.suspend(suspension); } } -} -struct RenderedRunner { - state: Shared>, - first_render: bool, + fn render(&self, state: &mut ComponentState, new_root: Html) { + // Currently not suspended, we remove any previous suspension and update + // normally. + if let Some(m) = state.suspension.take() { + let comp_scope = state.inner.any_scope(); + + let suspense_scope = comp_scope.find_parent_scope::().unwrap(); + let suspense = suspense_scope.get_component().unwrap(); + + suspense.resume(m); + } + + match state.render_state { + #[cfg(feature = "render")] + ComponentRenderState::Render { + ref mut bundle, + ref parent, + ref next_sibling, + ref node_ref, + .. + } => { + let scope = state.inner.any_scope(); + let new_node_ref = bundle.reconcile(&scope, parent, next_sibling.clone(), new_root); + node_ref.link(new_node_ref); + + let first_render = !state.has_rendered; + state.has_rendered = true; + + scheduler::push_component_rendered( + state.comp_id, + RenderedRunner { + state: self.state.clone(), + first_render, + }, + first_render, + ); + } + + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { ref mut sender } => { + if let Some(tx) = sender.take() { + tx.send(new_root).unwrap(); + } + } + }; + } } -impl Runnable for RenderedRunner { - fn run(self: Box) { - if let Some(state) = self.state.borrow_mut().as_mut() { - #[cfg(debug_assertions)] - super::log_event(state.vcomp_id, "rendered"); +#[cfg(feature = "render")] +mod feat_render { + use super::*; - if state.suspension.is_none() { - state.inner.rendered(self.first_render); + pub(crate) struct RenderedRunner { + pub state: Shared>, + pub first_render: bool, + } + + impl Runnable for RenderedRunner { + fn run(self: Box) { + if let Some(state) = self.state.borrow_mut().as_mut() { + #[cfg(debug_assertions)] + super::super::log_event(state.comp_id, "rendered"); + + if state.suspension.is_none() { + state.inner.rendered(self.first_render); + } } } } } +#[cfg(feature = "render")] +use feat_render::*; + +#[cfg(feature = "wasm_test")] #[cfg(test)] mod tests { extern crate self as yew; - use crate::dom_bundle::ComponentRenderState; + use super::*; use crate::html; use crate::html::*; use crate::Properties; use std::cell::RefCell; use std::ops::Deref; use std::rc::Rc; - #[cfg(feature = "wasm_test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); #[derive(Clone, Properties, Default, PartialEq)] @@ -461,11 +614,10 @@ mod tests { let scope = Scope::::new(None); let el = document.create_element("div").unwrap(); let node_ref = NodeRef::default(); - let render_state = ComponentRenderState::new(el, NodeRef::default(), &node_ref); let lifecycle = props.lifecycle.clone(); lifecycle.borrow_mut().clear(); - scope.mount_in_place(render_state, node_ref, Rc::new(props)); + scope.mount_in_place(el, NodeRef::default(), node_ref, Rc::new(props)); crate::scheduler::start_now(); assert_eq!(&lifecycle.borrow_mut().deref()[..], expected); diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index b6efe5cc8b9..30adb2ea04c 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -1,6 +1,7 @@ //! Components wrapped with context including properties, state, and link mod children; +#[cfg(any(feature = "render", feature = "ssr"))] mod lifecycle; mod properties; mod scope; @@ -8,45 +9,71 @@ mod scope; use super::{Html, HtmlResult, IntoHtmlResult}; pub use children::*; pub use properties::*; +#[cfg(feature = "render")] +pub(crate) use scope::Scoped; pub use scope::{AnyScope, Scope, SendAsMessage}; use std::rc::Rc; -#[cfg(debug_assertions)] + use std::sync::atomic::{AtomicUsize, Ordering}; -#[cfg(debug_assertions)] -thread_local! { - static EVENT_HISTORY: std::cell::RefCell>> - = Default::default(); - static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); -} +/// A unique component ID. +/// +/// This type is provided to better distinguish between component IDs and the older pointer-based +/// component ids. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub(crate) struct ComponentId(usize); -/// Push [Component] event to lifecycle debugging registry -#[cfg(debug_assertions)] -pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) { - EVENT_HISTORY.with(|h| { - h.borrow_mut() - .entry(vcomp_id) - .or_default() - .push(event.to_string()) - }); -} +impl Default for ComponentId { + fn default() -> Self { + static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); -/// Get [Component] event log from lifecycle debugging registry -#[cfg(debug_assertions)] -#[allow(dead_code)] -pub(crate) fn get_event_log(vcomp_id: usize) -> Vec { - EVENT_HISTORY.with(|h| { - h.borrow() - .get(&vcomp_id) - .map(|l| (*l).clone()) - .unwrap_or_default() - }) + Self(COMP_ID_COUNTER.fetch_add(1, Ordering::SeqCst)) + } } -#[cfg(debug_assertions)] -pub(crate) fn next_id() -> usize { - COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed)) +#[cfg(any(feature = "render", feature = "ssr"))] +mod feat_render_ssr { + use super::*; + + impl ComponentId { + #[inline] + pub fn new() -> Self { + Self::default() + } + } + + #[cfg(debug_assertions)] + thread_local! { + static EVENT_HISTORY: std::cell::RefCell>> + = Default::default(); + } + + /// Push [Component] event to lifecycle debugging registry + #[cfg(debug_assertions)] + pub(crate) fn log_event(comp_id: ComponentId, event: impl ToString) { + EVENT_HISTORY.with(|h| { + h.borrow_mut() + .entry(comp_id) + .or_default() + .push(event.to_string()) + }); + } + + /// Get [Component] event log from lifecycle debugging registry + #[cfg(debug_assertions)] + #[allow(dead_code)] + pub(crate) fn get_event_log(comp_id: ComponentId) -> Vec { + EVENT_HISTORY.with(|h| { + h.borrow() + .get(&comp_id) + .map(|l| (*l).clone()) + .unwrap_or_default() + }) + } } +#[cfg(debug_assertions)] +#[cfg(any(feature = "render", feature = "ssr"))] +pub(crate) use feat_render_ssr::*; /// The [`Component`]'s context. This contains component's [`Scope`] and and props and /// is passed to every lifecycle method. diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 1456438ae6b..db6cd0fef3c 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -1,63 +1,21 @@ //! Component scope module -use super::{ - lifecycle::{ - CompStateInner, ComponentState, CreateRunner, DestroyRunner, RenderRunner, UpdateEvent, - UpdateRunner, - }, - BaseComponent, -}; +#[cfg(any(feature = "render", feature = "ssr"))] +use crate::scheduler::Shared; +#[cfg(any(feature = "render", feature = "ssr"))] +use std::cell::RefCell; + +#[cfg(any(feature = "render", feature = "ssr"))] +use super::lifecycle::{ComponentState, UpdateEvent, UpdateRunner}; +use super::{BaseComponent, ComponentId}; use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; -use crate::dom_bundle::{ComponentRenderState, Scoped}; use crate::html::IntoComponent; -use crate::html::NodeRef; -use crate::scheduler::{self, Shared}; use std::any::{Any, TypeId}; -use std::cell::{Ref, RefCell}; use std::marker::PhantomData; use std::ops::Deref; use std::rc::Rc; use std::{fmt, iter}; -use web_sys::Element; - -#[derive(Debug)] -pub(crate) struct MsgQueue(Shared>); - -impl MsgQueue { - pub fn new() -> Self { - MsgQueue(Rc::default()) - } - - pub fn push(&self, msg: Msg) -> usize { - let mut inner = self.0.borrow_mut(); - inner.push(msg); - - inner.len() - } - - pub fn append(&self, other: &mut Vec) -> usize { - let mut inner = self.0.borrow_mut(); - inner.append(other); - - inner.len() - } - - pub fn drain(&self) -> Vec { - let mut other_queue = Vec::new(); - let mut inner = self.0.borrow_mut(); - - std::mem::swap(&mut *inner, &mut other_queue); - - other_queue - } -} - -impl Clone for MsgQueue { - fn clone(&self) -> Self { - MsgQueue(self.0.clone()) - } -} /// Untyped scope used for accessing parent scope #[derive(Clone)] @@ -84,6 +42,7 @@ impl From> for AnyScope { } impl AnyScope { + #[cfg(feature = "render")] #[cfg(test)] pub(crate) fn test() -> Self { Self { @@ -142,53 +101,18 @@ impl AnyScope { } } -impl Scoped for Scope { - fn to_any(&self) -> AnyScope { - self.clone().into() - } - - fn render_state(&self) -> Option> { - let state_ref = self.state.borrow(); - - // check that component hasn't been destroyed - state_ref.as_ref()?; - - Some(Ref::map(state_ref, |state_ref| { - &state_ref.as_ref().unwrap().render_state - })) - } - - /// Process an event to destroy a component - fn destroy(self, parent_to_detach: bool) { - scheduler::push_component_destroy(DestroyRunner { - state: self.state, - parent_to_detach, - }); - // Not guaranteed to already have the scheduler started - scheduler::start(); - } - - fn destroy_boxed(self: Box, parent_to_detach: bool) { - self.destroy(parent_to_detach) - } - - fn shift_node(&self, parent: Element, next_sibling: NodeRef) { - let mut state_ref = self.state.borrow_mut(); - if let Some(render_state) = state_ref.as_mut() { - render_state.render_state.shift(parent, next_sibling) - } - } -} - /// A context which allows sending messages to a component. pub struct Scope { _marker: PhantomData, parent: Option>, + + #[cfg(any(feature = "render", feature = "ssr"))] pub(crate) pending_messages: MsgQueue, + + #[cfg(any(feature = "render", feature = "ssr"))] pub(crate) state: Shared>, - #[cfg(debug_assertions)] - pub(crate) vcomp_id: usize, + pub(crate) id: ComponentId, } impl fmt::Debug for Scope { @@ -201,12 +125,15 @@ impl Clone for Scope { fn clone(&self) -> Self { Scope { _marker: PhantomData, + + #[cfg(any(feature = "render", feature = "ssr"))] pending_messages: self.pending_messages.clone(), parent: self.parent.clone(), + + #[cfg(any(feature = "render", feature = "ssr"))] state: self.state.clone(), - #[cfg(debug_assertions)] - vcomp_id: self.vcomp_id, + id: self.id, } } } @@ -217,107 +144,6 @@ impl Scope { self.parent.as_deref() } - /// Returns the linked component if available - pub fn get_component(&self) -> Option + '_> { - self.state.try_borrow().ok().and_then(|state_ref| { - state_ref.as_ref()?; - Some(Ref::map(state_ref, |state| { - &state - .as_ref() - .unwrap() - .inner - .as_any() - .downcast_ref::>() - .unwrap() - .component - })) - }) - } - - /// Crate a scope with an optional parent scope - pub(crate) fn new(parent: Option) -> Self { - let parent = parent.map(Rc::new); - let state = Rc::new(RefCell::new(None)); - let pending_messages = MsgQueue::new(); - - Scope { - _marker: PhantomData, - pending_messages, - state, - parent, - - #[cfg(debug_assertions)] - vcomp_id: super::next_id(), - } - } - - /// Mounts a component with `props` to the specified `element` in the DOM. - pub(crate) fn mount_in_place( - &self, - initial_render_state: ComponentRenderState, - node_ref: NodeRef, - props: Rc, - ) { - scheduler::push_component_create( - CreateRunner { - initial_render_state, - node_ref, - props, - scope: self.clone(), - }, - RenderRunner { - state: self.state.clone(), - }, - ); - // Not guaranteed to already have the scheduler started - scheduler::start(); - } - - pub(crate) fn reuse( - &self, - props: Rc, - node_ref: NodeRef, - next_sibling: NodeRef, - ) { - #[cfg(debug_assertions)] - super::log_event(self.vcomp_id, "reuse"); - - self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling)); - } - - fn push_update(&self, event: UpdateEvent) { - scheduler::push_component_update(UpdateRunner { - state: self.state.clone(), - event, - }); - // Not guaranteed to already have the scheduler started - scheduler::start(); - } - - /// Send a message to the component. - pub fn send_message(&self, msg: T) - where - T: Into, - { - // We are the first message in queue, so we queue the update. - if self.pending_messages.push(msg.into()) == 1 { - self.push_update(UpdateEvent::Message); - } - } - - /// Send a batch of messages to the component. - /// - /// This is slightly more efficient than calling [`send_message`](Self::send_message) - /// in a loop. - pub fn send_message_batch(&self, mut messages: Vec) { - let msg_len = messages.len(); - - // The queue was empty, so we queue the update - if self.pending_messages.append(&mut messages) == msg_len { - self.push_update(UpdateEvent::Message); - } - } - /// Creates a `Callback` which will send a message to the linked /// component's update method when invoked. pub fn callback(&self, function: F) -> Callback @@ -363,29 +189,68 @@ impl Scope { &self, callback: Callback, ) -> Option<(T, ContextHandle)> { - self.to_any().context(callback) + AnyScope::from(self.clone()).context(callback) } } #[cfg(feature = "ssr")] mod feat_ssr { use super::*; + use crate::scheduler; use futures::channel::oneshot; + use crate::html::component::lifecycle::{ + ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, + }; + impl Scope { - pub(crate) async fn render_to_string(self, w: &mut String, props: Rc) { + pub(crate) async fn render_to_string( + self, + w: &mut String, + props: Rc, + hydratable: bool, + ) { let (tx, rx) = oneshot::channel(); - let initial_render_state = ComponentRenderState::new_ssr(tx); + let state = ComponentRenderState::Ssr { sender: Some(tx) }; + + scheduler::push_component_create( + self.id, + CreateRunner { + initial_render_state: state, + props, + scope: self.clone(), + }, + RenderRunner { + state: self.state.clone(), + }, + ); + scheduler::start(); + + if hydratable { + #[cfg(debug_assertions)] + w.push_str(&format!("", std::any::type_name::())); - self.mount_in_place(initial_render_state, NodeRef::default(), props); + #[cfg(not(debug_assertions))] + w.push_str(""); + } let html = rx.await.unwrap(); - let self_any_scope = self.to_any(); - html.render_to_string(w, &self_any_scope).await; + let self_any_scope = AnyScope::from(self.clone()); + html.render_to_string(w, &self_any_scope, hydratable).await; + + if hydratable { + #[cfg(debug_assertions)] + w.push_str(&format!("", std::any::type_name::())); + + #[cfg(not(debug_assertions))] + w.push_str(""); + } scheduler::push_component_destroy(DestroyRunner { state: self.state.clone(), + + #[cfg(feature = "render")] parent_to_detach: false, }); scheduler::start(); @@ -393,6 +258,262 @@ mod feat_ssr { } } +#[cfg(not(any(feature = "ssr", feature = "render")))] +mod feat_no_render_ssr { + use super::*; + + // Skeleton code to provide public methods when no renderer are enabled. + impl Scope { + /// Returns the linked component if available + pub fn get_component(&self) -> Option + '_> { + Option::<&COMP>::None + } + + /// Send a message to the component. + pub fn send_message(&self, _msg: T) + where + T: Into, + { + } + + /// Send a batch of messages to the component. + /// + /// This is slightly more efficient than calling [`send_message`](Self::send_message) + /// in a loop. + pub fn send_message_batch(&self, _messages: Vec) {} + } +} + +#[cfg(any(feature = "ssr", feature = "render"))] +mod feat_render_ssr { + use super::*; + use crate::scheduler::{self, Shared}; + use std::cell::Ref; + + #[derive(Debug)] + pub(crate) struct MsgQueue(Shared>); + + impl MsgQueue { + pub fn new() -> Self { + MsgQueue(Rc::default()) + } + + pub fn push(&self, msg: Msg) -> usize { + let mut inner = self.0.borrow_mut(); + inner.push(msg); + + inner.len() + } + + pub fn append(&self, other: &mut Vec) -> usize { + let mut inner = self.0.borrow_mut(); + inner.append(other); + + inner.len() + } + + pub fn drain(&self) -> Vec { + let mut other_queue = Vec::new(); + let mut inner = self.0.borrow_mut(); + + std::mem::swap(&mut *inner, &mut other_queue); + + other_queue + } + } + + impl Clone for MsgQueue { + fn clone(&self) -> Self { + MsgQueue(self.0.clone()) + } + } + + impl Scope { + /// Crate a scope with an optional parent scope + pub(crate) fn new(parent: Option) -> Self { + let parent = parent.map(Rc::new); + + let state = Rc::new(RefCell::new(None)); + + let pending_messages = MsgQueue::new(); + + Scope { + _marker: PhantomData, + + pending_messages, + + state, + parent, + + id: ComponentId::new(), + } + } + + /// Returns the linked component if available + pub fn get_component(&self) -> Option + '_> { + self.state.try_borrow().ok().and_then(|state_ref| { + state_ref.as_ref()?; + // TODO: Replace unwrap with Ref::filter_map once it becomes stable. + Some(Ref::map(state_ref, |state| { + state + .as_ref() + .and_then(|m| m.downcast_comp_ref::()) + .unwrap() + })) + }) + } + + pub(super) fn push_update(&self, event: UpdateEvent) { + scheduler::push_component_update(UpdateRunner { + state: self.state.clone(), + event, + }); + // Not guaranteed to already have the scheduler started + scheduler::start(); + } + + /// Send a message to the component. + pub fn send_message(&self, msg: T) + where + T: Into, + { + // We are the first message in queue, so we queue the update. + if self.pending_messages.push(msg.into()) == 1 { + self.push_update(UpdateEvent::Message); + } + } + + /// Send a batch of messages to the component. + /// + /// This is slightly more efficient than calling [`send_message`](Self::send_message) + /// in a loop. + pub fn send_message_batch(&self, mut messages: Vec) { + let msg_len = messages.len(); + + // The queue was empty, so we queue the update + if self.pending_messages.append(&mut messages) == msg_len { + self.push_update(UpdateEvent::Message); + } + } + } +} + +#[cfg(any(feature = "ssr", feature = "render"))] +pub(crate) use feat_render_ssr::*; + +#[cfg(feature = "render")] +mod feat_render { + use super::*; + use crate::dom_bundle::Bundle; + use crate::html::component::lifecycle::{ + ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, + }; + use crate::html::NodeRef; + use crate::scheduler; + use std::cell::Ref; + use web_sys::Element; + + impl Scope + where + COMP: BaseComponent, + { + /// Mounts a component with `props` to the specified `element` in the DOM. + pub(crate) fn mount_in_place( + &self, + parent: Element, + next_sibling: NodeRef, + node_ref: NodeRef, + props: Rc, + ) { + let bundle = Bundle::new(&parent, &next_sibling, &node_ref); + let state = ComponentRenderState::Render { + bundle, + node_ref, + parent, + next_sibling, + }; + + scheduler::push_component_create( + self.id, + CreateRunner { + initial_render_state: state, + props, + scope: self.clone(), + }, + RenderRunner { + state: self.state.clone(), + }, + ); + // Not guaranteed to already have the scheduler started + scheduler::start(); + } + + pub(crate) fn reuse( + &self, + props: Rc, + node_ref: NodeRef, + next_sibling: NodeRef, + ) { + #[cfg(debug_assertions)] + super::super::log_event(self.id, "reuse"); + + self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling)); + } + } + + pub(crate) trait Scoped { + fn to_any(&self) -> AnyScope; + /// Get the render state if it hasn't already been destroyed + fn render_state(&self) -> Option>; + /// Shift the node associated with this scope to a new place + fn shift_node(&self, parent: Element, next_sibling: NodeRef); + /// Process an event to destroy a component + fn destroy(self, parent_to_detach: bool); + fn destroy_boxed(self: Box, parent_to_detach: bool); + } + + impl Scoped for Scope { + fn to_any(&self) -> AnyScope { + self.clone().into() + } + + fn render_state(&self) -> Option> { + let state_ref = self.state.borrow(); + + // check that component hasn't been destroyed + state_ref.as_ref()?; + + Some(Ref::map(state_ref, |state_ref| { + &state_ref.as_ref().unwrap().render_state + })) + } + + /// Process an event to destroy a component + fn destroy(self, parent_to_detach: bool) { + scheduler::push_component_destroy(DestroyRunner { + state: self.state, + parent_to_detach, + }); + // Not guaranteed to already have the scheduler started + scheduler::start(); + } + + fn destroy_boxed(self: Box, parent_to_detach: bool) { + self.destroy(parent_to_detach) + } + + fn shift_node(&self, parent: Element, next_sibling: NodeRef) { + scheduler::push_component_update(UpdateRunner { + state: self.state.clone(), + event: UpdateEvent::Shift(parent, next_sibling), + }) + } + } +} + +#[cfg(feature = "render")] +pub(crate) use feat_render::*; + #[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] #[cfg(any(target_arch = "wasm32", feature = "tokio"))] mod feat_io { diff --git a/packages/yew/src/html/mod.rs b/packages/yew/src/html/mod.rs index 7422cb9eca9..befc6b76678 100644 --- a/packages/yew/src/html/mod.rs +++ b/packages/yew/src/html/mod.rs @@ -14,11 +14,11 @@ pub use error::*; pub use listener::*; use crate::sealed::Sealed; -use crate::virtual_dom::{VNode, VPortal}; +use crate::virtual_dom::VNode; use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::JsValue; -use web_sys::{Element, Node}; +use web_sys::Node; /// A type which expected as a result of `view` function implementation. pub type Html = VNode; @@ -125,63 +125,61 @@ impl NodeRef { node.map(Into::into).map(INTO::from) } - /// Wrap an existing `Node` in a `NodeRef` - pub(crate) fn new(node: Node) -> Self { - let node_ref = NodeRef::default(); - node_ref.set(Some(node)); - node_ref - } - /// Place a Node in a reference for later use pub(crate) fn set(&self, node: Option) { let mut this = self.0.borrow_mut(); this.node = node; this.link = None; } +} - /// Link a downstream `NodeRef` - pub(crate) fn link(&self, node_ref: Self) { - // Avoid circular references - if self == &node_ref { - return; +#[cfg(feature = "render")] +mod feat_render { + use super::*; + + impl NodeRef { + /// Reuse an existing `NodeRef` + pub(crate) fn reuse(&self, node_ref: Self) { + // Avoid circular references + if self == &node_ref { + return; + } + + let mut this = self.0.borrow_mut(); + let existing = node_ref.0.borrow(); + this.node = existing.node.clone(); + this.link = existing.link.clone(); } - let mut this = self.0.borrow_mut(); - this.node = None; - this.link = Some(node_ref); - } + /// Link a downstream `NodeRef` + pub(crate) fn link(&self, node_ref: Self) { + // Avoid circular references + if self == &node_ref { + return; + } - /// Reuse an existing `NodeRef` - pub(crate) fn reuse(&self, node_ref: Self) { - // Avoid circular references - if self == &node_ref { - return; + let mut this = self.0.borrow_mut(); + this.node = None; + this.link = Some(node_ref); } - let mut this = self.0.borrow_mut(); - let existing = node_ref.0.borrow(); - this.node = existing.node.clone(); - this.link = existing.link.clone(); + /// Wrap an existing `Node` in a `NodeRef` + pub(crate) fn new(node: Node) -> Self { + let node_ref = NodeRef::default(); + node_ref.set(Some(node)); + node_ref + } } } -/// Render children into a DOM node that exists outside the hierarchy of the parent -/// component. -/// ## Relevant examples -/// - [Portals](https://github.com/yewstack/yew/tree/master/examples/portals) -pub fn create_portal(child: Html, host: Element) -> Html { - VNode::VPortal(VPortal::new(child, host)) -} - +#[cfg(feature = "wasm_test")] #[cfg(test)] mod tests { use super::*; use gloo_utils::document; - #[cfg(feature = "wasm_test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); #[test] diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 9c3be9f316b..9635ad09fae 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -18,6 +18,8 @@ //! Server-Side Rendering should work on all targets when feature `ssr` is enabled. //! //! ### Supported Features: +//! - `render`: Enables Client-side Rendering support and [`Renderer`]. +//! Only enable this feature if you are making a Yew application (not a library). //! - `ssr`: Enables Server-side Rendering support and [`ServerRenderer`]. //! - `tokio`: Enables future-based APIs on non-wasm32 targets with tokio runtime. (You may want to //! enable this if your application uses future-based APIs and it does not compile / lint on @@ -67,7 +69,7 @@ //! //!# fn dont_execute() { //! fn main() { -//! yew::start_app::(); +//! yew::Renderer::::new().render(); //! } //!# } //! ``` @@ -84,8 +86,6 @@ #![recursion_limit = "512"] extern crate self as yew; -use std::{cell::Cell, panic::PanicInfo}; - /// This macro provides a convenient way to create [`Classes`]. /// /// The macro takes a list of items similar to the [`vec!`] macro and returns a [`Classes`] instance. @@ -265,10 +265,13 @@ pub mod macros { pub mod callback; pub mod context; +#[cfg_attr(documenting, doc(cfg(feature = "render")))] +#[cfg(feature = "render")] mod dom_bundle; pub mod functional; pub mod html; mod io_coop; +pub mod portal; pub mod scheduler; mod sealed; #[cfg(feature = "ssr")] @@ -278,16 +281,20 @@ pub mod utils; pub mod virtual_dom; #[cfg(feature = "ssr")] pub use server_renderer::*; +#[cfg(feature = "render")] +mod app_handle; +#[cfg(feature = "render")] +mod renderer; +#[cfg(feature = "render")] #[cfg(test)] -pub mod tests { - pub use crate::dom_bundle::layout_tests; -} +pub mod tests; /// The module that contains all events available in the framework. pub mod events { pub use crate::html::TargetCast; + #[cfg(feature = "render")] pub use crate::dom_bundle::set_event_bubbling; #[doc(no_inline)] @@ -297,95 +304,31 @@ pub mod events { }; } -pub use crate::dom_bundle::AppHandle; -use web_sys::Element; - -use crate::html::IntoComponent; - -thread_local! { - static PANIC_HOOK_IS_SET: Cell = Cell::new(false); -} - -/// Set a custom panic hook. -/// Unless a panic hook is set through this function, Yew will -/// overwrite any existing panic hook when one of the `start_app*` functions are called. -pub fn set_custom_panic_hook(hook: Box) + Sync + Send + 'static>) { - std::panic::set_hook(hook); - PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true)); -} - -fn set_default_panic_hook() { - if !PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.replace(true)) { - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - } -} - -/// The main entry point of a Yew application. -/// If you would like to pass props, use the `start_app_with_props_in_element` method. -pub fn start_app_in_element(element: Element) -> AppHandle -where - ICOMP: IntoComponent, - ICOMP::Properties: Default, -{ - start_app_with_props_in_element(element, ICOMP::Properties::default()) -} +#[cfg(feature = "render")] +pub use crate::app_handle::AppHandle; +#[cfg(feature = "render")] +pub use crate::renderer::{set_custom_panic_hook, Renderer}; -/// Starts an yew app mounted to the body of the document. -/// Alias to start_app_in_element(Body) -pub fn start_app() -> AppHandle -where - ICOMP: IntoComponent, - ICOMP::Properties: Default, -{ - start_app_with_props(ICOMP::Properties::default()) -} - -/// The main entry point of a Yew application. This function does the -/// same as `start_app_in_element(...)` but allows to start an Yew application with properties. -pub fn start_app_with_props_in_element( - element: Element, - props: ICOMP::Properties, -) -> AppHandle -where - ICOMP: IntoComponent, -{ - set_default_panic_hook(); - AppHandle::::mount_with_props(element, Rc::new(props)) -} - -/// The main entry point of a Yew application. -/// This function does the same as `start_app(...)` but allows to start an Yew application with properties. -pub fn start_app_with_props(props: ICOMP::Properties) -> AppHandle -where - ICOMP: IntoComponent, -{ - start_app_with_props_in_element( - gloo_utils::document() - .body() - .expect("no body node found") - .into(), - props, - ) -} - -/// The Yew Prelude -/// -/// The purpose of this module is to alleviate imports of many common types: -/// -/// ``` -/// # #![allow(unused_imports)] -/// use yew::prelude::*; -/// ``` pub mod prelude { + //! The Yew Prelude + //! + //! The purpose of this module is to alleviate imports of many common types: + //! + //! ``` + //! # #![allow(unused_imports)] + //! use yew::prelude::*; + //! ``` + #[cfg(feature = "render")] + pub use crate::app_handle::AppHandle; pub use crate::callback::Callback; pub use crate::context::{ContextHandle, ContextProvider}; - pub use crate::dom_bundle::AppHandle; pub use crate::events::*; pub use crate::html::{ - create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, - Html, HtmlResult, IntoComponent, NodeRef, Properties, + BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, Html, HtmlResult, + IntoComponent, NodeRef, Properties, }; pub use crate::macros::{classes, html, html_nested}; + pub use crate::portal::Portal; pub use crate::suspense::Suspense; pub use crate::virtual_dom::AttrValue; @@ -393,4 +336,3 @@ pub mod prelude { } pub use self::prelude::*; -use std::rc::Rc; diff --git a/packages/yew/src/portal.rs b/packages/yew/src/portal.rs new file mode 100644 index 00000000000..c452bed41f6 --- /dev/null +++ b/packages/yew/src/portal.rs @@ -0,0 +1,53 @@ +//! a module to create a "Portal" that attaches its children to the specified host element. + +use crate::functional::{use_effect_with_deps, use_state}; +use crate::html::{Children, Html, Properties}; +use crate::virtual_dom::{VNode, VPortal}; +use crate::{function_component, html}; + +use web_sys::Element; + +/// Properties of [Portal]. +#[derive(Debug, Properties, PartialEq, Clone)] +pub struct PortalProps { + /// Children to be rendered to the host element. + #[prop_or_default] + pub children: Children, + + /// The host element of the portal. + pub host: Element, +} + +/// Render children into a DOM node that exists outside the hierarchy of the parent +/// component. +/// ## Relevant examples +/// - [Portals](https://github.com/yewstack/yew/tree/master/examples/portals) +#[function_component] +pub fn Portal(props: &PortalProps) -> Html { + let rendered = use_state(|| false); + + // Delay render of portals until after first render. + // + // This automatically excludes portals during server-side rendering and defers + // it to be attached after hydration is completed. + { + let rendered = rendered.clone(); + use_effect_with_deps( + move |_| { + rendered.set(true); + + || {} + }, + (), + ); + } + + if *rendered { + let PortalProps { children, host } = props.clone(); + let children = html! {<>{children}}; + + VNode::VPortal(VPortal::new(children, host)) + } else { + Html::default() + } +} diff --git a/packages/yew/src/renderer.rs b/packages/yew/src/renderer.rs new file mode 100644 index 00000000000..e0e7e6e51cc --- /dev/null +++ b/packages/yew/src/renderer.rs @@ -0,0 +1,94 @@ +use std::cell::Cell; +use std::panic::PanicInfo; +use std::rc::Rc; + +use web_sys::Element; + +use crate::app_handle::AppHandle; +use crate::html::IntoComponent; + +thread_local! { + static PANIC_HOOK_IS_SET: Cell = Cell::new(false); +} + +/// Set a custom panic hook. +/// Unless a panic hook is set through this function, Yew will +/// overwrite any existing panic hook when one of the `start_app*` functions are called. +#[cfg_attr(documenting, doc(cfg(feature = "render")))] +pub fn set_custom_panic_hook(hook: Box) + Sync + Send + 'static>) { + std::panic::set_hook(hook); + PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true)); +} + +fn set_default_panic_hook() { + if !PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.replace(true)) { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + } +} + +/// The Yew Renderer. +/// +/// This is the main entry point of a Yew application. +#[derive(Debug)] +#[cfg_attr(documenting, doc(cfg(feature = "render")))] +#[must_use = "Renderer does nothing unless render() is called."] +pub struct Renderer +where + ICOMP: IntoComponent + 'static, +{ + root: Element, + props: ICOMP::Properties, +} + +impl Default for Renderer +where + ICOMP: IntoComponent + 'static, + ICOMP::Properties: Default, +{ + fn default() -> Self { + Self::with_props(Default::default()) + } +} + +impl Renderer +where + ICOMP: IntoComponent + 'static, + ICOMP::Properties: Default, +{ + /// Creates a [Renderer] that renders into the document body with default properties. + pub fn new() -> Self { + Self::default() + } + + /// Creates a [Renderer] that renders into a custom root with default properties. + pub fn with_root(root: Element) -> Self { + Self::with_root_and_props(root, Default::default()) + } +} + +impl Renderer +where + ICOMP: IntoComponent + 'static, +{ + /// Creates a [Renderer] that renders into the document body with custom properties. + pub fn with_props(props: ICOMP::Properties) -> Self { + Self::with_root_and_props( + gloo_utils::document() + .body() + .expect("no body node found") + .into(), + props, + ) + } + + /// Creates a [Renderer] that renders into a custom root with custom properties. + pub fn with_root_and_props(root: Element, props: ICOMP::Properties) -> Self { + Self { root, props } + } + + /// Renders the application. + pub fn render(self) -> AppHandle { + set_default_panic_hook(); + AppHandle::::mount_with_props(self.root, Rc::new(self.props)) + } +} diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index ef888d71d44..7d72777c3b1 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -1,9 +1,11 @@ //! This module contains a scheduler. use std::cell::RefCell; -use std::collections::{hash_map::Entry, HashMap, VecDeque}; +use std::collections::BTreeMap; use std::rc::Rc; +use crate::html::ComponentId; + /// Alias for Rc> pub type Shared = Rc>; @@ -24,12 +26,18 @@ struct Scheduler { destroy: Vec>, create: Vec>, update: Vec>, - render_first: VecDeque>, - render: RenderScheduler, - /// Stacks to ensure child calls are always before parent calls - rendered_first: Vec>, - rendered: RenderedScheduler, + /// The Binary Tree Map guarantees components with lower id (parent) is rendered first and + /// no more than 1 render can be scheduled before a component is rendered. + /// + /// Parent can destroy child components but not otherwise, we can save unnecessary render by + /// rendering parent first. + render_first: BTreeMap>, + render: BTreeMap>, + + /// Binary Tree Map to guarantee children rendered are always called before parent calls + rendered_first: BTreeMap>, + rendered: BTreeMap>, } /// Execute closure with a mutable reference to the scheduler @@ -54,50 +62,70 @@ pub fn push(runnable: Box) { start(); } -/// Push a component creation, first render and first rendered [Runnable]s to be executed -pub(crate) fn push_component_create( - create: impl Runnable + 'static, - first_render: impl Runnable + 'static, -) { - with(|s| { - s.create.push(Box::new(create)); - s.render_first.push_back(Box::new(first_render)); - }); -} +#[cfg(any(feature = "ssr", feature = "render"))] +mod feat_render_ssr { + use super::*; -/// Push a component destruction [Runnable] to be executed -pub(crate) fn push_component_destroy(runnable: impl Runnable + 'static) { - with(|s| s.destroy.push(Box::new(runnable))); -} + /// Push a component creation, first render and first rendered [Runnable]s to be executed + pub(crate) fn push_component_create( + component_id: ComponentId, + create: impl Runnable + 'static, + first_render: impl Runnable + 'static, + ) { + with(|s| { + s.create.push(Box::new(create)); + s.render_first.insert(component_id, Box::new(first_render)); + }); + } -/// Push a component render and rendered [Runnable]s to be executed -pub(crate) fn push_component_render(component_id: usize, render: impl Runnable + 'static) { - with(|s| { - s.render.schedule(component_id, Box::new(render)); - }); -} + /// Push a component destruction [Runnable] to be executed + pub(crate) fn push_component_destroy(runnable: impl Runnable + 'static) { + with(|s| s.destroy.push(Box::new(runnable))); + } -pub(crate) fn push_component_rendered( - component_id: usize, - rendered: impl Runnable + 'static, - first_render: bool, -) { - with(|s| { - let rendered = Box::new(rendered); - - if first_render { - s.rendered_first.push(rendered); - } else { - s.rendered.schedule(component_id, rendered); - } - }); + /// Push a component render and rendered [Runnable]s to be executed + pub(crate) fn push_component_render( + component_id: ComponentId, + render: impl Runnable + 'static, + ) { + with(|s| { + s.render.insert(component_id, Box::new(render)); + }); + } + + /// Push a component update [Runnable] to be executed + pub(crate) fn push_component_update(runnable: impl Runnable + 'static) { + with(|s| s.update.push(Box::new(runnable))); + } } -/// Push a component update [Runnable] to be executed -pub(crate) fn push_component_update(runnable: impl Runnable + 'static) { - with(|s| s.update.push(Box::new(runnable))); +#[cfg(any(feature = "ssr", feature = "render"))] +pub(crate) use feat_render_ssr::*; + +#[cfg(feature = "render")] +mod feat_render { + use super::*; + + pub(crate) fn push_component_rendered( + component_id: ComponentId, + rendered: impl Runnable + 'static, + first_render: bool, + ) { + with(|s| { + let rendered = Box::new(rendered); + + if first_render { + s.rendered_first.insert(component_id, rendered); + } else { + s.rendered.insert(component_id, rendered); + } + }); + } } +#[cfg(feature = "render")] +pub(crate) use feat_render::*; + /// Execute any pending [Runnable]s pub(crate) fn start_now() { thread_local! { @@ -170,12 +198,26 @@ impl Scheduler { // Create events can be batched, as they are typically just for object creation to_run.append(&mut self.create); + // These typically do nothing and don't spawn any other events - can be batched. + // Should be run only after all first renders have finished. + if !to_run.is_empty() { + return; + } + // First render must never be skipped and takes priority over main, because it may need // to init `NodeRef`s // // Should be processed one at time, because they can spawn more create and rendered events // for their children. - if let Some(r) = self.render_first.pop_front() { + // + // To be replaced with BTreeMap::pop_front once it is stable. + if let Some(r) = self + .render_first + .keys() + .next() + .cloned() + .and_then(|m| self.render_first.remove(&m)) + { to_run.push(r); } @@ -184,7 +226,12 @@ impl Scheduler { if !to_run.is_empty() { return; } - to_run.extend(self.rendered_first.drain(..).rev()); + + if !self.rendered_first.is_empty() { + let mut rendered_first = BTreeMap::new(); + std::mem::swap(&mut self.rendered_first, &mut rendered_first); + to_run.extend(rendered_first.into_values().rev()); + } // Updates are after the first render to ensure we always have the entire child tree // rendered, once an update is processed. @@ -202,7 +249,17 @@ impl Scheduler { if !to_run.is_empty() { return; } - if let Some(r) = self.render.pop() { + + // To be replaced with BTreeMap::pop_front once it is stable. + // Should be processed one at time, because they can spawn more create and rendered events + // for their children. + if let Some(r) = self + .render + .keys() + .next() + .cloned() + .and_then(|m| self.render.remove(&m)) + { to_run.push(r); } @@ -211,91 +268,13 @@ impl Scheduler { if !to_run.is_empty() { return; } - self.rendered.drain_into(to_run); - } -} - -/// Task to be executed for specific component -struct QueueTask { - /// Tasks in the queue to skip for this component - skip: usize, - - /// Runnable to execute - runnable: Box, -} - -/// Scheduler for non-first component renders with deduplication -#[derive(Default)] -struct RenderScheduler { - /// Task registry by component ID - tasks: HashMap, - /// Task queue by component ID - queue: VecDeque, -} + if !self.rendered.is_empty() { + let mut rendered = BTreeMap::new(); + std::mem::swap(&mut self.rendered, &mut rendered); -impl RenderScheduler { - /// Schedule render task execution - fn schedule(&mut self, component_id: usize, runnable: Box) { - self.queue.push_back(component_id); - match self.tasks.entry(component_id) { - Entry::Vacant(e) => { - e.insert(QueueTask { skip: 0, runnable }); - } - Entry::Occupied(mut e) => { - let v = e.get_mut(); - v.skip += 1; - - // Technically the 2 runners should be functionally identical, but might as well - // overwrite it for good measure, accounting for future changes. We have it here - // anyway. - v.runnable = runnable; - } - } - } - - /// Try to pop a task from the queue, if any - fn pop(&mut self) -> Option> { - while let Some(id) = self.queue.pop_front() { - match self.tasks.entry(id) { - Entry::Occupied(mut e) => { - let v = e.get_mut(); - if v.skip == 0 { - return Some(e.remove().runnable); - } - v.skip -= 1; - } - Entry::Vacant(_) => (), - } - } - None - } -} - -/// Deduplicating scheduler for component rendered calls with deduplication -#[derive(Default)] -struct RenderedScheduler { - /// Task registry by component ID - tasks: HashMap>, - - /// Task stack by component ID - stack: Vec, -} - -impl RenderedScheduler { - /// Schedule rendered task execution - fn schedule(&mut self, component_id: usize, runnable: Box) { - if self.tasks.insert(component_id, runnable).is_none() { - self.stack.push(component_id); - } - } - - /// Drain all tasks into `dst`, if any - fn drain_into(&mut self, dst: &mut Vec>) { - for id in self.stack.drain(..).rev() { - if let Some(t) = self.tasks.remove(&id) { - dst.push(t); - } + // Children rendered lifecycle happen before parents. + to_run.extend(rendered.into_values().rev()); } } } diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index b7277340693..d1c775abf9a 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -10,6 +10,7 @@ where ICOMP: IntoComponent, { props: ICOMP::Properties, + hydratable: bool, } impl Default for ServerRenderer @@ -39,7 +40,20 @@ where { /// Creates a [ServerRenderer] with custom properties. pub fn with_props(props: ICOMP::Properties) -> Self { - Self { props } + Self { + props, + hydratable: true, + } + } + + /// Sets whether an the rendered result is hydratable. + /// + /// Defaults to `true`. + /// + /// When this is sets to `true`, the rendered artifact will include assistive nodes + /// to assist with the hydration process. + pub fn set_hydratable(&mut self, val: bool) { + self.hydratable = val; } /// Renders Yew Application. @@ -54,6 +68,8 @@ where /// Renders Yew Application to a String. pub async fn render_to_string(self, w: &mut String) { let scope = Scope::<::Component>::new(None); - scope.render_to_string(w, self.props.into()).await; + scope + .render_to_string(w, self.props.into(), self.hydratable) + .await; } } diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs index 66e35422a9a..7e0f8a8d2e1 100644 --- a/packages/yew/src/suspense/component.rs +++ b/packages/yew/src/suspense/component.rs @@ -1,9 +1,4 @@ -use crate::html::{Children, Component, Context, Html, Properties, Scope}; -use crate::virtual_dom::{Key, VList, VNode, VSuspense}; - -use web_sys::Element; - -use super::Suspension; +use crate::html::{Children, Html, Properties}; #[derive(Properties, PartialEq, Debug, Clone)] pub struct SuspenseProps { @@ -12,94 +7,139 @@ pub struct SuspenseProps { #[prop_or_default] pub fallback: Html, - - #[prop_or_default] - pub key: Option, } -#[derive(Debug)] -pub enum SuspenseMsg { - Suspend(Suspension), - Resume(Suspension), -} +#[cfg(any(feature = "render", feature = "ssr"))] +mod feat_render_ssr { + use super::*; -/// Suspend rendering and show a fallback UI until the underlying task completes. -#[derive(Debug)] -pub struct Suspense { - link: Scope, - suspensions: Vec, - detached_parent: Option, -} + use crate::html::{Children, Component, Context, Html, Scope}; + use crate::suspense::Suspension; + use crate::virtual_dom::{VNode, VSuspense}; + use crate::{function_component, html}; -impl Component for Suspense { - type Properties = SuspenseProps; - type Message = SuspenseMsg; + #[derive(Properties, PartialEq, Debug, Clone)] + pub(crate) struct BaseSuspenseProps { + pub children: Children, - fn create(ctx: &Context) -> Self { - Self { - link: ctx.link().clone(), - suspensions: Vec::new(), + pub fallback: Option, + } - #[cfg(target_arch = "wasm32")] - detached_parent: web_sys::window() - .and_then(|m| m.document()) - .and_then(|m| m.create_element("div").ok()), + #[derive(Debug)] + pub(crate) enum BaseSuspenseMsg { + Suspend(Suspension), + Resume(Suspension), + } - #[cfg(not(target_arch = "wasm32"))] - detached_parent: None, - } + #[derive(Debug)] + pub(crate) struct BaseSuspense { + link: Scope, + suspensions: Vec, } - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - match msg { - Self::Message::Suspend(m) => { - if m.resumed() { - return false; - } + impl Component for BaseSuspense { + type Properties = BaseSuspenseProps; + type Message = BaseSuspenseMsg; + + fn create(ctx: &Context) -> Self { + Self { + link: ctx.link().clone(), + suspensions: Vec::new(), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Self::Message::Suspend(m) => { + assert!( + ctx.props().fallback.is_some(), + "You cannot suspend from a component rendered as a fallback." + ); + + if m.resumed() { + return false; + } - m.listen(self.link.callback(Self::Message::Resume)); + m.listen(self.link.callback(Self::Message::Resume)); - self.suspensions.push(m); + self.suspensions.push(m); - true + true + } + Self::Message::Resume(ref m) => { + let suspensions_len = self.suspensions.len(); + self.suspensions.retain(|n| m != n); + + suspensions_len != self.suspensions.len() + } } - Self::Message::Resume(ref m) => { - let suspensions_len = self.suspensions.len(); - self.suspensions.retain(|n| m != n); + } - suspensions_len != self.suspensions.len() + fn view(&self, ctx: &Context) -> Html { + let BaseSuspenseProps { children, fallback } = (*ctx.props()).clone(); + let children = html! {<>{children}}; + + match fallback { + Some(fallback) => { + let vsuspense = VSuspense::new( + children, + fallback, + !self.suspensions.is_empty(), + // We don't need to key this as the key will be applied to the component. + None, + ); + + VNode::from(vsuspense) + } + None => children, } } } - fn view(&self, ctx: &Context) -> Html { - let SuspenseProps { - children, - fallback: fallback_vnode, - key, - } = (*ctx.props()).clone(); - - let children_vnode = - VNode::from(VList::with_children(children.into_iter().collect(), None)); - - let vsuspense = VSuspense::new( - children_vnode, - fallback_vnode, - self.detached_parent.clone(), - !self.suspensions.is_empty(), - key, - ); - - VNode::from(vsuspense) + impl BaseSuspense { + pub(crate) fn suspend(&self, s: Suspension) { + self.link.send_message(BaseSuspenseMsg::Suspend(s)); + } + + pub(crate) fn resume(&self, s: Suspension) { + self.link.send_message(BaseSuspenseMsg::Resume(s)); + } } -} -impl Suspense { - pub(crate) fn suspend(&self, s: Suspension) { - self.link.send_message(SuspenseMsg::Suspend(s)); + /// Suspend rendering and show a fallback UI until the underlying task completes. + #[function_component] + pub fn Suspense(props: &SuspenseProps) -> Html { + let SuspenseProps { children, fallback } = props.clone(); + + let fallback = html! { + + {fallback} + + }; + + html! { + + {children} + + } } +} + +#[cfg(any(feature = "render", feature = "ssr"))] +pub use feat_render_ssr::*; - pub(crate) fn resume(&self, s: Suspension) { - self.link.send_message(SuspenseMsg::Resume(s)); +#[cfg(not(any(feature = "ssr", feature = "render")))] +mod feat_no_render_ssr { + use super::*; + + use crate::function_component; + + /// Suspend rendering and show a fallback UI until the underlying task completes. + #[function_component] + pub fn Suspense(_props: &SuspenseProps) -> Html { + Html::default() } } + +#[cfg(not(any(feature = "ssr", feature = "render")))] +pub use feat_no_render_ssr::*; diff --git a/packages/yew/src/suspense/mod.rs b/packages/yew/src/suspense/mod.rs index 617c263775f..e4b47165fa5 100644 --- a/packages/yew/src/suspense/mod.rs +++ b/packages/yew/src/suspense/mod.rs @@ -3,5 +3,7 @@ mod component; mod suspension; +#[cfg(any(feature = "render", feature = "ssr"))] +pub(crate) use component::BaseSuspense; pub use component::Suspense; pub use suspension::{Suspension, SuspensionHandle, SuspensionResult}; diff --git a/packages/yew/src/dom_bundle/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs similarity index 85% rename from packages/yew/src/dom_bundle/tests/layout_tests.rs rename to packages/yew/src/tests/layout_tests.rs index d0ef714a5fc..682f7f596c8 100644 --- a/packages/yew/src/dom_bundle/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -1,4 +1,4 @@ -use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; +use crate::dom_bundle::Bundle; use crate::html::AnyScope; use crate::scheduler; use crate::virtual_dom::VNode; @@ -51,7 +51,10 @@ pub fn diff_layouts(layouts: Vec>) { let vnode = layout.node.clone(); log!("Independently apply layout '{}'", layout.name); - let (_, mut bundle) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone()); + let node_ref = NodeRef::default(); + + let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref); + bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -65,12 +68,7 @@ pub fn diff_layouts(layouts: Vec>) { log!("Independently reapply layout '{}'", layout.name); - vnode.reconcile_node( - &parent_scope, - &parent_element, - next_sibling.clone(), - &mut bundle, - ); + bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -91,17 +89,19 @@ pub fn diff_layouts(layouts: Vec>) { } // Sequentially apply each layout - let mut bundle: Option = None; + let node_ref = NodeRef::default(); + let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref); for layout in layouts.iter() { let next_vnode = layout.node.clone(); log!("Sequentially apply layout '{}'", layout.name); - next_vnode.reconcile_sequentially( + bundle.reconcile( &parent_scope, &parent_element, next_sibling.clone(), - &mut bundle, + next_vnode, ); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -116,12 +116,13 @@ pub fn diff_layouts(layouts: Vec>) { let next_vnode = layout.node.clone(); log!("Sequentially detach layout '{}'", layout.name); - next_vnode.reconcile_sequentially( + bundle.reconcile( &parent_scope, &parent_element, next_sibling.clone(), - &mut bundle, + next_vnode, ); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -132,9 +133,7 @@ pub fn diff_layouts(layouts: Vec>) { } // Detach last layout - if let Some(bundle) = bundle { - bundle.detach(&parent_element, false); - } + bundle.detach(&parent_element, false); scheduler::start_now(); assert_eq!( parent_element.inner_html(), diff --git a/packages/yew/src/tests/mod.rs b/packages/yew/src/tests/mod.rs new file mode 100644 index 00000000000..7c8881b072f --- /dev/null +++ b/packages/yew/src/tests/mod.rs @@ -0,0 +1 @@ +pub mod layout_tests; diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 76d1e4f3a05..0f5d7be1e36 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -1,14 +1,21 @@ //! This module contains the implementation of a virtual component (`VComp`). use super::Key; -use crate::dom_bundle::{Mountable, PropsWrapper}; use crate::html::{BaseComponent, IntoComponent, NodeRef}; use std::any::TypeId; use std::fmt; use std::rc::Rc; -#[cfg(debug_assertions)] -thread_local! {} +#[cfg(any(feature = "ssr", feature = "render"))] +use crate::html::{AnyScope, Scope}; + +#[cfg(feature = "render")] +use crate::html::Scoped; +#[cfg(feature = "render")] +use web_sys::Element; + +#[cfg(feature = "ssr")] +use futures::future::{FutureExt, LocalBoxFuture}; /// A virtual component. pub struct VComp { @@ -40,6 +47,85 @@ impl Clone for VComp { } } +pub(crate) trait Mountable { + fn copy(&self) -> Box; + + #[cfg(feature = "render")] + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box; + + #[cfg(feature = "render")] + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); + + #[cfg(feature = "ssr")] + fn render_to_string<'a>( + &'a self, + w: &'a mut String, + parent_scope: &'a AnyScope, + hydratable: bool, + ) -> LocalBoxFuture<'a, ()>; +} + +pub(crate) struct PropsWrapper { + props: Rc, +} + +impl PropsWrapper { + pub fn new(props: Rc) -> Self { + Self { props } + } +} + +impl Mountable for PropsWrapper { + fn copy(&self) -> Box { + let wrapper: PropsWrapper = PropsWrapper { + props: Rc::clone(&self.props), + }; + Box::new(wrapper) + } + + #[cfg(feature = "render")] + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + scope.mount_in_place(parent, next_sibling, node_ref, self.props); + + Box::new(scope) + } + + #[cfg(feature = "render")] + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { + let scope: Scope = scope.to_any().downcast::(); + scope.reuse(self.props, node_ref, next_sibling); + } + + #[cfg(feature = "ssr")] + fn render_to_string<'a>( + &'a self, + w: &'a mut String, + parent_scope: &'a AnyScope, + hydratable: bool, + ) -> LocalBoxFuture<'a, ()> { + async move { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + scope + .render_to_string(w, self.props.clone(), hydratable) + .await; + } + .boxed_local() + } +} + /// A virtual child component. pub struct VChild { /// The component properties @@ -124,10 +210,15 @@ mod feat_ssr { use crate::html::AnyScope; impl VComp { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { self.mountable .as_ref() - .render_to_string(w, parent_scope) + .render_to_string(w, parent_scope, hydratable) .await; } } @@ -163,7 +254,8 @@ mod ssr_tests { } } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 9304f00d2f6..4fa5eb8ac48 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -90,12 +90,17 @@ mod feat_ssr { use crate::html::AnyScope; impl VList { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { // Concurrently render all children. for fragment in futures::future::join_all(self.children.iter().map(|m| async move { let mut w = String::new(); - m.render_to_string(&mut w, parent_scope).await; + m.render_to_string(&mut w, parent_scope, hydratable).await; w })) @@ -123,7 +128,8 @@ mod ssr_tests { html! {
{"Hello "}{s}{"!"}
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -153,7 +159,8 @@ mod ssr_tests { } } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 975f3cb84db..72ce6faface 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -157,13 +157,20 @@ mod feat_ssr { &'a self, w: &'a mut String, parent_scope: &'a AnyScope, + hydratable: bool, ) -> LocalBoxFuture<'a, ()> { async move { match self { - VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope).await, - VNode::VText(vtext) => vtext.render_to_string(w).await, - VNode::VComp(vcomp) => vcomp.render_to_string(w, parent_scope).await, - VNode::VList(vlist) => vlist.render_to_string(w, parent_scope).await, + VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope, hydratable).await, + VNode::VText(vtext) => { + vtext.render_to_string(w, parent_scope, hydratable).await + } + VNode::VComp(vcomp) => { + vcomp.render_to_string(w, parent_scope, hydratable).await + } + VNode::VList(vlist) => { + vlist.render_to_string(w, parent_scope, hydratable).await + } // We are pretty safe here as it's not possible to get a web_sys::Node without DOM // support in the first place. // @@ -175,7 +182,9 @@ mod feat_ssr { // Portals are not rendered. VNode::VPortal(_) => {} VNode::VSuspense(vsuspense) => { - vsuspense.render_to_string(w, parent_scope).await + vsuspense + .render_to_string(w, parent_scope, hydratable) + .await } } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 690b6b94bee..aab55f0662b 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -1,5 +1,4 @@ use super::{Key, VNode}; -use web_sys::Element; /// This struct represents a suspendable DOM fragment. #[derive(Clone, Debug, PartialEq)] @@ -8,8 +7,6 @@ pub struct VSuspense { pub(crate) children: Box, /// Fallback nodes when suspended. pub(crate) fallback: Box, - /// The element to attach to when children is not attached to DOM - pub(crate) detached_parent: Option, /// Whether the current status is suspended. pub(crate) suspended: bool, /// The Key. @@ -17,17 +14,10 @@ pub struct VSuspense { } impl VSuspense { - pub(crate) fn new( - children: VNode, - fallback: VNode, - detached_parent: Option, - suspended: bool, - key: Option, - ) -> Self { + pub fn new(children: VNode, fallback: VNode, suspended: bool, key: Option) -> Self { Self { children: children.into(), fallback: fallback.into(), - detached_parent, suspended, key, } @@ -40,9 +30,24 @@ mod feat_ssr { use crate::html::AnyScope; impl VSuspense { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { + if hydratable { + w.push_str(""); + } + // always render children on the server side. - self.children.render_to_string(w, parent_scope).await; + self.children + .render_to_string(w, parent_scope, hydratable) + .await; + + if hydratable { + w.push_str(""); + } } } } @@ -130,7 +135,8 @@ mod ssr_tests { let s = local .run_until(async move { - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); renderer.render().await }) diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index ed9debd0dba..91bec8f1cef 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -418,8 +418,19 @@ mod feat_ssr { use crate::{html::AnyScope, virtual_dom::VText}; use std::fmt::Write; + // Elements that cannot have any child elements. + static VOID_ELEMENTS: &[&str; 14] = &[ + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", + "source", "track", "wbr", + ]; + impl VTag { - pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { + pub(crate) async fn render_to_string( + &self, + w: &mut String, + parent_scope: &AnyScope, + hydratable: bool, + ) { write!(w, "<{}", self.tag()).unwrap(); let write_attr = |w: &mut String, name: &str, val: Option<&str>| { @@ -450,7 +461,9 @@ mod feat_ssr { VTagInner::Input(_) => {} VTagInner::Textarea { .. } => { if let Some(m) = self.value() { - VText::new(m.to_owned()).render_to_string(w).await; + VText::new(m.to_owned()) + .render_to_string(w, parent_scope, hydratable) + .await; } w.push_str(""); @@ -460,9 +473,17 @@ mod feat_ssr { ref children, .. } => { - children.render_to_string(w, parent_scope).await; - - write!(w, "", tag).unwrap(); + if !VOID_ELEMENTS.contains(&tag.as_ref()) { + // We don't write children of void elements nor closing tags. + children.render_to_string(w, parent_scope, hydratable).await; + + write!(w, "", tag).unwrap(); + } else { + #[cfg(debug_assertions)] + { + assert!(children.is_empty(), "{} cannot have any children!", tag); + } + } } } } @@ -483,7 +504,8 @@ mod ssr_tests { html! {
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -497,7 +519,8 @@ mod ssr_tests { html! {
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -511,7 +534,8 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -525,7 +549,8 @@ mod ssr_tests { html! {
{"Hello!"}
} } - let renderer = ServerRenderer::::new(); + let mut renderer = ServerRenderer::::new(); + renderer.set_hydratable(false); let s = renderer.render().await; @@ -539,7 +564,8 @@ mod ssr_tests { html! { +
+ +
+
+ }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait..."}
}; + + html! { +
+ + + +
+ } + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + sleep(Duration::from_millis(10)).await; + + let result = obtain_result(); + + assert_eq!( + result.as_str(), + r#"
"# + ); + gloo_utils::document() + .query_selector(".take-a-break") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + sleep(Duration::from_millis(10)).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...
"); + + sleep(Duration::from_millis(50)).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); +} + +#[wasm_bindgen_test] +async fn hydration_nested_suspense_works() { + #[derive(PartialEq)] + pub struct SleepState { + s: Suspension, + } + + impl SleepState { + fn new() -> Self { + let (s, handle) = Suspension::new(); + + spawn_local(async move { + sleep(Duration::from_millis(50)).await; + + handle.resume(); + }); + + Self { s } + } + } + + impl Reducible for SleepState { + type Action = (); + + fn reduce(self: Rc, _action: Self::Action) -> Rc { + Self::new().into() + } + } + + #[hook] + pub fn use_sleep() -> SuspensionResult> { + let sleep_state = use_reducer(SleepState::new); + + if sleep_state.s.resumed() { + Ok(Rc::new(move || sleep_state.dispatch(()))) + } else { + Err(sleep_state.s.clone()) + } + } + + #[function_component(InnerContent)] + fn inner_content() -> HtmlResult { + let resleep = use_sleep()?; + + let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + + Ok(html! { +
+
+ +
+
+ }) + } + + #[function_component(Content)] + fn content() -> HtmlResult { + let resleep = use_sleep()?; + + let fallback = html! {
{"wait...(inner)"}
}; + + let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + + Ok(html! { +
+
+ +
+ + + +
+ }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait...(outer)"}
}; + + html! { +
+ + + +
+ } + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::ZERO).await; + + Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + // outer suspense is hydrating... + sleep(Duration::from_millis(10)).await; + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + sleep(Duration::from_millis(50)).await; + + // inner suspense is hydrating... + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + sleep(Duration::from_millis(50)).await; + + // hydrated. + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + gloo_utils::document() + .query_selector(".take-a-break") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + sleep(Duration::from_millis(10)).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...(outer)
"); + + sleep(Duration::from_millis(50)).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + gloo_utils::document() + .query_selector(".take-a-break2") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + sleep(Duration::from_millis(10)).await; + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
wait...(inner)
"# + ); + + sleep(Duration::from_millis(50)).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); +} From b1377bdd1dbdeb6369cf864313bebd8eeb93a738 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 19 Mar 2022 08:57:11 +0900 Subject: [PATCH 11/36] Fix comp_id. --- packages/yew/src/html/component/lifecycle.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 921b5700f9d..d9be184dc7a 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -186,7 +186,6 @@ impl ComponentState { scope: Scope, props: Rc, ) -> Self { - #[cfg(debug_assertions)] let comp_id = scope.id; let context = Context { scope, props }; From 09e42750c080a20fa83577fc48cdeea28c1acd74 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Tue, 22 Mar 2022 22:40:11 +0900 Subject: [PATCH 12/36] Move some code away from generics. --- examples/function_router/src/main.rs | 2 +- packages/yew/src/functional/mod.rs | 125 ++++++++++++++++----------- 2 files changed, 74 insertions(+), 53 deletions(-) diff --git a/examples/function_router/src/main.rs b/examples/function_router/src/main.rs index ee2be1e7df0..150608e0df4 100644 --- a/examples/function_router/src/main.rs +++ b/examples/function_router/src/main.rs @@ -9,6 +9,6 @@ pub use app::*; fn main() { #[cfg(target_arch = "wasm32")] wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); - #[cfg(feature = "render")] + #[cfg(feature = "csr")] yew::Renderer::::new().render(); } diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 35226a536d0..03c2196b569 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -26,12 +26,13 @@ use std::cell::RefCell; use std::fmt; use std::rc::Rc; +use wasm_bindgen::prelude::*; + mod hooks; pub use hooks::*; -use crate::html::Context; - use crate::html::sealed::SealedBaseComponent; +use crate::html::Context; /// This attribute creates a function component from a normal Rust function. /// @@ -85,6 +86,19 @@ pub struct HookContext { } impl HookContext { + fn new(scope: AnyScope, re_render: ReRender) -> RefCell { + RefCell::new(HookContext { + effects: Vec::new(), + scope, + re_render, + states: Vec::new(), + + counter: 0, + #[cfg(debug_assertions)] + total_hook_counter: None, + }) + } + pub(crate) fn next_state(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc where T: 'static, @@ -103,7 +117,7 @@ impl HookContext { } }; - state.downcast().unwrap() + state.downcast().unwrap_throw() } pub(crate) fn next_effect(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc @@ -120,6 +134,49 @@ impl HookContext { t } + + #[inline(always)] + fn prepare_run(&mut self) { + self.counter = 0; + } + + #[cfg(debug_assertions)] + fn assert_hook_context(&mut self, render_ok: bool) { + // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot + // detect early return (as the return can be Err(_), Suspension). + if render_ok { + match self.total_hook_counter { + Some(m) => { + if m != self.counter { + panic!("Hooks are called conditionally."); + } + } + None => { + self.total_hook_counter = Some(self.counter); + } + } + } else if let Some(m) = self.total_hook_counter { + // Suspended Components can have less hooks called when suspended, but not more. + if m < self.counter { + panic!("Hooks are called conditionally."); + } + } + } + + fn run_effects(&self) { + for effect in self.effects.iter() { + effect.rendered(); + } + } + + fn drain_states(&mut self) { + // We clear the effects as these are also references to states. + self.effects.clear(); + + for state in self.states.drain(..) { + drop(state); + } + } } impl fmt::Debug for HookContext { @@ -167,21 +224,14 @@ where fn create(ctx: &Context) -> Self { let scope = AnyScope::from(ctx.link().clone()); + let re_render = { + let link = ctx.link().clone(); + Rc::new(move || link.send_message(())) + }; + Self { _never: std::marker::PhantomData::default(), - hook_ctx: RefCell::new(HookContext { - effects: Vec::new(), - scope, - re_render: { - let link = ctx.link().clone(); - Rc::new(move || link.send_message(())) - }, - states: Vec::new(), - - counter: 0, - #[cfg(debug_assertions)] - total_hook_counter: None, - }), + hook_ctx: HookContext::new(scope, re_render), } } @@ -195,56 +245,27 @@ where fn view(&self, ctx: &Context) -> HtmlResult { let props = ctx.props(); - let mut ctx = self.hook_ctx.borrow_mut(); - ctx.counter = 0; + let mut hook_ctx = self.hook_ctx.borrow_mut(); + + hook_ctx.prepare_run(); #[allow(clippy::let_and_return)] - let result = T::run(&mut *ctx, props); + let result = T::run(&mut *hook_ctx, props); #[cfg(debug_assertions)] - { - // Procedural Macros can catch most conditionally called hooks at compile time, but it cannot - // detect early return (as the return can be Err(_), Suspension). - if result.is_err() { - if let Some(m) = ctx.total_hook_counter { - // Suspended Components can have less hooks called when suspended, but not more. - if m < ctx.counter { - panic!("Hooks are called conditionally."); - } - } - } else { - match ctx.total_hook_counter { - Some(m) => { - if m != ctx.counter { - panic!("Hooks are called conditionally."); - } - } - None => { - ctx.total_hook_counter = Some(ctx.counter); - } - } - } - } + hook_ctx.assert_hook_context(result.is_ok()); result } fn rendered(&mut self, _ctx: &Context, _first_render: bool) { let hook_ctx = self.hook_ctx.borrow(); - - for effect in hook_ctx.effects.iter() { - effect.rendered(); - } + hook_ctx.run_effects(); } fn destroy(&mut self, _ctx: &Context) { let mut hook_ctx = self.hook_ctx.borrow_mut(); - // We clear the effects as these are also references to states. - hook_ctx.effects.clear(); - - for state in hook_ctx.states.drain(..) { - drop(state); - } + hook_ctx.drain_states(); } } From 4a653c8283151e1b33622e680402b23982d55b9a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 11:59:09 +0900 Subject: [PATCH 13/36] Fix everything. --- packages/yew/src/app_handle.rs | 16 +++++--- packages/yew/src/dom_bundle/bcomp.rs | 10 ++++- packages/yew/src/dom_bundle/blist.rs | 3 +- packages/yew/src/dom_bundle/bnode.rs | 12 +++--- packages/yew/src/dom_bundle/bsuspense.rs | 14 +++---- packages/yew/src/dom_bundle/btag/mod.rs | 11 ++--- packages/yew/src/dom_bundle/btext.rs | 2 + packages/yew/src/dom_bundle/fragment.rs | 31 ++++++++------- packages/yew/src/dom_bundle/mod.rs | 29 +++++--------- packages/yew/src/dom_bundle/traits.rs | 2 +- packages/yew/src/html/component/lifecycle.rs | 42 +++++++++++--------- packages/yew/src/html/component/scope.rs | 25 +++++++----- packages/yew/src/lib.rs | 6 +-- packages/yew/src/virtual_dom/vcomp.rs | 12 +++--- 14 files changed, 115 insertions(+), 100 deletions(-) diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 3650e789147..b741bec52a9 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -75,20 +75,26 @@ mod feat_hydration { where ICOMP: IntoComponent, { - pub(crate) fn hydrate_with_props(element: Element, props: Rc) -> Self { + pub(crate) fn hydrate_with_props(host: Element, props: Rc) -> Self { let app = Self { scope: Scope::new(None), }; - let mut fragment = Fragment::collect_children(&element); + let mut fragment = Fragment::collect_children(&host); + let hosting_root = BSubtree::create_root(&host); - app.scope - .hydrate_in_place(element.clone(), &mut fragment, NodeRef::default(), props); + app.scope.hydrate_in_place( + hosting_root, + host.clone(), + &mut fragment, + NodeRef::default(), + props, + ); // We remove all remaining nodes, this mimics the clear_element behaviour in // mount_with_props. for node in fragment.iter() { - element.remove_child(node).unwrap(); + host.remove_child(node).unwrap(); } app diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index fb29389c846..b47c24ff579 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -128,6 +128,7 @@ mod feat_hydration { impl Hydratable for VComp { fn hydrate( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, @@ -139,8 +140,13 @@ mod feat_hydration { key, } = self; - let scoped = - mountable.hydrate(parent_scope, parent.clone(), fragment, node_ref.clone()); + let scoped = mountable.hydrate( + root.clone(), + parent_scope, + parent.clone(), + fragment, + node_ref.clone(), + ); ( node_ref.clone(), diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 963547d8535..ce6c0878d28 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -462,6 +462,7 @@ mod feat_hydration { impl Hydratable for VList { fn hydrate( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, @@ -470,7 +471,7 @@ mod feat_hydration { let mut children = Vec::with_capacity(self.children.len()); for (index, child) in self.children.into_iter().enumerate() { - let (child_node_ref, child) = child.hydrate(parent_scope, parent, fragment); + let (child_node_ref, child) = child.hydrate(root, parent_scope, parent, fragment); if index == 0 { node_ref.reuse(child_node_ref); diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 3f3f5b5734f..8df25ea1602 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -242,25 +242,26 @@ mod feat_hydration { impl Hydratable for VNode { fn hydrate( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, ) -> (NodeRef, Self::Bundle) { match self { VNode::VTag(vtag) => { - let (node_ref, tag) = vtag.hydrate(parent_scope, parent, fragment); + let (node_ref, tag) = vtag.hydrate(root, parent_scope, parent, fragment); (node_ref, tag.into()) } VNode::VText(vtext) => { - let (node_ref, text) = vtext.hydrate(parent_scope, parent, fragment); + let (node_ref, text) = vtext.hydrate(root, parent_scope, parent, fragment); (node_ref, text.into()) } VNode::VComp(vcomp) => { - let (node_ref, comp) = vcomp.hydrate(parent_scope, parent, fragment); + let (node_ref, comp) = vcomp.hydrate(root, parent_scope, parent, fragment); (node_ref, comp.into()) } VNode::VList(vlist) => { - let (node_ref, list) = vlist.hydrate(parent_scope, parent, fragment); + let (node_ref, list) = vlist.hydrate(root, parent_scope, parent, fragment); (node_ref, list.into()) } // You cannot hydrate a VRef. @@ -272,7 +273,8 @@ mod feat_hydration { panic!("VPortal is not hydratable. Try to create your portal with the component.") } VNode::VSuspense(vsuspense) => { - let (node_ref, suspense) = vsuspense.hydrate(parent_scope, parent, fragment); + let (node_ref, suspense) = + vsuspense.hydrate(root, parent_scope, parent, fragment); (node_ref, suspense.into()) } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 30ce04608ed..241f8688176 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -42,12 +42,12 @@ impl ReconcileTarget for BSuspense { Some(m) => { match m { Fallback::Bundle(bundle) => { - bundle.detach(parent, parent_to_detach); + bundle.detach(root, parent, parent_to_detach); } #[cfg(feature = "hydration")] Fallback::Fragment(fragment) => { - fragment.detach(parent, parent_to_detach); + fragment.detach(root, parent, parent_to_detach); } } @@ -177,7 +177,7 @@ impl Reconcilable for VSuspense { match fallback { Fallback::Bundle(bundle) => { - fallback.reconcile_node(root, parent_scope, parent, next_sibling, bundle) + vfallback.reconcile_node(root, parent_scope, parent, next_sibling, bundle) } #[cfg(feature = "hydration")] Fallback::Fragment(fragment) => { @@ -223,11 +223,6 @@ impl Reconcilable for VSuspense { unreachable!() } }; - suspense - .fallback_bundle - .take() - .unwrap() // We just matched Some(_) - .detach(root, parent, false); children_bundle.shift(parent, next_sibling.clone()); children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle) @@ -245,6 +240,7 @@ mod feat_hydration { impl Hydratable for VSuspense { fn hydrate( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, @@ -267,7 +263,7 @@ mod feat_hydration { let (_, children_bundle) = self.children - .hydrate(parent_scope, &detached_parent, &mut nodes); + .hydrate(root, parent_scope, &detached_parent, &mut nodes); // We trim all leading text nodes before checking as it's likely these are whitespaces. nodes.trim_start_text_nodes(&detached_parent); diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index df09caf7b03..5f995bb99b2 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -297,6 +297,7 @@ mod feat_hydration { impl Hydratable for VTag { fn hydrate( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, @@ -335,23 +336,23 @@ mod feat_hydration { ); // We simply registers listeners and updates all attributes. - let attributes = attributes.apply(&el); - let listeners = listeners.apply(&el); + let attributes = attributes.apply(root, &el); + let listeners = listeners.apply(root, &el); // For input and textarea elements, we update their value anyways. let inner = match inner { VTagInner::Input(f) => { - let f = f.apply(el.unchecked_ref()); + let f = f.apply(root, el.unchecked_ref()); BTagInner::Input(f) } VTagInner::Textarea { value } => { - let value = value.apply(el.unchecked_ref()); + let value = value.apply(root, el.unchecked_ref()); BTagInner::Textarea { value } } VTagInner::Other { children, tag } => { let mut nodes = Fragment::collect_children(&el); - let (_, child_bundle) = children.hydrate(parent_scope, &el, &mut nodes); + let (_, child_bundle) = children.hydrate(root, parent_scope, &el, &mut nodes); nodes.trim_start_text_nodes(parent); diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index be2c1597d0c..8a0f506fce8 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -101,6 +101,7 @@ mod feat_hydration { impl Hydratable for VText { fn hydrate( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, @@ -135,6 +136,7 @@ mod feat_hydration { // Similarly, the value of the text node may be a combination of multiple VText vnodes. // So we always need to override their values. self.attach( + root, parent_scope, parent, fragment diff --git a/packages/yew/src/dom_bundle/fragment.rs b/packages/yew/src/dom_bundle/fragment.rs index 273e1cf1867..820724b8623 100644 --- a/packages/yew/src/dom_bundle/fragment.rs +++ b/packages/yew/src/dom_bundle/fragment.rs @@ -3,6 +3,7 @@ use std::ops::{Deref, DerefMut}; use web_sys::{Element, Node}; +use super::BSubtree; use crate::html::NodeRef; /// A Hydration Fragment @@ -25,7 +26,7 @@ impl DerefMut for Fragment { impl Fragment { /// Collects child nodes of an element into a VecDeque. - pub(crate) fn collect_children(parent: &Element) -> Self { + pub fn collect_children(parent: &Element) -> Self { let mut fragment = VecDeque::with_capacity(parent.child_nodes().length() as usize); let mut current_node = parent.first_child(); @@ -40,17 +41,8 @@ impl Fragment { Self(fragment) } - /// Shift current Fragment into a different position in the dom. - pub(crate) fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - for node in self.iter() { - next_parent - .insert_before(node, next_sibling.get().as_ref()) - .unwrap(); - } - } - /// Collects nodes for a Component Bundle or a Suspense Boundary. - pub(crate) fn collect_between( + pub fn collect_between( collect_from: &mut Fragment, parent: &Element, open_start_mark: &str, @@ -128,7 +120,7 @@ impl Fragment { } /// Remove child nodes until first non-text node. - pub(crate) fn trim_start_text_nodes(&mut self, parent: &Element) { + pub fn trim_start_text_nodes(&mut self, parent: &Element) { while let Some(ref m) = self.front().cloned() { if m.node_type() == Node::TEXT_NODE { self.pop_front(); @@ -141,7 +133,7 @@ impl Fragment { } /// Deeply clones all nodes. - pub(crate) fn deep_clone(&self) -> Self { + pub fn deep_clone(&self) -> Self { let nodes = self .iter() .map(|m| m.clone_node_with_deep(true).expect("failed to clone node.")) @@ -150,8 +142,8 @@ impl Fragment { Self(nodes) } - /// Detaches the fragment from DOM. - pub(crate) fn detach(self, parent: &Element, parent_to_detach: bool) { + // detaches current fragment. + pub fn detach(self, _root: &BSubtree, parent: &Element, parent_to_detach: bool) { if !parent_to_detach { for node in self.iter() { parent @@ -160,4 +152,13 @@ impl Fragment { } } } + + /// Shift current Fragment into a different position in the dom. + pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + for node in self.iter() { + next_parent + .insert_before(node, next_sibling.get().as_ref()) + .unwrap(); + } + } } diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 1bb5ee20951..edc2c0746af 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -5,6 +5,12 @@ //! In order to efficiently implement updates, and diffing, additional information has to be //! kept around. This information is carried in the bundle. +use web_sys::Element; + +use crate::html::AnyScope; +use crate::html::NodeRef; +use crate::virtual_dom::VNode; + mod bcomp; mod blist; mod bnode; @@ -17,25 +23,14 @@ mod subtree_root; #[cfg(feature = "hydration")] mod fragment; -use gloo::utils::document; -use web_sys::{Element, Node}; - mod traits; mod utils; -use crate::html::AnyScope; -use crate::html::NodeRef; -use crate::virtual_dom::VNode; - use bcomp::BComp; use blist::BList; use bnode::BNode; use bportal::BPortal; use bsuspense::BSuspense; -use btag::BTag; -use btext::BText; -use traits::{DomBundle, Reconcilable}; -use utils::{insert_node, test_log}; #[cfg(feature = "hydration")] pub(crate) use fragment::Fragment; @@ -44,9 +39,6 @@ use traits::Hydratable; #[cfg(feature = "hydration")] use utils::node_type_str; -#[doc(hidden)] // Publically exported from crate::events -pub use btag::set_event_bubbling; - /// A Bundle. /// /// Each component holds a bundle that represents a realised layout, designated by a VNode. @@ -56,7 +48,6 @@ use subtree_root::EventDescriptor; use traits::{Reconcilable, ReconcileTarget}; use utils::{insert_node, test_log}; -#[doc(hidden)] // Publically exported from crate::events pub use subtree_root::set_event_bubbling; pub(crate) use subtree_root::BSubtree; @@ -85,7 +76,6 @@ impl Bundle { /// Applies a virtual dom layout to current bundle. pub fn reconcile( &mut self, - root: &BSubtree, parent_scope: &AnyScope, parent: &Element, @@ -96,7 +86,7 @@ impl Bundle { } /// Detaches current bundle. - pub fn detach(self, parent: &Element, parent_to_detach: bool) { + pub fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) { self.0.detach(root, parent, parent_to_detach); } } @@ -108,13 +98,14 @@ mod feat_hydration { impl Bundle { /// Creates a bundle by hydrating a virtual dom layout. pub fn hydrate( + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, node: VNode, ) -> (NodeRef, Self) { - let (node_ref, root) = node.hydrate(parent_scope, parent, fragment); - (node_ref, Self(root)) + let (node_ref, bundle) = node.hydrate(root, parent_scope, parent, fragment); + (node_ref, Self(bundle)) } } } diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs index a0e5593d38c..17a4ff8de3d 100644 --- a/packages/yew/src/dom_bundle/traits.rs +++ b/packages/yew/src/dom_bundle/traits.rs @@ -70,7 +70,6 @@ pub(super) trait Reconcilable { fn reconcile( self, - root: &BSubtree, parent_scope: &AnyScope, parent: &Element, @@ -115,6 +114,7 @@ mod feat_hydration { /// DOM tree is hydrated from top to bottom. This is different than VDiff::apply. fn hydrate( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index f938fcf0c35..2a094d867c4 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -33,7 +33,7 @@ pub(crate) enum ComponentRenderState { parent: Element, next_sibling: NodeRef, node_ref: NodeRef, - + root: BSubtree, fragment: Fragment, }, @@ -49,13 +49,14 @@ impl std::fmt::Debug for ComponentRenderState { #[cfg(feature = "csr")] Self::Render { ref bundle, - ref root, + root, ref parent, ref next_sibling, ref node_ref, } => f .debug_struct("ComponentRenderState::Render") .field("bundle", bundle) + .field("root", root) .field("parent", parent) .field("next_sibling", next_sibling) .field("node_ref", node_ref) @@ -67,6 +68,7 @@ impl std::fmt::Debug for ComponentRenderState { ref parent, ref next_sibling, ref node_ref, + ref root, } => f .debug_struct("ComponentRenderState::Render") .field("fragment", fragment) @@ -107,6 +109,18 @@ impl ComponentRenderState { *parent = next_parent; *next_sibling = next_next_sibling; } + #[cfg(feature = "csr")] + Self::Hydration { + fragment, + parent, + next_sibling, + .. + } => { + fragment.shift(&next_parent, next_next_sibling.clone()); + + *parent = next_parent; + *next_sibling = next_next_sibling; + } #[cfg(feature = "ssr")] Self::Ssr { .. } => { @@ -385,16 +399,13 @@ impl Runnable for DestroyRunner { // We need to detach the hydrate fragment if the component is not hydrated. #[cfg(feature = "hydration")] ComponentRenderState::Hydration { - ref fragment, + ref root, + fragment, ref parent, ref node_ref, .. } => { - for node in fragment.iter() { - parent - .remove_child(node) - .expect("failed to remove fragment node."); - } + fragment.detach(root, parent, self.parent_to_detach); node_ref.set(None); } @@ -519,6 +530,7 @@ impl RenderRunner { ref parent, ref node_ref, ref next_sibling, + ref root, } => { // We schedule a "first" render to run immediately after hydration, // for the following reason: @@ -527,9 +539,9 @@ impl RenderRunner { // not meant to be suspended.). scheduler::push_component_render( state.comp_id, - RenderRunner { + Box::new(RenderRunner { state: self.state.clone(), - }, + }), ); let scope = state.inner.any_scope(); @@ -537,7 +549,7 @@ impl RenderRunner { // This first node is not guaranteed to be correct here. // As it may be a comment node that is removed afterwards. // but we link it anyways. - let (node, bundle) = Bundle::hydrate(&scope, parent, fragment, new_root); + let (node, bundle) = Bundle::hydrate(root, &scope, parent, fragment, new_root); // We trim all text nodes before checking as it's likely these are whitespaces. fragment.trim_start_text_nodes(parent); @@ -547,6 +559,7 @@ impl RenderRunner { node_ref.link(node); state.render_state = ComponentRenderState::Render { + root: root.clone(), bundle, parent: parent.clone(), node_ref: node_ref.clone(), @@ -596,7 +609,6 @@ mod tests { extern crate self as yew; use super::*; - use crate::dom_bundle::BSubtree; use crate::html; use crate::html::*; @@ -718,12 +730,6 @@ mod tests { fn test_lifecycle(props: Props, expected: &[&str]) { let document = gloo_utils::document(); let scope = Scope::::new(None); - let el = document.create_element("div").unwrap(); - let node_ref = NodeRef::default(); - let lifecycle = props.lifecycle.clone(); - - lifecycle.borrow_mut().clear(); - scope.mount_in_place(el, NodeRef::default(), node_ref, Rc::new(props)); let parent = document.create_element("div").unwrap(); let root = BSubtree::create_root(&parent); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 095b17c7dbf..d2e5e86bc7a 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -43,16 +43,6 @@ impl From> for AnyScope { } impl AnyScope { - #[cfg(feature = "csr")] - #[cfg(test)] - pub(crate) fn test() -> Self { - Self { - type_id: TypeId::of::<()>(), - parent: None, - typed_scope: Rc::new(()), - } - } - /// Returns the parent scope pub fn get_parent(&self) -> Option<&AnyScope> { self.parent.as_deref() @@ -417,6 +407,17 @@ mod feat_csr { use std::cell::Ref; use web_sys::Element; + impl AnyScope { + #[cfg(test)] + pub(crate) fn test() -> Self { + Self { + type_id: TypeId::of::<()>(), + parent: None, + typed_scope: Rc::new(()), + } + } + } + impl Scope where COMP: BaseComponent, @@ -524,7 +525,7 @@ mod feat_csr { mod feat_hydration { use super::*; - use crate::dom_bundle::Fragment; + use crate::dom_bundle::{BSubtree, Fragment}; use crate::html::component::lifecycle::{ComponentRenderState, CreateRunner, RenderRunner}; use crate::html::NodeRef; use crate::scheduler; @@ -545,6 +546,7 @@ mod feat_hydration { /// immediately. pub(crate) fn hydrate_in_place( &self, + root: BSubtree, parent: Element, fragment: &mut Fragment, node_ref: NodeRef, @@ -566,6 +568,7 @@ mod feat_hydration { let next_sibling = NodeRef::default(); let state = ComponentRenderState::Hydration { + root, parent, node_ref, next_sibling, diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 150e7a83efc..fe96c0a83df 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -274,7 +274,6 @@ mod dom_bundle; pub mod functional; pub mod html; mod io_coop; -pub mod portal; pub mod scheduler; mod sealed; #[cfg(feature = "ssr")] @@ -329,11 +328,10 @@ pub mod prelude { pub use crate::context::{ContextHandle, ContextProvider}; pub use crate::events::*; pub use crate::html::{ - BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, Html, HtmlResult, - IntoComponent, NodeRef, Properties, + create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, + Html, HtmlResult, IntoComponent, NodeRef, Properties, }; pub use crate::macros::{classes, html, html_nested}; - pub use crate::portal::Portal; pub use crate::suspense::Suspense; pub use crate::virtual_dom::AttrValue; diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 036aa36bae2..253d7f1c5c4 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -78,6 +78,7 @@ pub(crate) trait Mountable { #[cfg(feature = "hydration")] fn hydrate( self: Box, + root: BSubtree, parent_scope: &AnyScope, parent: Element, fragment: &mut Fragment, @@ -143,13 +144,14 @@ impl Mountable for PropsWrapper { #[cfg(feature = "hydration")] fn hydrate( self: Box, + root: BSubtree, parent_scope: &AnyScope, parent: Element, fragment: &mut Fragment, node_ref: NodeRef, ) -> Box { let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.hydrate_in_place(parent, fragment, node_ref, self.props); + scope.hydrate_in_place(root, parent, fragment, node_ref, self.props); Box::new(scope) } @@ -283,10 +285,10 @@ mod ssr_tests { } } - let mut renderer = ServerRenderer::::new(); - renderer.set_hydratable(false); - - let s = renderer.render().await; + let s = ServerRenderer::::new() + .hydratable(false) + .render() + .await; assert_eq!( s, From 1805c491dadb9582d294298fd8dfaecc74299573 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 12:01:18 +0900 Subject: [PATCH 14/36] trybuild? --- .../base_component_impl-fail.stderr | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr index a7e774e1dc7..563ac8e341e 100644 --- a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr +++ b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr @@ -1,12 +1,12 @@ error[E0277]: the trait bound `Comp: yew::Component` is not satisfied - --> tests/failed_tests/base_component_impl-fail.rs:6:6 - | -6 | impl BaseComponent for Comp { - | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp` - | - = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp` + --> tests/failed_tests/base_component_impl-fail.rs:6:6 + | +6 | impl BaseComponent for Comp { + | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp` + | + = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp` note: required by a bound in `BaseComponent` - --> src/html/component/mod.rs - | - | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` + --> src/html/component/mod.rs + | + | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` From a008b5b195a85389972c194e80f6657d83c06cfc Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 13:44:08 +0900 Subject: [PATCH 15/36] Collectable! --- packages/yew/src/dom_bundle/bsuspense.rs | 10 ++++----- packages/yew/src/dom_bundle/fragment.rs | 27 +++++++++++++----------- packages/yew/src/html/component/scope.rs | 9 ++++++-- packages/yew/src/virtual_dom/mod.rs | 15 +++++++++++-- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 241f8688176..6c6e8cbc57d 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -236,6 +236,7 @@ mod feat_hydration { use super::*; use crate::dom_bundle::{Fragment, Hydratable}; + use crate::virtual_dom::Collectable; impl Hydratable for VSuspense { fn hydrate( @@ -249,11 +250,8 @@ mod feat_hydration { .create_element("div") .expect("failed to create detached element"); - // We start hydration with the BSuspense being suspended. - // A subsequent render will resume the BSuspense if not needed to be suspended. - - let fallback_fragment = - Fragment::collect_between(fragment, parent, "", "suspense"); + let collectable = Collectable::Suspense; + let fallback_fragment = Fragment::collect_between(fragment, &collectable, parent); let mut nodes = fallback_fragment.deep_clone(); @@ -283,6 +281,8 @@ mod feat_hydration { detached_parent, key: self.key, + // We start hydration with the BSuspense being suspended. + // A subsequent render will resume the BSuspense if not needed to be suspended. fallback: Some(Fallback::Fragment(fallback_fragment)), }, ) diff --git a/packages/yew/src/dom_bundle/fragment.rs b/packages/yew/src/dom_bundle/fragment.rs index 820724b8623..13c4ad28aca 100644 --- a/packages/yew/src/dom_bundle/fragment.rs +++ b/packages/yew/src/dom_bundle/fragment.rs @@ -5,6 +5,7 @@ use web_sys::{Element, Node}; use super::BSubtree; use crate::html::NodeRef; +use crate::virtual_dom::Collectable; /// A Hydration Fragment #[derive(Default, Debug, Clone, PartialEq, Eq)] @@ -44,22 +45,21 @@ impl Fragment { /// Collects nodes for a Component Bundle or a Suspense Boundary. pub fn collect_between( collect_from: &mut Fragment, + collect_for: &Collectable, parent: &Element, - open_start_mark: &str, - close_start_mark: &str, - end_mark: &str, - kind_name: &str, ) -> Self { let is_open_tag = |node: &Node| { let comment_text = node.text_content().unwrap_or_else(|| "".to_string()); - comment_text.starts_with(&open_start_mark) && comment_text.ends_with(&end_mark) + comment_text.starts_with(collect_for.open_start_mark()) + && comment_text.ends_with(collect_for.end_mark()) }; let is_close_tag = |node: &Node| { let comment_text = node.text_content().unwrap_or_else(|| "".to_string()); - comment_text.starts_with(&close_start_mark) && comment_text.ends_with(&end_mark) + comment_text.starts_with(collect_for.close_start_mark()) + && comment_text.ends_with(collect_for.end_mark()) }; // We trim all leading text nodes as it's likely these are whitespaces. @@ -67,21 +67,24 @@ impl Fragment { let first_node = collect_from .pop_front() - .unwrap_or_else(|| panic!("expected {} opening tag, found EOF", kind_name)); + .unwrap_or_else(|| panic!("expected {} opening tag, found EOF", collect_for.name())); assert_eq!( first_node.node_type(), Node::COMMENT_NODE, // TODO: improve error message with human readable node type name. "expected {} start, found node type {}", - kind_name, + collect_for.name(), first_node.node_type() ); let mut nodes = VecDeque::new(); if !is_open_tag(&first_node) { - panic!("expected {} opening tag, found comment node", kind_name); + panic!( + "expected {} opening tag, found comment node", + collect_for.name() + ); } // We remove the opening tag. @@ -91,9 +94,9 @@ impl Fragment { let mut nested_layers = 1; loop { - current_node = collect_from - .pop_front() - .unwrap_or_else(|| panic!("expected {} closing tag, found EOF", kind_name)); + current_node = collect_from.pop_front().unwrap_or_else(|| { + panic!("expected {} closing tag, found EOF", collect_for.name()) + }); if current_node.node_type() == Node::COMMENT_NODE { if is_open_tag(¤t_node) { diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index d2e5e86bc7a..dd023bb9854 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -529,6 +529,7 @@ mod feat_hydration { use crate::html::component::lifecycle::{ComponentRenderState, CreateRunner, RenderRunner}; use crate::html::NodeRef; use crate::scheduler; + use crate::virtual_dom::Collectable; use web_sys::Element; @@ -562,8 +563,12 @@ mod feat_hydration { self.id )); - let fragment = - Fragment::collect_between(fragment, &parent, "<[", "", "component"); + #[cfg(debug_assertions)] + let collectable = Collectable::Component(std::any::type_name::()); + #[cfg(not(debug_assertions))] + let collectable = Collectable::Component; + + let fragment = Fragment::collect_between(fragment, &collectable, &parent); node_ref.set(fragment.front().cloned()); let next_sibling = NodeRef::default(); diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 2970eae9874..750a6adee2d 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -179,7 +179,7 @@ mod tests_attr_value { } } -#[cfg(feature = "ssr")] // & feature = "hydration" +#[cfg(any(feature = "ssr", feature = "hydration"))] mod feat_ssr_hydration { /// A collectable. /// @@ -251,10 +251,21 @@ mod feat_ssr_hydration { w.push_str(self.end_mark()); w.push_str("-->"); } + + #[cfg(feature = "hydration")] + pub fn name(&self) -> &'static str { + match self { + #[cfg(debug_assertions)] + Self::Component(_) => "Component", + #[cfg(not(debug_assertions))] + Self::Component => "Component", + Self::Suspense => "Suspense", + } + } } } -#[cfg(feature = "ssr")] +#[cfg(any(feature = "ssr", feature = "hydration"))] pub(crate) use feat_ssr_hydration::*; /// A collection of attributes for an element From 3f0ec5924c81f7cb7ef3007e860f75f6c62819f5 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 14:05:50 +0900 Subject: [PATCH 16/36] Phantom component. --- packages/yew/src/html/component/children.rs | 9 ++ packages/yew/src/html/component/lifecycle.rs | 2 +- packages/yew/src/html/component/marker.rs | 147 +++++++++++++++++++ packages/yew/src/html/component/mod.rs | 2 + packages/yew/tests/hydration.rs | 2 +- 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 packages/yew/src/html/component/marker.rs diff --git a/packages/yew/src/html/component/children.rs b/packages/yew/src/html/component/children.rs index c0fbd1d858d..69368c0cdc3 100644 --- a/packages/yew/src/html/component/children.rs +++ b/packages/yew/src/html/component/children.rs @@ -2,6 +2,7 @@ use crate::html::Html; use crate::virtual_dom::{VChild, VNode}; +use crate::Properties; use std::fmt; /// A type used for accepting children elements in Component::Properties. @@ -208,3 +209,11 @@ impl IntoIterator for ChildrenRenderer { self.children.into_iter() } } + +/// A [Properties] type with Children being the only property. +#[derive(Debug, Properties, PartialEq)] +pub struct ChildrenProps { + /// The Children of a Component. + #[prop_or_default] + pub children: Children, +} diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 2a094d867c4..bcfbd33fe12 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -109,7 +109,7 @@ impl ComponentRenderState { *parent = next_parent; *next_sibling = next_next_sibling; } - #[cfg(feature = "csr")] + #[cfg(feature = "hydration")] Self::Hydration { fragment, parent, diff --git a/packages/yew/src/html/component/marker.rs b/packages/yew/src/html/component/marker.rs new file mode 100644 index 00000000000..42c61216063 --- /dev/null +++ b/packages/yew/src/html/component/marker.rs @@ -0,0 +1,147 @@ +//! Primitive Components & Properties Types + +use crate::function_component; +use crate::html; +use crate::html::{ChildrenProps, Html, IntoComponent}; + +/// A Component to represent a component that does not exist in current implementation. +/// +/// During Hydration, Yew expected the Virtual DOM hierarchy to match the the layout used in server-side +/// rendering. However, sometimes it is possible / reasonable to omit certain components from one +/// side of the implementation. This component is used to represent a component as if a component "existed" +/// in the place it is defined. +/// +/// # Warning +/// +/// The Real DOM hierarchy must also match the server-side rendered artifact. This component is +/// only usable when the original component does not introduce any additional elements. (e.g.: Context +/// Providers) +/// +/// A generic parameter is provided to help identify the component to be substituted. +/// The type of the generic parameter is not required to be the same component that was in the other +/// implementation. However, this behaviour may change in the future if more debug assertions were +/// to be introduced. It is recommended that the generic parameter represents the component in the +/// other implementation. +/// +/// # Example +/// +/// ``` +/// use yew::prelude::*; +/// # use yew::html::ChildrenProps; +/// # +/// # #[function_component] +/// # fn Comp(props: &ChildrenProps) -> Html { +/// # Html::default() +/// # } +/// # +/// # #[function_component] +/// # fn Provider(props: &ChildrenProps) -> Html { +/// # let children = props.children.clone(); +/// # +/// # html! { <>{children} } +/// # } +/// # type Provider1 = Provider; +/// # type Provider2 = Provider; +/// # type Provider3 = Provider; +/// # type Provider4 = Provider; +/// +/// #[function_component] +/// fn ServerApp() -> Html { +/// // The Server Side Rendering Application has 3 Providers. +/// html! { +/// +/// +/// +/// +/// +/// +/// +/// } +/// } +/// +/// #[function_component] +/// fn App() -> Html { +/// // The Client Side Rendering Application has 4 Providers. +/// html! { +/// +/// +/// +/// +/// // This provider does not exist on the server-side +/// // Hydration will fail due to Virtual DOM layout mismatch. +/// +/// +/// +/// +/// +/// +/// +/// } +/// } +/// ``` +/// +/// To mitigate this, we can use a `PhantomComponent`: +/// +/// ``` +/// use yew::prelude::*; +/// # use yew::html::{PhantomComponent, ChildrenProps}; +/// # +/// # #[function_component] +/// # fn Comp(props: &ChildrenProps) -> Html { +/// # Html::default() +/// # } +/// # +/// # #[function_component] +/// # fn Provider(props: &ChildrenProps) -> Html { +/// # let children = props.children.clone(); +/// # +/// # html! { <>{children} } +/// # } +/// # type Provider1 = Provider; +/// # type Provider2 = Provider; +/// # type Provider3 = Provider; +/// # type Provider4 = Provider; +/// +/// #[function_component] +/// fn ServerApp() -> Html { +/// html! { +/// +/// +/// +/// // We add a PhantomComponent for Provider4, +/// // it acts if a Provider4 component presents in this position. +/// > +/// +/// > +/// +/// +/// +/// } +/// } +/// +/// #[function_component] +/// fn App() -> Html { +/// html! { +/// +/// +/// +/// +/// // Hydration will succeed as the PhantomComponent in the server-side +/// // implementation will represent a Provider4 component in this position. +/// +/// +/// +/// +/// +/// +/// +/// } +/// } +/// ``` +#[function_component] +pub fn PhantomComponent(props: &ChildrenProps) -> Html +where + T: IntoComponent, +{ + html! { <>{props.children.clone()} } +} diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index a9962ffcf4f..a77ad48dbce 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -3,11 +3,13 @@ mod children; #[cfg(any(feature = "csr", feature = "ssr"))] mod lifecycle; +mod marker; mod properties; mod scope; use super::{Html, HtmlResult, IntoHtmlResult}; pub use children::*; +pub use marker::*; pub use properties::*; #[cfg(feature = "csr")] diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index e06e3f57b92..6becd1c9678 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -475,7 +475,7 @@ async fn hydration_nested_suspense_works() { let result = obtain_result(); assert_eq!( result.as_str(), - r#"
"# + r#"
"# ); sleep(Duration::from_millis(50)).await; From fb65153bba44282a4ca3776a1816a3701564d246 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 14:17:29 +0900 Subject: [PATCH 17/36] Migrate docs as well. --- .../advanced-topics/server-side-rendering.md | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index d69dda3cbdc..51549987a44 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -126,11 +126,77 @@ suspended. With this approach, developers can build a client-agnostic, SSR ready application with data fetching with very little effort. +## SSR Hydration + +Hydration is the process that connects a Yew application to the +server-side generated HTML file. By default, `ServerRender` prints +hydratable string which includes additional information to facilitate hydration. +When the `Renderer::hydrate` method is called, instead of start rendering from +scratch, Yew will reconcliate the Virtual DOM generated by the application to +the html string generated by the server renderer. + +:::caution + +To successfully hydrate an html representation created by the +`ServerRenderer`, the client must produce a Virtual DOM layout that +exactly matches the one used for SSR including components that does not +contain any elements. If you have any component that is only useful in +one implementation, you may want to use a `PhantomComponent` to fill the +position of the extra component. +::: + +## Component Lifecycle during hydration + +During Hydration, components are created in a different order +(top-to-bottom for siblings). After a component is created, it will be +rendered at least twice before any effects are called. It is important to make +sure your main function of the function component is side-effect free. +It should not mutate any states or trigger additional renders. If your +component currently mutates states or triggers additional renders, move +them into an `use_effect` hook. + +:::danger Struct Components + +Whilst it's possible to use Struct Components with server-side rendering in +hydration, the components will be created in a different order than they +are directly rendered on the client-side. The render function will be called +multiple times before the rendered function will be called and the +component will start receiving messages immediately after the `view()` +function is called after the first time. At that time, the DOM may not +be connected, you should prevent any access to `NodeRef` until +`rendered()` method is called. + +When using SSR and Hydration, prefer function components whenever +possible. + +::: + +## Example + +```rust ,ignore +use yew::prelude::*; +use yew::Renderer; + +#[function_component] +fn App() -> Html { + html! {
{"Hello, World!"}
} +} + +fn main() { + let renderer = Renderer::::new(); + + // hydrates everything under body element, removes trailing + // elements (if any). + renderer.hydrate(); +} +``` + Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/simple_ssr) +Example: [ssr\_router](https://github.com/yewstack/yew/tree/master/examples/ssr_router) :::caution -Server-side rendering is experiemental and currently has no hydration support. -However, you can still use it to generate static websites. +Server-side rendering is currently experiemental. If you find a bug, please file +an issue on [GitHub](https://github.com/yewstack/yew/issues/new?assignees=&labels=bug&template=bug_report.md&title=). ::: From cf5f99db46660ee03badbee0422e97a9424c43a9 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 14:21:13 +0900 Subject: [PATCH 18/36] Update example. --- examples/simple_ssr/Cargo.toml | 17 ++++-- .../simple_ssr/src/bin/simple_ssr_hydrate.rs | 8 +++ .../simple_ssr/src/bin/simple_ssr_server.rs | 53 +++++++++++++++++++ examples/simple_ssr/src/{main.rs => lib.rs} | 50 +++-------------- 4 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 examples/simple_ssr/src/bin/simple_ssr_hydrate.rs create mode 100644 examples/simple_ssr/src/bin/simple_ssr_server.rs rename examples/simple_ssr/src/{main.rs => lib.rs} (64%) diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml index d4812fcc79b..50f879f0c5c 100644 --- a/examples/simple_ssr/Cargo.toml +++ b/examples/simple_ssr/Cargo.toml @@ -6,9 +6,20 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1.15.0", features = ["full"] } -warp = "0.3" -yew = { path = "../../packages/yew", features = ["ssr"] } +yew = { path = "../../packages/yew", features = ["ssr", "hydration"] } reqwest = { version = "0.11.8", features = ["json"] } serde = { version = "1.0.132", features = ["derive"] } uuid = { version = "0.8.2", features = ["serde"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" +wasm-logger = "0.2" +log = "0.4" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1.15.0", features = ["full"] } +warp = "0.3" +structopt = "0.3" +num_cpus = "1.13" +tokio-util = { version = "0.7", features = ["rt"] } +once_cell = "1.5" diff --git a/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs b/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs new file mode 100644 index 00000000000..858995bcf90 --- /dev/null +++ b/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs @@ -0,0 +1,8 @@ +use simple_ssr::App; +use yew::prelude::*; + +fn main() { + #[cfg(target_arch = "wasm32")] + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); + Renderer::::new().hydrate(); +} diff --git a/examples/simple_ssr/src/bin/simple_ssr_server.rs b/examples/simple_ssr/src/bin/simple_ssr_server.rs new file mode 100644 index 00000000000..17a20d17cae --- /dev/null +++ b/examples/simple_ssr/src/bin/simple_ssr_server.rs @@ -0,0 +1,53 @@ +use once_cell::sync::Lazy; +use simple_ssr::App; +use std::path::PathBuf; +use structopt::StructOpt; +use tokio_util::task::LocalPoolHandle; +use warp::Filter; + +// We spawn a local pool that is as big as the number of cpu threads. +static LOCAL_POOL: Lazy = Lazy::new(|| LocalPoolHandle::new(num_cpus::get())); + +/// A basic example +#[derive(StructOpt, Debug)] +struct Opt { + /// the "dist" created by trunk directory to be served for hydration. + #[structopt(short, long, parse(from_os_str))] + dir: PathBuf, +} + +async fn render(index_html_s: &str) -> String { + let content = LOCAL_POOL + .spawn_pinned(move || async move { + let renderer = yew::ServerRenderer::::new(); + + renderer.render().await + }) + .await + .expect("the task has failed."); + + // Good enough for an example, but developers should avoid the replace and extra allocation + // here in an actual app. + index_html_s.replace("", &format!("{}", content)) +} + +#[tokio::main] +async fn main() { + let opts = Opt::from_args(); + + let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html")) + .await + .expect("failed to read index.html"); + + let html = warp::path::end().then(move || { + let index_html_s = index_html_s.clone(); + + async move { warp::reply::html(render(&index_html_s).await) } + }); + + let routes = html.or(warp::fs::dir(opts.dir)); + + println!("You can view the website at: http://localhost:8080/"); + + warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +} diff --git a/examples/simple_ssr/src/main.rs b/examples/simple_ssr/src/lib.rs similarity index 64% rename from examples/simple_ssr/src/main.rs rename to examples/simple_ssr/src/lib.rs index 58dbb0dda8d..ad7af2e0bba 100644 --- a/examples/simple_ssr/src/main.rs +++ b/examples/simple_ssr/src/lib.rs @@ -2,13 +2,15 @@ use std::cell::RefCell; use std::rc::Rc; use serde::{Deserialize, Serialize}; -use tokio::task::LocalSet; -use tokio::task::{spawn_blocking, spawn_local}; use uuid::Uuid; -use warp::Filter; use yew::prelude::*; use yew::suspense::{Suspension, SuspensionResult}; +#[cfg(not(target_arch = "wasm32"))] +use tokio::task::spawn_local; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::spawn_local; + #[derive(Serialize, Deserialize)] struct UuidResponse { uuid: Uuid, @@ -79,7 +81,7 @@ fn Content() -> HtmlResult { } #[function_component] -fn App() -> Html { +pub fn App() -> Html { let fallback = html! {
{"Loading..."}
}; html! { @@ -88,43 +90,3 @@ fn App() -> Html { } } - -async fn render() -> String { - let content = spawn_blocking(move || { - use tokio::runtime::Builder; - let set = LocalSet::new(); - - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - set.block_on(&rt, async { - let renderer = yew::ServerRenderer::::new(); - - renderer.render().await - }) - }) - .await - .expect("the thread has failed."); - - format!( - r#" - - - Yew SSR Example - - - {} - - -"#, - content - ) -} - -#[tokio::main] -async fn main() { - let routes = warp::any().then(|| async move { warp::reply::html(render().await) }); - - println!("You can view the website at: http://localhost:8080/"); - - warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; -} From 383d6b76c7b0704a30181877f54fe91e18bd9a04 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 14:39:59 +0900 Subject: [PATCH 19/36] Fix docs and improve debug message. --- packages/yew/src/dom_bundle/bnode.rs | 2 +- packages/yew/src/dom_bundle/fragment.rs | 2 +- packages/yew/src/virtual_dom/mod.rs | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 8df25ea1602..fd876a39bb4 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -270,7 +270,7 @@ mod feat_hydration { } // You cannot hydrate a VPortal. VNode::VPortal(_) => { - panic!("VPortal is not hydratable. Try to create your portal with the component.") + panic!("VPortal is not hydratable. Try to create your portal by delaying it with use_effect.") } VNode::VSuspense(vsuspense) => { let (node_ref, suspense) = diff --git a/packages/yew/src/dom_bundle/fragment.rs b/packages/yew/src/dom_bundle/fragment.rs index 13c4ad28aca..9822477c697 100644 --- a/packages/yew/src/dom_bundle/fragment.rs +++ b/packages/yew/src/dom_bundle/fragment.rs @@ -42,7 +42,7 @@ impl Fragment { Self(fragment) } - /// Collects nodes for a Component Bundle or a Suspense Boundary. + /// Collects nodes for a Component Bundle or a BSuspense. pub fn collect_between( collect_from: &mut Fragment, collect_for: &Collectable, diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 750a6adee2d..cf0885fcda9 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -181,6 +181,8 @@ mod tests_attr_value { #[cfg(any(feature = "ssr", feature = "hydration"))] mod feat_ssr_hydration { + use super::*; + /// A collectable. /// /// This indicates a kind that can be collected from fragment to be processed at a later time @@ -253,13 +255,13 @@ mod feat_ssr_hydration { } #[cfg(feature = "hydration")] - pub fn name(&self) -> &'static str { + pub fn name(&self) -> Cow<'static, str> { match self { #[cfg(debug_assertions)] - Self::Component(_) => "Component", + Self::Component(m) => format!("Component({})", m).into(), #[cfg(not(debug_assertions))] - Self::Component => "Component", - Self::Suspense => "Suspense", + Self::Component => "Component".into(), + Self::Suspense => "Suspense".into(), } } } From e105e28078dcdbcb37f881567626b58047b22831 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 15:04:16 +0900 Subject: [PATCH 20/36] Minor fixing. --- .../simple_ssr/src/bin/simple_ssr_hydrate.rs | 3 +-- packages/yew/src/dom_bundle/mod.rs | 17 +++++++---------- packages/yew/src/dom_bundle/traits.rs | 2 +- packages/yew/src/virtual_dom/mod.rs | 4 +--- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs b/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs index 858995bcf90..8634be81faf 100644 --- a/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs +++ b/examples/simple_ssr/src/bin/simple_ssr_hydrate.rs @@ -1,8 +1,7 @@ use simple_ssr::App; -use yew::prelude::*; fn main() { #[cfg(target_arch = "wasm32")] wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); - Renderer::::new().hydrate(); + yew::Renderer::::new().hydrate(); } diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index edc2c0746af..af5658c825b 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -32,16 +32,6 @@ use bnode::BNode; use bportal::BPortal; use bsuspense::BSuspense; -#[cfg(feature = "hydration")] -pub(crate) use fragment::Fragment; -#[cfg(feature = "hydration")] -use traits::Hydratable; -#[cfg(feature = "hydration")] -use utils::node_type_str; - -/// A Bundle. -/// -/// Each component holds a bundle that represents a realised layout, designated by a VNode. use btag::{BTag, Registry}; use btext::BText; use subtree_root::EventDescriptor; @@ -52,6 +42,13 @@ pub use subtree_root::set_event_bubbling; pub(crate) use subtree_root::BSubtree; +#[cfg(feature = "hydration")] +pub(crate) use fragment::Fragment; +#[cfg(feature = "hydration")] +use traits::Hydratable; +#[cfg(feature = "hydration")] +use utils::node_type_str; + /// A Bundle. /// /// Each component holds a bundle that represents a realised layout, designated by a [VNode]. diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs index 17a4ff8de3d..e87f17fc31b 100644 --- a/packages/yew/src/dom_bundle/traits.rs +++ b/packages/yew/src/dom_bundle/traits.rs @@ -111,7 +111,7 @@ mod feat_hydration { /// /// # Important /// - /// DOM tree is hydrated from top to bottom. This is different than VDiff::apply. + /// DOM tree is hydrated from top to bottom. This is different than [`Reconcilable`]. fn hydrate( self, root: &BSubtree, diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index cf0885fcda9..b1e43204cba 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -181,8 +181,6 @@ mod tests_attr_value { #[cfg(any(feature = "ssr", feature = "hydration"))] mod feat_ssr_hydration { - use super::*; - /// A collectable. /// /// This indicates a kind that can be collected from fragment to be processed at a later time @@ -255,7 +253,7 @@ mod feat_ssr_hydration { } #[cfg(feature = "hydration")] - pub fn name(&self) -> Cow<'static, str> { + pub fn name(&self) -> super::Cow<'static, str> { match self { #[cfg(debug_assertions)] Self::Component(m) => format!("Component({})", m).into(), From 137f89bc905ae361706aa3503bed61807558f9f2 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 15:21:15 +0900 Subject: [PATCH 21/36] Add hydration to feature soundness check. --- .github/workflows/main-checks.yml | 2 ++ packages/yew/Makefile.toml | 2 ++ packages/yew/src/dom_bundle/fragment.rs | 3 +-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index 2a1ccce5592..8794ef1bd3c 100644 --- a/.github/workflows/main-checks.yml +++ b/.github/workflows/main-checks.yml @@ -32,6 +32,7 @@ jobs: cargo clippy -- --deny=warnings cargo clippy --features=ssr -- --deny=warnings cargo clippy --features=csr -- --deny=warnings + cargo clippy --features=hydration -- --deny=warnings cargo clippy --all-features --all-targets -- --deny=warnings working-directory: packages/yew @@ -62,6 +63,7 @@ jobs: cargo clippy --release -- --deny=warnings cargo clippy --release --features=ssr -- --deny=warnings cargo clippy --release --features=csr -- --deny=warnings + cargo clippy --release --features=hydration -- --deny=warnings cargo clippy --release --all-features --all-targets -- --deny=warnings working-directory: packages/yew diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index bf44c39d317..c08f61944a9 100644 --- a/packages/yew/Makefile.toml +++ b/packages/yew/Makefile.toml @@ -39,11 +39,13 @@ set -ex cargo clippy -- --deny=warnings cargo clippy --features=ssr -- --deny=warnings cargo clippy --features=csr -- --deny=warnings +cargo clippy --features=hydration -- --deny=warnings cargo clippy --all-features --all-targets -- --deny=warnings cargo clippy --release -- --deny=warnings cargo clippy --release --features=ssr -- --deny=warnings cargo clippy --release --features=csr -- --deny=warnings +cargo clippy --release --features=hydration -- --deny=warnings cargo clippy --release --all-features --all-targets -- --deny=warnings ''' diff --git a/packages/yew/src/dom_bundle/fragment.rs b/packages/yew/src/dom_bundle/fragment.rs index 9822477c697..1ce1c1c0773 100644 --- a/packages/yew/src/dom_bundle/fragment.rs +++ b/packages/yew/src/dom_bundle/fragment.rs @@ -90,11 +90,10 @@ impl Fragment { // We remove the opening tag. parent.remove_child(&first_node).unwrap(); - let mut current_node; let mut nested_layers = 1; loop { - current_node = collect_from.pop_front().unwrap_or_else(|| { + let current_node = collect_from.pop_front().unwrap_or_else(|| { panic!("expected {} closing tag, found EOF", collect_for.name()) }); From 054026dbcb7b873c34018bfb92d21da2983f40d1 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 15:29:10 +0900 Subject: [PATCH 22/36] Fix name in debug. --- packages/yew/src/html/component/lifecycle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index bcfbd33fe12..4c1391559d0 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -70,7 +70,7 @@ impl std::fmt::Debug for ComponentRenderState { ref node_ref, ref root, } => f - .debug_struct("ComponentRenderState::Render") + .debug_struct("ComponentRenderState::Hydration") .field("fragment", fragment) .field("root", root) .field("parent", parent) From 5fb2ed8b8fd493579a7f677314990e45ce2e618f Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 15:30:07 +0900 Subject: [PATCH 23/36] Remove Shift. --- packages/yew/src/html/component/lifecycle.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 4c1391559d0..37ee6d04d4b 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -291,8 +291,6 @@ pub(crate) enum UpdateEvent { #[cfg(feature = "csr")] Properties(Rc, NodeRef, NodeRef), /// Shift Scope. - #[cfg(feature = "render")] - Shift(Element, NodeRef), } pub(crate) struct UpdateRunner { From 9b23b5572bab7bcd88a5d1d2678ac6cccea30d46 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 15:33:54 +0900 Subject: [PATCH 24/36] Remove comment. --- packages/yew/src/html/component/lifecycle.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 37ee6d04d4b..c0c0abd514c 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -287,10 +287,8 @@ pub(crate) enum UpdateEvent { /// Drain messages for a component. Message, /// Wraps properties, node ref, and next sibling for a component - #[cfg(feature = "csr")] Properties(Rc, NodeRef, NodeRef), - /// Shift Scope. } pub(crate) struct UpdateRunner { From 7065ac9ba6a06f70a7d0dc5d17148bd1c68e5de7 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 26 Mar 2022 16:27:34 +0900 Subject: [PATCH 25/36] Adjust readme. --- examples/simple_ssr/README.md | 15 +++++++++++++-- examples/simple_ssr/index.html | 9 +++++++++ examples/ssr_router/README.md | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 examples/simple_ssr/index.html diff --git a/examples/simple_ssr/README.md b/examples/simple_ssr/README.md index 95cf18b43ea..6c02a63edf3 100644 --- a/examples/simple_ssr/README.md +++ b/examples/simple_ssr/README.md @@ -2,5 +2,16 @@ This example demonstrates server-side rendering. -Run `cargo run -p simple_ssr` and navigate to http://localhost:8080/ to -view results. +# How to run this example + +1. build hydration bundle + +`trunk build examples/simple_ssr/index.html` + +2. Run the server + +`cargo run --bin simple_ssr_server -- --dir examples/simple_ssr/dist` + +3. Open Browser + +Navigate to http://localhost:8080/ to view results. diff --git a/examples/simple_ssr/index.html b/examples/simple_ssr/index.html new file mode 100644 index 00000000000..62951cf4073 --- /dev/null +++ b/examples/simple_ssr/index.html @@ -0,0 +1,9 @@ + + + + + Yew SSR Example + + + + diff --git a/examples/ssr_router/README.md b/examples/ssr_router/README.md index 7865b2d0513..0f65d5a50b9 100644 --- a/examples/ssr_router/README.md +++ b/examples/ssr_router/README.md @@ -6,7 +6,7 @@ of the function router example. # How to run this example -1. build hydration bundle +1. Build Hydration Bundle `trunk build examples/ssr_router/index.html` From eb5631d7ab4e12d193db7cd3de88548a4358bde0 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 1 Apr 2022 08:09:25 +0900 Subject: [PATCH 26/36] Update website/docs/advanced-topics/server-side-rendering.md Co-authored-by: Muhammad Hamza --- website/docs/advanced-topics/server-side-rendering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index 51549987a44..7c0d90ed9c0 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -159,7 +159,7 @@ them into an `use_effect` hook. Whilst it's possible to use Struct Components with server-side rendering in hydration, the components will be created in a different order than they -are directly rendered on the client-side. The render function will be called +are directly rendered on the client-side. The view function will be called multiple times before the rendered function will be called and the component will start receiving messages immediately after the `view()` function is called after the first time. At that time, the DOM may not From 236baa1af010a45acea82501dff272638cd8bb43 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 1 Apr 2022 08:09:43 +0900 Subject: [PATCH 27/36] Update packages/yew/src/dom_bundle/bnode.rs Co-authored-by: Muhammad Hamza --- packages/yew/src/dom_bundle/bnode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index fd876a39bb4..6fa288a6e16 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -266,7 +266,7 @@ mod feat_hydration { } // You cannot hydrate a VRef. VNode::VRef(_) => { - panic!("VRef is not hydratable. Try move it to a component mounted after an effect.") + panic!("VRef is not hydratable. Try moving it to a component mounted after an effect.") } // You cannot hydrate a VPortal. VNode::VPortal(_) => { From 6cb88c9a136ff9bfa9e3de6264bfdf09d7e059ec Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 1 Apr 2022 08:09:49 +0900 Subject: [PATCH 28/36] Update packages/yew/src/dom_bundle/bnode.rs Co-authored-by: Muhammad Hamza --- packages/yew/src/dom_bundle/bnode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 6fa288a6e16..79586b70361 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -270,7 +270,7 @@ mod feat_hydration { } // You cannot hydrate a VPortal. VNode::VPortal(_) => { - panic!("VPortal is not hydratable. Try to create your portal by delaying it with use_effect.") + panic!("VPortal is not hydratable. Try creating your portal by delaying it with use_effect.") } VNode::VSuspense(vsuspense) => { let (node_ref, suspense) = From 95889daa9b7afeeb8e84d62fd43ad89253ae875e Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Fri, 1 Apr 2022 09:21:48 +0900 Subject: [PATCH 29/36] Once via structopt, now direct clap. --- examples/ssr_router/Cargo.toml | 2 +- examples/ssr_router/src/bin/ssr_router_server.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/ssr_router/Cargo.toml b/examples/ssr_router/Cargo.toml index 2725f536ef8..49092a942f0 100644 --- a/examples/ssr_router/Cargo.toml +++ b/examples/ssr_router/Cargo.toml @@ -17,8 +17,8 @@ wasm-logger = "0.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.15.0", features = ["full"] } warp = "0.3" -structopt = "0.3" env_logger = "0.9" num_cpus = "1.13" tokio-util = { version = "0.7", features = ["rt"] } once_cell = "1.5" +clap = { version = "3.1.7", features = ["derive"] } diff --git a/examples/ssr_router/src/bin/ssr_router_server.rs b/examples/ssr_router/src/bin/ssr_router_server.rs index fcea83c105e..d23a0e02198 100644 --- a/examples/ssr_router/src/bin/ssr_router_server.rs +++ b/examples/ssr_router/src/bin/ssr_router_server.rs @@ -1,8 +1,8 @@ +use clap::Parser; use function_router::{ServerApp, ServerAppProps}; use once_cell::sync::Lazy; use std::collections::HashMap; use std::path::PathBuf; -use structopt::StructOpt; use tokio_util::task::LocalPoolHandle; use warp::Filter; @@ -10,7 +10,7 @@ use warp::Filter; static LOCAL_POOL: Lazy = Lazy::new(|| LocalPoolHandle::new(num_cpus::get())); /// A basic example -#[derive(StructOpt, Debug)] +#[derive(Parser, Debug)] struct Opt { /// the "dist" created by trunk directory to be served for hydration. #[structopt(short, long, parse(from_os_str))] @@ -43,7 +43,7 @@ async fn render(index_html_s: &str, url: &str, queries: HashMap) async fn main() { env_logger::init(); - let opts = Opt::from_args(); + let opts = Opt::parse(); let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html")) .await From ffd05f59a57e03bdcedd8ce461ac9cb44984e7a9 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Apr 2022 10:38:59 +0900 Subject: [PATCH 30/36] Fix docs and empty fragment. --- packages/yew/src/dom_bundle/bsuspense.rs | 7 +++-- .../advanced-topics/server-side-rendering.md | 26 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 6c6e8cbc57d..8be6af8afb9 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -182,7 +182,10 @@ impl Reconcilable for VSuspense { #[cfg(feature = "hydration")] Fallback::Fragment(fragment) => { let node_ref = NodeRef::default(); - node_ref.set(fragment.front().cloned()); + match fragment.front().cloned() { + Some(m) => node_ref.set(Some(m)), + None => node_ref.link(next_sibling), + } node_ref } } @@ -220,7 +223,7 @@ impl Reconcilable for VSuspense { fragment.detach(root, parent, false); } None => { - unreachable!() + unreachable!("None condition has been checked before.") } }; diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index 7c0d90ed9c0..01ac1602abc 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -130,16 +130,16 @@ application with data fetching with very little effort. Hydration is the process that connects a Yew application to the server-side generated HTML file. By default, `ServerRender` prints -hydratable string which includes additional information to facilitate hydration. +hydratable html string which includes additional information to facilitate hydration. When the `Renderer::hydrate` method is called, instead of start rendering from -scratch, Yew will reconcliate the Virtual DOM generated by the application to -the html string generated by the server renderer. +scratch, Yew will reconcile the Virtual DOM generated by the application +with the html string generated by the server renderer. :::caution To successfully hydrate an html representation created by the `ServerRenderer`, the client must produce a Virtual DOM layout that -exactly matches the one used for SSR including components that does not +exactly matches the one used for SSR including components that do not contain any elements. If you have any component that is only useful in one implementation, you may want to use a `PhantomComponent` to fill the position of the extra component. @@ -147,23 +147,21 @@ position of the extra component. ## Component Lifecycle during hydration -During Hydration, components are created in a different order -(top-to-bottom for siblings). After a component is created, it will be -rendered at least twice before any effects are called. It is important to make -sure your main function of the function component is side-effect free. -It should not mutate any states or trigger additional renders. If your -component currently mutates states or triggers additional renders, move -them into an `use_effect` hook. +During Hydration, components schedule 2 consecutive renders after it is +created. Any effects are called after the second render completes. +It is important to make sure that the render function of the your +component is side-effect free. It should not mutate any states or trigger +additional renders. If your component currently mutates states or triggers +additional renders, move them into an `use_effect` hook. :::danger Struct Components Whilst it's possible to use Struct Components with server-side rendering in -hydration, the components will be created in a different order than they -are directly rendered on the client-side. The view function will be called +hydrationt, the view function will be called multiple times before the rendered function will be called and the component will start receiving messages immediately after the `view()` function is called after the first time. At that time, the DOM may not -be connected, you should prevent any access to `NodeRef` until +be connected, you should prevent any access to rendered nodes until `rendered()` method is called. When using SSR and Hydration, prefer function components whenever From bfef5d3079a61611407179cb743f888f0b67de10 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Apr 2022 11:24:47 +0900 Subject: [PATCH 31/36] Remove struct component warning. --- website/docs/advanced-topics/server-side-rendering.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index 01ac1602abc..c802b02535d 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -164,9 +164,6 @@ function is called after the first time. At that time, the DOM may not be connected, you should prevent any access to rendered nodes until `rendered()` method is called. -When using SSR and Hydration, prefer function components whenever -possible. - ::: ## Example From b5dcd2f80ae3e59e03c8b0a53cdcf83620f8eb76 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sat, 2 Apr 2022 14:39:38 +0900 Subject: [PATCH 32/36] Move function router into a separate binary. --- examples/function_router/Cargo.toml | 7 +- examples/function_router/index.html | 2 +- examples/function_router/src/app.rs | 73 ++++++++----------- .../src/{main.rs => bin/function_router.rs} | 9 +-- examples/simple_ssr/Cargo.toml | 2 +- .../simple_ssr/src/bin/simple_ssr_server.rs | 5 +- 6 files changed, 40 insertions(+), 58 deletions(-) rename examples/function_router/src/{main.rs => bin/function_router.rs} (56%) diff --git a/examples/function_router/Cargo.toml b/examples/function_router/Cargo.toml index 76258d84570..9bc73d082da 100644 --- a/examples/function_router/Cargo.toml +++ b/examples/function_router/Cargo.toml @@ -13,14 +13,11 @@ yew-router = { path = "../../packages/yew-router" } serde = { version = "1.0", features = ["derive"] } lazy_static = "1.4.0" gloo-timers = "0.2" +wasm-logger = "0.2" +instant = { version = "0.1", features = ["wasm-bindgen"] } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } -instant = { version = "0.1", features = ["wasm-bindgen"] } -wasm-logger = "0.2" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -instant = { version = "0.1" } [features] csr = ["yew/csr"] diff --git a/examples/function_router/index.html b/examples/function_router/index.html index 2c5aee2ff8f..2a75b730809 100644 --- a/examples/function_router/index.html +++ b/examples/function_router/index.html @@ -11,7 +11,7 @@ href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css" /> - + diff --git a/examples/function_router/src/app.rs b/examples/function_router/src/app.rs index ce581a59bf4..4eac347d887 100644 --- a/examples/function_router/src/app.rs +++ b/examples/function_router/src/app.rs @@ -1,4 +1,8 @@ +use std::collections::HashMap; + use yew::prelude::*; +use yew::virtual_dom::AttrValue; +use yew_router::history::{AnyHistory, History, MemoryHistory}; use yew_router::prelude::*; use crate::components::nav::Nav; @@ -47,53 +51,40 @@ pub fn App() -> Html { } } -#[cfg(not(target_arch = "wasm32"))] -mod arch_native { - use super::*; - - use yew::virtual_dom::AttrValue; - use yew_router::history::{AnyHistory, History, MemoryHistory}; - - use std::collections::HashMap; - - #[derive(Properties, PartialEq, Debug)] - pub struct ServerAppProps { - pub url: AttrValue, - pub queries: HashMap, - } +#[derive(Properties, PartialEq, Debug)] +pub struct ServerAppProps { + pub url: AttrValue, + pub queries: HashMap, +} - #[function_component] - pub fn ServerApp(props: &ServerAppProps) -> Html { - let history = AnyHistory::from(MemoryHistory::new()); - history - .push_with_query(&*props.url, &props.queries) - .unwrap(); +#[function_component] +pub fn ServerApp(props: &ServerAppProps) -> Html { + let history = AnyHistory::from(MemoryHistory::new()); + history + .push_with_query(&*props.url, &props.queries) + .unwrap(); - html! { - -