From ecd2f6a2dfc38d09f6e747c180d93cb15f565bdf Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 10 Mar 2022 22:38:54 +0900 Subject: [PATCH 1/8] 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 | 385 +++++++++----- packages/yew/src/html/component/mod.rs | 74 +-- packages/yew/src/html/component/scope.rs | 489 +++++++++++------- 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 | 294 ++++++----- 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 | 88 +++- packages/yew/src/virtual_dom/vsuspense.rs | 12 +- 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 +- 103 files changed, 1653 insertions(+), 1200 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..268de5697d0 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -2,15 +2,68 @@ use super::scope::{AnyScope, Scope}; use super::BaseComponent; -use crate::dom_bundle::ComponentRenderState; -use crate::html::RenderError; +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,11 +143,12 @@ 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, @@ -107,7 +161,6 @@ pub struct ComponentState { impl ComponentState { pub(crate) fn new( initial_render_state: ComponentRenderState, - node_ref: NodeRef, scope: Scope, props: Rc, ) -> Self { @@ -123,19 +176,29 @@ impl ComponentState { Self { inner, render_state: initial_render_state, - node_ref, suspension: None, + + #[cfg(feature = "render")] has_rendered: false, #[cfg(debug_assertions)] vcomp_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, } @@ -149,7 +212,6 @@ impl Runnable for CreateRunner { *current_state = Some(ComponentState::new( self.initial_render_state, - self.node_ref, self.scope.clone(), self.props, )); @@ -157,31 +219,79 @@ 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 } }; @@ -204,8 +314,10 @@ impl Runnable for UpdateRunner { } } -pub struct DestroyRunner { +pub(crate) struct DestroyRunner { pub state: Shared>, + + #[cfg(feature = "render")] pub parent_to_detach: bool, } @@ -216,13 +328,28 @@ impl Runnable for DestroyRunner { super::log_event(state.vcomp_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>, } @@ -233,120 +360,147 @@ impl Runnable for RenderRunner { super::log_event(state.vcomp_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); - } + if suspension.resumed() { + // schedule a render immediately if suspension is resumed. - 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, - ); - } - } + scheduler::push_component_render( + shared_state.as_ptr() as usize, + RenderRunner { + state: shared_state, + }, + ); + } else { + // We schedule a render after current suspension is resumed. + let comp_scope = state.inner.any_scope(); - 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()); + let suspense_scope = comp_scope + .find_parent_scope::() + .expect("To suspend rendering, a component is required."); + let suspense = suspense_scope.get_component().unwrap(); - suspense.suspend(m); - } + suspension.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_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( + self.state.as_ptr() as usize, + 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.vcomp_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 +615,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..c4ea55950fc 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,54 @@ 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); -} +#[cfg(any(feature = "render", feature = "ssr"))] +mod feat_render_ssr { + #[cfg(debug_assertions)] + thread_local! { + static EVENT_HISTORY: std::cell::RefCell>> + = Default::default(); + static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + } -/// 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()) - }); -} + /// 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()) + }); + } -/// 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() - }) -} + /// 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() + }) + } -#[cfg(debug_assertions)] -pub(crate) fn next_id() -> usize { - COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed)) + #[cfg(debug_assertions)] + pub(crate) fn next_id() -> usize { + COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed)) + } + + #[cfg(debug_assertions)] + use std::sync::atomic::{AtomicUsize, Ordering}; } +#[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 02557af8fbf..6269f0bb794 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; 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,49 +101,15 @@ 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)] @@ -201,8 +126,12 @@ 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)] @@ -217,107 +146,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 +191,46 @@ 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) { let (tx, rx) = oneshot::channel(); - let initial_render_state = ComponentRenderState::new_ssr(tx); - - self.mount_in_place(initial_render_state, NodeRef::default(), props); + let state = ComponentRenderState::Ssr { sender: Some(tx) }; + + scheduler::push_component_create( + CreateRunner { + initial_render_state: state, + props, + scope: self.clone(), + }, + RenderRunner { + state: self.state.clone(), + }, + ); + scheduler::start(); let html = rx.await.unwrap(); - let self_any_scope = self.to_any(); + let self_any_scope = AnyScope::from(self.clone()); html.render_to_string(w, &self_any_scope).await; scheduler::push_component_destroy(DestroyRunner { state: self.state.clone(), + + #[cfg(feature = "render")] parent_to_detach: false, }); scheduler::start(); @@ -393,6 +238,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, + + #[cfg(debug_assertions)] + vcomp_id: super::super::next_id(), + } + } + + /// 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( + 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.vcomp_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..eaa80523a3f 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -1,7 +1,7 @@ //! This module contains a scheduler. use std::cell::RefCell; -use std::collections::{hash_map::Entry, HashMap, VecDeque}; +use std::collections::VecDeque; use std::rc::Rc; /// Alias for Rc> @@ -25,10 +25,13 @@ struct Scheduler { create: Vec>, update: Vec>, render_first: VecDeque>, + + #[cfg(any(feature = "ssr", feature = "render"))] render: RenderScheduler, /// Stacks to ensure child calls are always before parent calls rendered_first: Vec>, + #[cfg(feature = "render")] rendered: RenderedScheduler, } @@ -54,50 +57,155 @@ 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))); -} + use std::collections::{hash_map::Entry, HashMap}; -/// 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 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)); + }); + } + + /// 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 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 update [Runnable] to be executed + pub(crate) fn push_component_update(runnable: impl Runnable + 'static) { + with(|s| s.update.push(Box::new(runnable))); + } + + /// 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)] + pub(super) struct RenderScheduler { + /// Task registry by component ID + tasks: HashMap, + + /// Task queue by component ID + queue: VecDeque, + } + + impl RenderScheduler { + /// Schedule render task execution + pub 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; -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); + // 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 + pub 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 + } + } } -/// 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::*; + + use std::collections::HashMap; + + 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); + } + }); + } + + /// Deduplicating scheduler for component rendered calls with deduplication + #[derive(Default)] + pub(super) struct RenderedScheduler { + /// Task registry by component ID + tasks: HashMap>, + + /// Task stack by component ID + stack: Vec, + } + + impl RenderedScheduler { + /// Schedule rendered task execution + pub 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 + pub 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); + } + } + } + } } +#[cfg(feature = "render")] +pub(crate) use feat_render::*; + /// Execute any pending [Runnable]s pub(crate) fn start_now() { thread_local! { @@ -195,107 +303,29 @@ impl Scheduler { // Likely to cause duplicate renders via component updates, so placed before them to_run.append(&mut self.main); - // Run after all possible updates to avoid duplicate renders. - // - // Should be processed one at time, because they can spawn more create and first render - // events for their children. - if !to_run.is_empty() { - return; - } - if let Some(r) = self.render.pop() { - to_run.push(r); - } - - // These typically do nothing and don't spawn any other events - can be batched. - // Should be run only after all renders have finished. - 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, -} - -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; + #[cfg(any(feature = "ssr", feature = "render"))] + { + // Run after all possible updates to avoid duplicate renders. + // + // Should be processed one at time, because they can spawn more create and first render + // events for their children. + if !to_run.is_empty() { + return; } - } - } - /// 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(_) => (), + if let Some(r) = self.render.pop() { + to_run.push(r); } } - 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); + #[cfg(feature = "render")] + { + // These typically do nothing and don't spawn any other events - can be batched. + // Should be run only after all renders have finished. + if !to_run.is_empty() { + return; } + self.rendered.drain_into(to_run); } } } 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..f12c58de1da 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,81 @@ 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, + ) -> 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, + ) -> LocalBoxFuture<'a, ()> { + async move { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + scope.render_to_string(w, self.props.clone()).await; + } + .boxed_local() + } +} + /// A virtual child component. pub struct VChild { /// The component properties diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 690b6b94bee..e9448c5aa91 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, } diff --git a/packages/yew/tests/mod.rs b/packages/yew/tests/mod.rs index a4ad6656a68..3f5cc20a19d 100644 --- a/packages/yew/tests/mod.rs +++ b/packages/yew/tests/mod.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + mod common; use common::obtain_result; @@ -25,12 +27,13 @@ async fn props_are_passed() { } } - yew::start_app_with_props_in_element::( + yew::Renderer::::with_root_and_props( gloo_utils::document().get_element_by_id("output").unwrap(), PropsPassedFunctionProps { value: "props".to_string(), }, - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index 2691ff8ff18..62fe40f9e36 100644 --- a/packages/yew/tests/suspense.rs +++ b/packages/yew/tests/suspense.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + mod common; use common::obtain_result; @@ -95,7 +97,8 @@ async fn suspense_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(); TimeoutFuture::new(10).await; let result = obtain_result(); @@ -244,7 +247,8 @@ async fn suspense_not_suspended_at_start() { } } - 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(); TimeoutFuture::new(10).await; @@ -362,7 +366,8 @@ async fn suspense_nested_suspense_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(); TimeoutFuture::new(10).await; let result = obtain_result(); @@ -517,10 +522,11 @@ async fn effects_not_run_when_suspended() { counter: counter.clone(), }; - yew::start_app_with_props_in_element::( + yew::Renderer::::with_root_and_props( gloo_utils::document().get_element_by_id("output").unwrap(), props, - ); + ) + .render(); TimeoutFuture::new(10).await; let result = obtain_result(); diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index 8ab7dba3d1d..19d23d2c64d 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + mod common; use common::obtain_result_by_id; @@ -61,9 +63,10 @@ async fn use_context_scoping_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; @@ -143,9 +146,10 @@ async fn use_context_works_with_multiple_types() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; } @@ -242,9 +246,10 @@ async fn use_context_update_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew/tests/use_effect.rs b/packages/yew/tests/use_effect.rs index 1d701a98122..9ca90ea27f1 100644 --- a/packages/yew/tests/use_effect.rs +++ b/packages/yew/tests/use_effect.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + mod common; use common::obtain_result; @@ -64,12 +66,13 @@ async fn use_effect_destroys_on_component_drop() { let destroy_counter = Rc::new(std::cell::RefCell::new(0)); let destroy_counter_c = destroy_counter.clone(); - yew::start_app_with_props_in_element::( + yew::Renderer::::with_root_and_props( gloo_utils::document().get_element_by_id("output").unwrap(), WrapperProps { destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1), }, - ); + ) + .render(); sleep(Duration::ZERO).await; @@ -102,9 +105,10 @@ async fn use_effect_works_many_times() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); @@ -135,9 +139,10 @@ async fn use_effect_works_once() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); @@ -182,9 +187,10 @@ async fn use_effect_refires_on_dependency_change() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result: String = obtain_result(); diff --git a/packages/yew/tests/use_memo.rs b/packages/yew/tests/use_memo.rs index 927e9bc760f..10a47a125bc 100644 --- a/packages/yew/tests/use_memo.rs +++ b/packages/yew/tests/use_memo.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + use std::sync::atomic::{AtomicBool, Ordering}; mod common; @@ -46,9 +48,10 @@ async fn use_memo_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index 4fbab648ed8..4935ac79721 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + use std::collections::HashSet; use std::rc::Rc; @@ -54,9 +56,10 @@ async fn use_reducer_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); @@ -113,9 +116,10 @@ async fn use_reducer_eq_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); diff --git a/packages/yew/tests/use_ref.rs b/packages/yew/tests/use_ref.rs index f08be26ebd7..ee03d669e79 100644 --- a/packages/yew/tests/use_ref.rs +++ b/packages/yew/tests/use_ref.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + mod common; use common::obtain_result; @@ -28,9 +30,10 @@ async fn use_ref_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index 911fdd06fd4..33e7364a0d3 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "wasm_test")] + mod common; use common::obtain_result; @@ -25,9 +27,10 @@ async fn use_state_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "5"); @@ -67,9 +70,10 @@ async fn multiple_use_state_setters() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "11"); @@ -95,9 +99,10 @@ async fn use_state_eq_works() { } } - yew::start_app_in_element::( + yew::Renderer::::with_root( gloo_utils::document().get_element_by_id("output").unwrap(), - ); + ) + .render(); sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "1"); diff --git a/tools/website-test/Cargo.toml b/tools/website-test/Cargo.toml index 43d17d80a25..b6208ba4907 100644 --- a/tools/website-test/Cargo.toml +++ b/tools/website-test/Cargo.toml @@ -16,7 +16,7 @@ js-sys = "0.3" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" weblog = "0.3.0" -yew = { path = "../../packages/yew/", features = ["ssr"] } +yew = { path = "../../packages/yew/", features = ["ssr", "render"] } yew-router = { path = "../../packages/yew-router/" } tokio = { version = "1.15.0", features = ["full"] } diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx index 8551dfcc17e..def273f2090 100644 --- a/website/docs/advanced-topics/portals.mdx +++ b/website/docs/advanced-topics/portals.mdx @@ -6,8 +6,7 @@ description: "Rendering into out-of-tree DOM nodes" ## What is a portal? Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. -`yew::create_portal(child, host)` returns a `Html` value that renders `child` not hierarchically under its parent component, -but as a child of the `host` element. +The `` component renders its `children` into the `host` element provided in its properties. ## Usage @@ -17,13 +16,15 @@ such as controlling the contents of an element's stylesheets to the surrounding document's `` and collecting referenced elements inside a central `` element of an ``. -Note that `yew::create_portal` is a low-level building block. Libraries should use it to implement -higher-level APIs which can then be consumed by applications. For example, here is a -simple modal dialogue that renders its `children` into an element outside `yew`'s control, +# Example + +This example implements a simple modal dialogue that renders its `children` +into an element outside `yew`'s control, identified by the `id="modal_host"`. ```rust -use yew::{html, create_portal, function_component, Children, Properties, Html}; +use yew::{html, function_component, Children, Properties, Html}; +use yew::portal::Portal; #[derive(Properties, PartialEq)] pub struct ModalProps { @@ -37,10 +38,11 @@ fn modal(props: &ModalProps) -> Html { .get_element_by_id("modal_host") .expect("a #modal_host element"); - create_portal( - html!{ {for props.children.iter()} }, - modal_host.into(), - ) + html! { + + { for props.children.iter() } + + } } ``` diff --git a/website/docs/getting-started/build-a-sample-app.mdx b/website/docs/getting-started/build-a-sample-app.mdx index 74a81f54bdf..b4c91c9c96e 100644 --- a/website/docs/getting-started/build-a-sample-app.mdx +++ b/website/docs/getting-started/build-a-sample-app.mdx @@ -55,18 +55,31 @@ edition = "2018" [dependencies] # you can check the latest version here: https://crates.io/crates/yew -yew = "0.19" +yew = { version = "0.19", features = ["render"] } ``` +:::info + +You only need feature `render` if you are building an application. +It will enable the `Renderer` and all client-side rendering related code. + +If you are making a library, do not enable this feature as it will pull in +client-side rendering logic into the server-side rendering bundle. + +If you need the Renderer for testing or examples, you should enable it +in the `dev-dependencies` instead. + +::: + #### Update main.rs We need to generate a template which sets up a root Component called `App` which renders a button that updates its value when clicked. Replace the contents of `src/main.rs` with the following code. :::note -The call to `yew::start_app::()` inside the `main` function starts your application and mounts +The call to `yew::Renderer::::new().render()` inside the `main` function starts your application and mounts it to the page's `` tag. If you would like to start your application with any dynamic -properties, you can instead use `yew::start_app_with_props::(..)`. +properties, you can instead use `yew::Renderer::::with_props(..).render()`. ::: ```rust ,no_run, title=main.rs @@ -92,7 +105,7 @@ fn App() -> Html { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } ``` diff --git a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx index dd511251d35..d53222491ca 100644 --- a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx +++ b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx @@ -43,3 +43,9 @@ will be sent to the reducer function in the same order as they are dispatched. The reducer function can see all previous changes at the time they are run. ::: + +## Yew Renderer + +`start_app*` has been replaced by `yew::Renderer`. + +You need to enable feature `render` to use `yew::Renderer`. diff --git a/website/docs/tutorial/index.mdx b/website/docs/tutorial/index.mdx index 8b0872c34b4..a0a2dbd0950 100644 --- a/website/docs/tutorial/index.mdx +++ b/website/docs/tutorial/index.mdx @@ -72,9 +72,22 @@ version = "0.1.0" edition = "2018" [dependencies] -yew = { git = "https://github.com/yewstack/yew/" } +yew = { git = "https://github.com/yewstack/yew/", features = ["render"] } ``` +:::info + +You only need feature `render` if you are building an application. +It will enable the `Renderer` and all client-side rendering related code. + +If you are making a library, do not enable this feature as it will pull in +client-side rendering logic into the server-side rendering bundle. + +If you need the Renderer for testing or examples, you should enable it +in the `dev-dependencies` instead. + +::: + ```rust ,no_run title="src/main.rs" use yew::prelude::*; @@ -86,7 +99,7 @@ fn app() -> Html { } fn main() { - yew::start_app::(); + yew::Renderer::::new().render(); } ``` From fc8a323a6b6bf5c3439b5980666556aed3ba790d Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Thu, 10 Mar 2022 23:08:01 +0900 Subject: [PATCH 2/8] Child components always render after parents. --- packages/yew/src/html/component/lifecycle.rs | 31 ++- packages/yew/src/html/component/mod.rs | 45 +++-- packages/yew/src/html/component/scope.rs | 15 +- packages/yew/src/scheduler.rs | 199 +++++++------------ 4 files changed, 127 insertions(+), 163 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 268de5697d0..e2e44885d74 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,7 +1,7 @@ //! Component lifecycle module use super::scope::{AnyScope, Scope}; -use super::BaseComponent; +use super::{BaseComponent, ComponentId}; use crate::html::{Html, RenderError}; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{BaseSuspense, Suspension}; @@ -153,9 +153,7 @@ pub(crate) struct ComponentState { suspension: Option, - // Used for debug logging - #[cfg(debug_assertions)] - pub(crate) vcomp_id: usize, + pub(crate) comp_id: ComponentId, } impl ComponentState { @@ -165,7 +163,7 @@ impl ComponentState { 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 { @@ -181,8 +179,7 @@ impl ComponentState { #[cfg(feature = "render")] has_rendered: false, - #[cfg(debug_assertions)] - vcomp_id, + comp_id, } } @@ -208,7 +205,7 @@ 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, @@ -297,13 +294,13 @@ impl Runnable for UpdateRunner { #[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(), }, @@ -325,7 +322,7 @@ 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(); @@ -357,7 +354,7 @@ 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(m) => self.render(state, m), @@ -373,11 +370,13 @@ impl RenderRunner { // suspension to parent element. let shared_state = self.state.clone(); + let comp_id = state.comp_id; + if suspension.resumed() { // schedule a render immediately if suspension is resumed. scheduler::push_component_render( - shared_state.as_ptr() as usize, + state.comp_id, RenderRunner { state: shared_state, }, @@ -393,7 +392,7 @@ impl RenderRunner { suspension.listen(Callback::from(move |_| { scheduler::push_component_render( - shared_state.as_ptr() as usize, + comp_id, RenderRunner { state: shared_state.clone(), }, @@ -442,7 +441,7 @@ impl RenderRunner { state.has_rendered = true; scheduler::push_component_rendered( - self.state.as_ptr() as usize, + state.comp_id, RenderedRunner { state: self.state.clone(), first_render, @@ -474,7 +473,7 @@ mod feat_render { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] - super::super::log_event(state.vcomp_id, "rendered"); + super::super::log_event(state.comp_id, "rendered"); if state.suspension.is_none() { state.inner.rendered(self.first_render); diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index c4ea55950fc..30adb2ea04c 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -14,21 +14,46 @@ pub(crate) use scope::Scoped; pub use scope::{AnyScope, Scope, SendAsMessage}; use std::rc::Rc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// 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); + +impl Default for ComponentId { + fn default() -> Self { + static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + + Self(COMP_ID_COUNTER.fetch_add(1, Ordering::SeqCst)) + } +} + #[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>> + static EVENT_HISTORY: std::cell::RefCell>> = Default::default(); - static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); } /// Push [Component] event to lifecycle debugging registry #[cfg(debug_assertions)] - pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) { + pub(crate) fn log_event(comp_id: ComponentId, event: impl ToString) { EVENT_HISTORY.with(|h| { h.borrow_mut() - .entry(vcomp_id) + .entry(comp_id) .or_default() .push(event.to_string()) }); @@ -37,22 +62,14 @@ mod feat_render_ssr { /// Get [Component] event log from lifecycle debugging registry #[cfg(debug_assertions)] #[allow(dead_code)] - pub(crate) fn get_event_log(vcomp_id: usize) -> Vec { + pub(crate) fn get_event_log(comp_id: ComponentId) -> Vec { EVENT_HISTORY.with(|h| { h.borrow() - .get(&vcomp_id) + .get(&comp_id) .map(|l| (*l).clone()) .unwrap_or_default() }) } - - #[cfg(debug_assertions)] - pub(crate) fn next_id() -> usize { - COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed)) - } - - #[cfg(debug_assertions)] - use std::sync::atomic::{AtomicUsize, Ordering}; } #[cfg(debug_assertions)] #[cfg(any(feature = "render", feature = "ssr"))] diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 6269f0bb794..754e95b36cc 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; #[cfg(any(feature = "render", feature = "ssr"))] use super::lifecycle::{ComponentState, UpdateEvent, UpdateRunner}; -use super::BaseComponent; +use super::{BaseComponent, ComponentId}; use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; use crate::html::IntoComponent; @@ -112,8 +112,7 @@ pub struct Scope { #[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 { @@ -134,8 +133,7 @@ impl Clone for Scope { #[cfg(any(feature = "render", feature = "ssr"))] state: self.state.clone(), - #[cfg(debug_assertions)] - vcomp_id: self.vcomp_id, + id: self.id, } } } @@ -211,6 +209,7 @@ mod feat_ssr { let state = ComponentRenderState::Ssr { sender: Some(tx) }; scheduler::push_component_create( + self.id, CreateRunner { initial_render_state: state, props, @@ -325,8 +324,7 @@ mod feat_render_ssr { state, parent, - #[cfg(debug_assertions)] - vcomp_id: super::super::next_id(), + id: ComponentId::new(), } } @@ -415,6 +413,7 @@ mod feat_render { }; scheduler::push_component_create( + self.id, CreateRunner { initial_render_state: state, props, @@ -435,7 +434,7 @@ mod feat_render { next_sibling: NodeRef, ) { #[cfg(debug_assertions)] - super::super::log_event(self.vcomp_id, "reuse"); + super::super::log_event(self.id, "reuse"); self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling)); } diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index eaa80523a3f..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::VecDeque; +use std::collections::BTreeMap; use std::rc::Rc; +use crate::html::ComponentId; + /// Alias for Rc> pub type Shared = Rc>; @@ -24,15 +26,18 @@ struct Scheduler { destroy: Vec>, create: Vec>, update: Vec>, - render_first: VecDeque>, - - #[cfg(any(feature = "ssr", feature = "render"))] - render: RenderScheduler, - /// Stacks to ensure child calls are always before parent calls - rendered_first: Vec>, - #[cfg(feature = "render")] - 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 @@ -61,16 +66,15 @@ pub fn push(runnable: Box) { mod feat_render_ssr { use super::*; - use std::collections::{hash_map::Entry, HashMap}; - /// 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.push_back(Box::new(first_render)); + s.render_first.insert(component_id, Box::new(first_render)); }); } @@ -80,9 +84,12 @@ mod feat_render_ssr { } /// Push a component render and rendered [Runnable]s to be executed - pub(crate) fn push_component_render(component_id: usize, render: impl Runnable + 'static) { + pub(crate) fn push_component_render( + component_id: ComponentId, + render: impl Runnable + 'static, + ) { with(|s| { - s.render.schedule(component_id, Box::new(render)); + s.render.insert(component_id, Box::new(render)); }); } @@ -90,63 +97,6 @@ mod feat_render_ssr { pub(crate) fn push_component_update(runnable: impl Runnable + 'static) { with(|s| s.update.push(Box::new(runnable))); } - - /// 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)] - pub(super) struct RenderScheduler { - /// Task registry by component ID - tasks: HashMap, - - /// Task queue by component ID - queue: VecDeque, - } - - impl RenderScheduler { - /// Schedule render task execution - pub 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 - pub 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 - } - } } #[cfg(any(feature = "ssr", feature = "render"))] @@ -156,10 +106,8 @@ pub(crate) use feat_render_ssr::*; mod feat_render { use super::*; - use std::collections::HashMap; - pub(crate) fn push_component_rendered( - component_id: usize, + component_id: ComponentId, rendered: impl Runnable + 'static, first_render: bool, ) { @@ -167,40 +115,12 @@ mod feat_render { let rendered = Box::new(rendered); if first_render { - s.rendered_first.push(rendered); + s.rendered_first.insert(component_id, rendered); } else { - s.rendered.schedule(component_id, rendered); + s.rendered.insert(component_id, rendered); } }); } - - /// Deduplicating scheduler for component rendered calls with deduplication - #[derive(Default)] - pub(super) struct RenderedScheduler { - /// Task registry by component ID - tasks: HashMap>, - - /// Task stack by component ID - stack: Vec, - } - - impl RenderedScheduler { - /// Schedule rendered task execution - pub 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 - pub 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); - } - } - } - } } #[cfg(feature = "render")] @@ -278,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); } @@ -292,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. @@ -303,29 +242,39 @@ impl Scheduler { // Likely to cause duplicate renders via component updates, so placed before them to_run.append(&mut self.main); - #[cfg(any(feature = "ssr", feature = "render"))] + // Run after all possible updates to avoid duplicate renders. + // + // Should be processed one at time, because they can spawn more create and first render + // events for their children. + if !to_run.is_empty() { + return; + } + + // 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)) { - // Run after all possible updates to avoid duplicate renders. - // - // Should be processed one at time, because they can spawn more create and first render - // events for their children. - if !to_run.is_empty() { - return; - } + to_run.push(r); + } - if let Some(r) = self.render.pop() { - to_run.push(r); - } + // These typically do nothing and don't spawn any other events - can be batched. + // Should be run only after all renders have finished. + if !to_run.is_empty() { + return; } - #[cfg(feature = "render")] - { - // These typically do nothing and don't spawn any other events - can be batched. - // Should be run only after all renders have finished. - if !to_run.is_empty() { - return; - } - self.rendered.drain_into(to_run); + if !self.rendered.is_empty() { + let mut rendered = BTreeMap::new(); + std::mem::swap(&mut self.rendered, &mut rendered); + + // Children rendered lifecycle happen before parents. + to_run.extend(rendered.into_values().rev()); } } } From b605a9d34fdb8aab56f4b51e9bf08bd0ca33c282 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 20 Mar 2022 21:02:06 +0900 Subject: [PATCH 3/8] Cleanup residual Portal references. --- examples/portals/src/main.rs | 37 +++++++++++------------- website/docs/advanced-topics/portals.mdx | 22 +++++++------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index c4886ff25a4..39b46a4f72a 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::prelude::*; +use yew::{create_portal, html, Children, Component, Context, Html, NodeRef, Properties}; #[derive(Properties, PartialEq)] pub struct ShadowDOMProps { @@ -49,16 +49,15 @@ impl Component for ShadowDOMHost { } fn view(&self, ctx: &Context) -> Html { - let contents = match self.inner_host { - Some(ref m) => { - let children = ctx.props().children.clone(); + let contents = if let Some(ref inner_host) = self.inner_host { + create_portal( html! { - - {children} - - } - } - None => Html::default(), + {for ctx.props().children.iter()} + }, + inner_host.clone(), + ) + } else { + html! { <> } }; html! {
@@ -77,17 +76,15 @@ impl Component for App { type Properties = (); fn create(_ctx: &Context) -> Self { - let document_head: Element = gloo_utils::document() + let document_head = gloo_utils::document() .head() - .expect("head element to be present") - .into(); - let style_html = html! { - - - - }; + .expect("head element to be present"); + let style_html = create_portal( + html! { + + }, + document_head.into(), + ); Self { style_html } } diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx index def273f2090..8551dfcc17e 100644 --- a/website/docs/advanced-topics/portals.mdx +++ b/website/docs/advanced-topics/portals.mdx @@ -6,7 +6,8 @@ description: "Rendering into out-of-tree DOM nodes" ## What is a portal? Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. -The `` component renders its `children` into the `host` element provided in its properties. +`yew::create_portal(child, host)` returns a `Html` value that renders `child` not hierarchically under its parent component, +but as a child of the `host` element. ## Usage @@ -16,15 +17,13 @@ such as controlling the contents of an element's stylesheets to the surrounding document's `` and collecting referenced elements inside a central `` element of an ``. -# Example - -This example implements a simple modal dialogue that renders its `children` -into an element outside `yew`'s control, +Note that `yew::create_portal` is a low-level building block. Libraries should use it to implement +higher-level APIs which can then be consumed by applications. For example, here is a +simple modal dialogue that renders its `children` into an element outside `yew`'s control, identified by the `id="modal_host"`. ```rust -use yew::{html, function_component, Children, Properties, Html}; -use yew::portal::Portal; +use yew::{html, create_portal, function_component, Children, Properties, Html}; #[derive(Properties, PartialEq)] pub struct ModalProps { @@ -38,11 +37,10 @@ fn modal(props: &ModalProps) -> Html { .get_element_by_id("modal_host") .expect("a #modal_host element"); - html! { - - { for props.children.iter() } - - } + create_portal( + html!{ {for props.children.iter()} }, + modal_host.into(), + ) } ``` From 8d57d0560d7e8fc0f35d2746927f17c0d3e6ff5b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 20 Mar 2022 21:22:40 +0900 Subject: [PATCH 4/8] Fix after merge. --- packages/yew/Cargo.toml | 4 ++-- packages/yew/src/html/component/lifecycle.rs | 1 - packages/yew/src/html/component/mod.rs | 3 ++- packages/yew/src/scheduler.rs | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index de6303fc6a4..ce95f74204a 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -79,7 +79,7 @@ trybuild = "1" [features] ssr = ["futures", "html-escape"] csr = [] -doc_test = ["csr"] +doc_test = ["csr", "ssr"] wasm_test = ["csr"] default = [] @@ -87,5 +87,5 @@ default = [] tokio = { version = "1.15.0", features = ["full"] } [package.metadata.docs.rs] -features = ["doc_test", "ssr"] +features = ["doc_test"] rustdoc-args = ["--cfg", "documenting"] diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index a18d2773e38..2057d6ef84a 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -162,7 +162,6 @@ impl ComponentState { scope: Scope, props: Rc, ) -> Self { - #[cfg(debug_assertions)] let comp_id = scope.id; let context = Context { scope, props }; diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index c043aee106e..fac112d2ec2 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -31,7 +31,7 @@ impl Default for ComponentId { } } -#[cfg(any(feature = "render", feature = "ssr"))] +#[cfg(any(feature = "csr", feature = "ssr"))] mod feat_csr_ssr { use super::*; @@ -71,6 +71,7 @@ mod feat_csr_ssr { }) } } + #[cfg(debug_assertions)] #[cfg(any(feature = "csr", feature = "ssr"))] pub(crate) use feat_csr_ssr::*; diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index 36c2efb57de..9bc83fd9b4f 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -105,8 +105,6 @@ pub(crate) use feat_csr_ssr::*; mod feat_csr { use super::*; - use std::collections::HashMap; - pub(crate) fn push_component_rendered( component_id: ComponentId, rendered: impl Runnable + 'static, From 48ea85da9dabf455f43bcba6311d69274f07a22a Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 20 Mar 2022 21:26:14 +0900 Subject: [PATCH 5/8] Fix 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 563ac8e341e..a7e774e1dc7 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 a7aac494b23e0a9dd92daad0e50f647f0a6dbf8b Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 20 Mar 2022 21:59:53 +0900 Subject: [PATCH 6/8] Opt for usize. --- packages/yew/src/html/component/lifecycle.rs | 4 +-- packages/yew/src/html/component/mod.rs | 36 +++----------------- packages/yew/src/html/component/scope.rs | 9 +++-- packages/yew/src/scheduler.rs | 19 ++++------- 4 files changed, 19 insertions(+), 49 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 2057d6ef84a..ae07712ee1a 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,7 +1,7 @@ //! Component lifecycle module use super::scope::{AnyScope, Scope}; -use super::{BaseComponent, ComponentId}; +use super::BaseComponent; use crate::html::{Html, RenderError}; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{BaseSuspense, Suspension}; @@ -153,7 +153,7 @@ pub(crate) struct ComponentState { suspension: Option, - pub(crate) comp_id: ComponentId, + pub(crate) comp_id: usize, } impl ComponentState { diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index fac112d2ec2..613a664b48a 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -14,43 +14,16 @@ pub(crate) use scope::Scoped; pub use scope::{AnyScope, Scope, SendAsMessage}; use std::rc::Rc; -use std::sync::atomic::{AtomicUsize, Ordering}; - -/// 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); - -impl Default for ComponentId { - fn default() -> Self { - static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); - - Self(COMP_ID_COUNTER.fetch_add(1, Ordering::SeqCst)) - } -} - +#[cfg(debug_assertions)] #[cfg(any(feature = "csr", feature = "ssr"))] mod feat_csr_ssr { - use super::*; - - impl ComponentId { - #[inline] - pub fn new() -> Self { - Self::default() - } - } - - #[cfg(debug_assertions)] thread_local! { - static EVENT_HISTORY: std::cell::RefCell>> + 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) { + pub(crate) fn log_event(comp_id: usize, event: impl ToString) { EVENT_HISTORY.with(|h| { h.borrow_mut() .entry(comp_id) @@ -60,9 +33,8 @@ mod feat_csr_ssr { } /// Get [Component] event log from lifecycle debugging registry - #[cfg(debug_assertions)] #[allow(dead_code)] - pub(crate) fn get_event_log(comp_id: ComponentId) -> Vec { + pub(crate) fn get_event_log(comp_id: usize) -> Vec { EVENT_HISTORY.with(|h| { h.borrow() .get(&comp_id) diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 03c0375a0a0..bbcc0435e7c 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; #[cfg(any(feature = "csr", feature = "ssr"))] use super::lifecycle::{ComponentState, UpdateEvent, UpdateRunner}; -use super::{BaseComponent, ComponentId}; +use super::BaseComponent; use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; @@ -113,7 +113,7 @@ pub struct Scope { #[cfg(any(feature = "csr", feature = "ssr"))] pub(crate) state: Shared>, - pub(crate) id: ComponentId, + pub(crate) id: usize, } impl fmt::Debug for Scope { @@ -269,6 +269,7 @@ mod feat_csr_ssr { use super::*; use crate::scheduler::{self, Shared}; use std::cell::Ref; + use std::sync::atomic::{AtomicUsize, Ordering}; #[derive(Debug)] pub(crate) struct MsgQueue(Shared>); @@ -308,6 +309,8 @@ mod feat_csr_ssr { } } + static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + impl Scope { /// Crate a scope with an optional parent scope pub(crate) fn new(parent: Option) -> Self { @@ -325,7 +328,7 @@ mod feat_csr_ssr { state, parent, - id: ComponentId::new(), + id: COMP_ID_COUNTER.fetch_add(1, Ordering::SeqCst), } } diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index 9bc83fd9b4f..dda231c29cd 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -4,8 +4,6 @@ use std::cell::RefCell; use std::collections::BTreeMap; use std::rc::Rc; -use crate::html::ComponentId; - /// Alias for Rc> pub type Shared = Rc>; @@ -32,12 +30,12 @@ struct Scheduler { /// /// Parent can destroy child components but not otherwise, we can save unnecessary render by /// rendering parent first. - render_first: BTreeMap>, - render: BTreeMap>, + render_first: BTreeMap>, + render: BTreeMap>, /// Binary Tree Map to guarantee children rendered are always called before parent calls - rendered_first: BTreeMap>, - rendered: BTreeMap>, + rendered_first: BTreeMap>, + rendered: BTreeMap>, } /// Execute closure with a mutable reference to the scheduler @@ -67,7 +65,7 @@ mod feat_csr_ssr { use super::*; /// Push a component creation, first render and first rendered [Runnable]s to be executed pub(crate) fn push_component_create( - component_id: ComponentId, + component_id: usize, create: impl Runnable + 'static, first_render: impl Runnable + 'static, ) { @@ -83,10 +81,7 @@ mod feat_csr_ssr { } /// Push a component render and rendered [Runnable]s to be executed - pub(crate) fn push_component_render( - component_id: ComponentId, - render: impl Runnable + 'static, - ) { + pub(crate) fn push_component_render(component_id: usize, render: impl Runnable + 'static) { with(|s| { s.render.insert(component_id, Box::new(render)); }); @@ -106,7 +101,7 @@ mod feat_csr { use super::*; pub(crate) fn push_component_rendered( - component_id: ComponentId, + component_id: usize, rendered: impl Runnable + 'static, first_render: bool, ) { From d8e96a64118d33e09e3fcb124643b3eb37aa0d52 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Sun, 20 Mar 2022 22:09:51 +0900 Subject: [PATCH 7/8] Strip Generics. --- packages/yew/src/html/component/lifecycle.rs | 16 +++++----- packages/yew/src/html/component/scope.rs | 32 ++++++++++---------- packages/yew/src/scheduler.rs | 24 +++++++-------- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index ae07712ee1a..92056f2a6f6 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -301,9 +301,9 @@ impl Runnable for UpdateRunner { if schedule_render { scheduler::push_component_render( state.comp_id, - RenderRunner { + Box::new(RenderRunner { state: self.state.clone(), - }, + }), ); // Only run from the scheduler, so no need to call `scheduler::start()` } @@ -377,9 +377,9 @@ impl RenderRunner { scheduler::push_component_render( state.comp_id, - RenderRunner { + Box::new(RenderRunner { state: shared_state, - }, + }), ); } else { // We schedule a render after current suspension is resumed. @@ -393,9 +393,9 @@ impl RenderRunner { suspension.listen(Callback::from(move |_| { scheduler::push_component_render( comp_id, - RenderRunner { + Box::new(RenderRunner { state: shared_state.clone(), - }, + }), ); scheduler::start(); })); @@ -442,10 +442,10 @@ impl RenderRunner { scheduler::push_component_rendered( state.comp_id, - RenderedRunner { + Box::new(RenderedRunner { state: self.state.clone(), first_render, - }, + }), first_render, ); } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index bbcc0435e7c..04582114fb9 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -211,14 +211,14 @@ mod feat_ssr { scheduler::push_component_create( self.id, - CreateRunner { + Box::new(CreateRunner { initial_render_state: state, props, scope: self.clone(), - }, - RenderRunner { + }), + Box::new(RenderRunner { state: self.state.clone(), - }, + }), ); scheduler::start(); @@ -227,12 +227,12 @@ mod feat_ssr { let self_any_scope = AnyScope::from(self.clone()); html.render_to_string(w, &self_any_scope).await; - scheduler::push_component_destroy(DestroyRunner { + scheduler::push_component_destroy(Box::new(DestroyRunner { state: self.state.clone(), #[cfg(feature = "csr")] parent_to_detach: false, - }); + })); scheduler::start(); } } @@ -347,10 +347,10 @@ mod feat_csr_ssr { } pub(super) fn push_update(&self, event: UpdateEvent) { - scheduler::push_component_update(UpdateRunner { + scheduler::push_component_update(Box::new(UpdateRunner { state: self.state.clone(), event, - }); + })); // Not guaranteed to already have the scheduler started scheduler::start(); } @@ -418,14 +418,14 @@ mod feat_csr { scheduler::push_component_create( self.id, - CreateRunner { + Box::new(CreateRunner { initial_render_state: state, props, scope: self.clone(), - }, - RenderRunner { + }), + Box::new(RenderRunner { state: self.state.clone(), - }, + }), ); // Not guaranteed to already have the scheduler started scheduler::start(); @@ -473,10 +473,10 @@ mod feat_csr { /// Process an event to destroy a component fn destroy(self, parent_to_detach: bool) { - scheduler::push_component_destroy(DestroyRunner { + scheduler::push_component_destroy(Box::new(DestroyRunner { state: self.state, parent_to_detach, - }); + })); // Not guaranteed to already have the scheduler started scheduler::start(); } @@ -486,10 +486,10 @@ mod feat_csr { } fn shift_node(&self, parent: Element, next_sibling: NodeRef) { - scheduler::push_component_update(UpdateRunner { + scheduler::push_component_update(Box::new(UpdateRunner { state: self.state.clone(), event: UpdateEvent::Shift(parent, next_sibling), - }) + })) } } } diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index dda231c29cd..a3ec5459021 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -66,30 +66,30 @@ mod feat_csr_ssr { /// Push a component creation, first render and first rendered [Runnable]s to be executed pub(crate) fn push_component_create( component_id: usize, - create: impl Runnable + 'static, - first_render: impl Runnable + 'static, + create: Box, + first_render: Box, ) { with(|s| { - s.create.push(Box::new(create)); - s.render_first.insert(component_id, Box::new(first_render)); + s.create.push(create); + s.render_first.insert(component_id, first_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_destroy(runnable: Box) { + with(|s| s.destroy.push(runnable)); } /// Push a component render and rendered [Runnable]s to be executed - pub(crate) fn push_component_render(component_id: usize, render: impl Runnable + 'static) { + pub(crate) fn push_component_render(component_id: usize, render: Box) { with(|s| { - s.render.insert(component_id, Box::new(render)); + s.render.insert(component_id, 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))); + pub(crate) fn push_component_update(runnable: Box) { + with(|s| s.update.push(runnable)); } } @@ -102,12 +102,10 @@ mod feat_csr { pub(crate) fn push_component_rendered( component_id: usize, - rendered: impl Runnable + 'static, + rendered: Box, first_render: bool, ) { with(|s| { - let rendered = Box::new(rendered); - if first_render { s.rendered_first.insert(component_id, rendered); } else { From cf0d73d87c49630c836bc002bd3a33693d98c8d0 Mon Sep 17 00:00:00 2001 From: Kaede Hoshikawa Date: Mon, 21 Mar 2022 13:02:15 +0900 Subject: [PATCH 8/8] take instead of replace, first instead of front. --- packages/yew/src/scheduler.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index a3ec5459021..26b914d4909 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -202,7 +202,7 @@ impl Scheduler { // Should be processed one at time, because they can spawn more create and rendered events // for their children. // - // To be replaced with BTreeMap::pop_front once it is stable. + // To be replaced with BTreeMap::pop_first once it is stable. if let Some(r) = self .render_first .keys() @@ -220,8 +220,8 @@ impl Scheduler { } if !self.rendered_first.is_empty() { - let mut rendered_first = BTreeMap::new(); - std::mem::swap(&mut self.rendered_first, &mut rendered_first); + let rendered_first = std::mem::take(&mut self.rendered_first); + // Children rendered lifecycle happen before parents. to_run.extend(rendered_first.into_values().rev()); } @@ -242,7 +242,7 @@ impl Scheduler { return; } - // To be replaced with BTreeMap::pop_front once it is stable. + // To be replaced with BTreeMap::pop_first 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 @@ -262,9 +262,7 @@ impl Scheduler { } if !self.rendered.is_empty() { - let mut rendered = BTreeMap::new(); - std::mem::swap(&mut self.rendered, &mut rendered); - + let rendered = std::mem::take(&mut self.rendered); // Children rendered lifecycle happen before parents. to_run.extend(rendered.into_values().rev()); }