diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index db27c709143..2a1ccce5592 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=csr -- --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=csr -- --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..168054447d2 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 = ["csr"] } 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..5748fa3b744 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 = ["csr"] } 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..65097ec6e2a 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 = ["csr"] } 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..6710ec24fcd 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 = ["csr"] } 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..ed3a385b898 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 = ["csr"] } 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..5f57593a292 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 = ["csr"] } 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 8b7091cdbbe..4105a2df663 100644 --- a/examples/function_memory_game/Cargo.toml +++ b/examples/function_memory_game/Cargo.toml @@ -13,7 +13,7 @@ gloo = "0.6" nanoid = "0.4" rand = "0.8" getrandom = { version = "0.2", features = ["js"] } -yew = { path = "../../packages/yew" } +yew = { path = "../../packages/yew", features = ["csr"] } [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..76258d84570 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] +csr = ["yew/csr"] diff --git a/examples/function_router/index.html b/examples/function_router/index.html index d7101222d8b..2c5aee2ff8f 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..c4e6e4f5ea6 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 = ["csr"] } [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..697c051e3c4 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", "csr"] } 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..b5bc69697be 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 = ["csr"] } 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..adb57cca236 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 = ["csr"] } 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..80c7055c0a2 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 = ["csr"] } [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..746925edabf 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 = ["csr"] } [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..bb55ead5744 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 = ["csr"] } 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..04e6bcf6913 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 = ["csr"] } 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..15e521ec2af 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 = ["csr"] } 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 02296734691..03293193d9b 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 = ["csr"] } zxcvbn = "2.1.2, <2.2.0" 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..308c2f8db5b 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 = ["csr"] } gloo-utils = "0.1" wasm-bindgen = "0.2" diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 7c16713a894..39b46a4f72a 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -102,5 +102,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..fa7f017dfd1 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 = ["csr"] } 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..57caf1701ac 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", "csr"] } 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..43fcd63ce0d 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 = ["csr"] } 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..1b31fcf0493 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 = ["csr"] } 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..509795052ff 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 = ["csr"] } 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..a97db2bfdf8 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 = ["csr"] } 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..f54a08bad9d 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 = ["csr"] } 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..8ff2574aec8 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 = ["csr"] } [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..de6303fc6a4 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"] +csr = [] +doc_test = ["csr"] +wasm_test = ["csr"] default = [] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index 6cb68f638c4..bf44c39d317 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=csr -- --deny=warnings +cargo clippy --all-features --all-targets -- --deny=warnings + +cargo clippy --release -- --deny=warnings +cargo clippy --release --features=ssr -- --deny=warnings +cargo clippy --release --features=csr -- --deny=warnings +cargo clippy --release --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..a5cc9ca4cd3 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 = "csr")))] 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..4b071d2eee0 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, Reconcilable, ReconcileTarget}; +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,22 +19,20 @@ 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() } } -impl DomBundle for BComp { +impl ReconcileTarget for BComp { fn detach(self, _parent: &Element, parent_to_detach: bool) { self.scope.destroy_boxed(parent_to_detach); } @@ -123,173 +116,11 @@ 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::*; - use crate::dom_bundle::{DomBundle, Reconcilable}; + use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::scheduler; use crate::{ html, @@ -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..edd1cfc7605 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -1,6 +1,6 @@ //! This module contains fragments bundles, a [BList] use super::{test_log, BNode}; -use crate::dom_bundle::{DomBundle, Reconcilable}; +use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VList, VNode, VText}; use std::borrow::Borrow; @@ -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() } @@ -353,7 +353,7 @@ impl BList { } } -impl DomBundle for BList { +impl ReconcileTarget for BList { fn detach(self, parent: &Element, parent_to_detach: bool) { for child in self.rev_children.into_iter() { child.detach(parent, parent_to_detach); diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 0e80563fd30..2401ea6f27f 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -1,7 +1,7 @@ //! This module contains the bundle version of an abstract node [BNode] use super::{BComp, BList, BPortal, BSuspense, BTag, BText}; -use crate::dom_bundle::{DomBundle, Reconcilable}; +use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VNode}; use gloo::console; @@ -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(), @@ -41,7 +41,7 @@ impl BNode { } } -impl DomBundle for BNode { +impl ReconcileTarget for BNode { /// Remove VNode from parent. fn detach(self, parent: &Element, parent_to_detach: bool) { match self { diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index a5c6d769181..65fe4088a58 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -2,7 +2,7 @@ use super::test_log; use super::BNode; -use crate::dom_bundle::{DomBundle, Reconcilable}; +use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::Key; use crate::virtual_dom::VPortal; @@ -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 @@ -19,7 +19,7 @@ pub struct BPortal { node: Box, } -impl DomBundle for BPortal { +impl ReconcileTarget for BPortal { fn detach(self, _: &Element, _parent_to_detach: bool) { test_log!("Detaching portal from host{:?}", self.host.outer_html()); self.node.detach(&self.host, false); @@ -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..55d4767a8fd 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -1,6 +1,6 @@ //! This module contains the bundle version of a supsense [BSuspense] -use super::{BNode, DomBundle, Reconcilable}; +use super::{BNode, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; use crate::NodeRef; @@ -8,7 +8,7 @@ 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 +18,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 @@ -29,7 +29,7 @@ impl BSuspense { } } -impl DomBundle for BSuspense { +impl ReconcileTarget for BSuspense { fn detach(self, parent: &Element, parent_to_detach: bool) { if let Some(fallback) = self.fallback_bundle { fallback.detach(parent, parent_to_detach); 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..ed902baa1f6 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -5,7 +5,7 @@ mod listeners; pub use listeners::set_event_bubbling; -use super::{insert_node, BList, BNode, DomBundle, Reconcilable}; +use super::{insert_node, BList, BNode, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; use crate::virtual_dom::{Attributes, Key, VTag}; @@ -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, @@ -68,7 +68,7 @@ pub struct BTag { key: Option, } -impl DomBundle for BTag { +impl ReconcileTarget for BTag { fn detach(self, parent: &Element, parent_to_detach: bool) { self.listeners.unregister(); @@ -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,10 +277,11 @@ impl BTag { } } +#[cfg(feature = "wasm_test")] #[cfg(test)] mod tests { use super::*; - use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; + use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget}; use crate::html; use crate::html::AnyScope; use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE}; @@ -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..51c54f0a4c5 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -1,6 +1,6 @@ //! This module contains the bundle implementation of text [BText]. -use super::{insert_node, BNode, DomBundle, Reconcilable}; +use super::{insert_node, BNode, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::{AttrValue, VText}; use crate::NodeRef; @@ -9,12 +9,12 @@ 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, } -impl DomBundle for BText { +impl ReconcileTarget for BText { fn detach(self, parent: &Element, parent_to_detach: bool) { if !parent_to_detach { let result = parent.remove_child(&self.text_node); @@ -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..b1aa8da3045 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::{Reconcilable, ReconcileTarget}; +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..a3873da9bfe --- /dev/null +++ b/packages/yew/src/dom_bundle/traits.rs @@ -0,0 +1,92 @@ +use super::BNode; +use crate::html::AnyScope; +use crate::html::NodeRef; +use web_sys::Element; + +/// A Reconcile Target. +/// +/// When a [Reconcilable] is attached, a reconcile target is created to store additional +/// information. +pub(super) trait ReconcileTarget { + /// 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: ReconcileTarget; + + /// 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..245e1c55d21 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::{Callback, Context, HtmlResult}; use std::any::Any; use std::rc::Rc; -pub struct CompStateInner +#[cfg(feature = "csr")] +use crate::dom_bundle::Bundle; +#[cfg(feature = "csr")] +use crate::html::NodeRef; +#[cfg(feature = "csr")] +use web_sys::Element; + +pub(crate) enum ComponentRenderState { + #[cfg(feature = "csr")] + 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 = "csr")] + 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 = "csr")] 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 = "csr")] 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 = "csr")] Properties(Rc, NodeRef, NodeRef), + /// Shift Scope. + #[cfg(feature = "csr")] + 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 = "csr")] + UpdateEvent::Properties(props, next_node_ref, next_sibling) => { + match state.render_state { + #[cfg(feature = "csr")] + 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 = "csr")] + 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 = "csr")] 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 = "csr")] + 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 = "csr")] + 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 = "csr")] +mod feat_csr { + 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 = "csr")] +use feat_csr::*; + +#[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..add550a126f 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 = "csr", 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 = "csr")] +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 = "csr", feature = "ssr"))] +mod feat_csr_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 = "csr", feature = "ssr"))] +pub(crate) use feat_csr_ssr::*; /// The [`Component`]'s context. This contains component's [`Scope`] and and props and /// is passed to every lifecycle method. diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 1456438ae6b..22d47159c92 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 = "csr", feature = "ssr"))] +use crate::scheduler::Shared; +#[cfg(any(feature = "csr", feature = "ssr"))] +use std::cell::RefCell; + +#[cfg(any(feature = "csr", 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 = "csr")] #[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 = "csr", feature = "ssr"))] pub(crate) pending_messages: MsgQueue, + + #[cfg(any(feature = "csr", 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 = "csr", feature = "ssr"))] pending_messages: self.pending_messages.clone(), parent: self.parent.clone(), + + #[cfg(any(feature = "csr", 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 = "csr")] parent_to_detach: false, }); scheduler::start(); @@ -393,6 +238,262 @@ mod feat_ssr { } } +#[cfg(not(any(feature = "ssr", feature = "csr")))] +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 = "csr"))] +mod feat_csr_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 = "csr"))] +pub(crate) use feat_csr_ssr::*; + +#[cfg(feature = "csr")] +mod feat_csr { + 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 = "csr")] +pub(crate) use feat_csr::*; + #[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..3ee8093737e 100644 --- a/packages/yew/src/html/mod.rs +++ b/packages/yew/src/html/mod.rs @@ -125,43 +125,50 @@ 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; } +} + +#[cfg(feature = "csr")] +mod feat_csr { + use super::*; - /// Link a downstream `NodeRef` - pub(crate) fn link(&self, node_ref: Self) { - // Avoid circular references - if self == &node_ref { - return; + 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 + } } } @@ -173,15 +180,14 @@ 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..7227be1c9db 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: +//! - `csr`: 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,6 +265,8 @@ pub mod macros { pub mod callback; pub mod context; +#[cfg_attr(documenting, doc(cfg(feature = "csr")))] +#[cfg(feature = "csr")] mod dom_bundle; pub mod functional; pub mod html; @@ -278,16 +280,20 @@ pub mod utils; pub mod virtual_dom; #[cfg(feature = "ssr")] pub use server_renderer::*; +#[cfg(feature = "csr")] +mod app_handle; +#[cfg(feature = "csr")] +mod renderer; +#[cfg(feature = "csr")] #[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 = "csr")] pub use crate::dom_bundle::set_event_bubbling; #[doc(no_inline)] @@ -297,89 +303,24 @@ 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 = "csr")] +pub use crate::app_handle::AppHandle; +#[cfg(feature = "csr")] +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 = "csr")] + 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, @@ -393,4 +334,3 @@ pub mod prelude { } pub use self::prelude::*; -use std::rc::Rc; diff --git a/packages/yew/src/renderer.rs b/packages/yew/src/renderer.rs new file mode 100644 index 00000000000..c51a3e37a58 --- /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 an application is rendered with [Renderer]. +#[cfg_attr(documenting, doc(cfg(feature = "csr")))] +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 = "csr")))] +#[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..c9d1f7f31de 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 = "csr"))] render: RenderScheduler, /// Stacks to ensure child calls are always before parent calls rendered_first: Vec>, + #[cfg(feature = "csr")] 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 = "csr"))] +mod feat_csr_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 = "csr"))] +pub(crate) use feat_csr_ssr::*; + +#[cfg(feature = "csr")] +mod feat_csr { + 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 = "csr")] +pub(crate) use feat_csr::*; + /// 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 = "csr"))] + { + // 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 = "csr")] + { + // 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..e00897224dd 100644 --- a/packages/yew/src/suspense/component.rs +++ b/packages/yew/src/suspense/component.rs @@ -94,6 +94,7 @@ impl Component for Suspense { } } +#[cfg(any(feature = "csr", feature = "ssr"))] impl Suspense { pub(crate) fn suspend(&self, s: Suspension) { self.link.send_message(SuspenseMsg::Suspend(s)); 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..aebd72a366f 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 = "csr"))] +use crate::html::{AnyScope, Scope}; + +#[cfg(feature = "csr")] +use crate::html::Scoped; +#[cfg(feature = "csr")] +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 = "csr")] + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box; + + #[cfg(feature = "csr")] + 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 = "csr")] + 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 = "csr")] + 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/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 c910d6131e7..290ec9bf678 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", "csr"] } yew-router = { path = "../../packages/yew-router/" } tokio = { version = "1.15.0", features = ["full"] } diff --git a/website/docs/getting-started/build-a-sample-app.mdx b/website/docs/getting-started/build-a-sample-app.mdx index 74a81f54bdf..59360b1439c 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 = ["csr"] } ``` +:::info + +You only need feature `csr` 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..4ce4d0e707a 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 = ["csr"] } ``` +:::info + +You only need feature `csr` 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(); } ```