mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
SSR Hydration (#2552)
* Bring changes to this branch. * Add feature hydration. * Hydrate text. * Hydrate tag. * Hydrate node. * Hydrate List. * Hydrate Suspense. * Hydrate component. * Renderer::hydrate. * Add example and tests. * Fix comp_id. * Move some code away from generics. * Fix everything. * trybuild? * Collectable! * Phantom component. * Migrate docs as well. * Update example. * Fix docs and improve debug message. * Minor fixing. * Add hydration to feature soundness check. * Fix name in debug. * Remove Shift. * Remove comment. * Adjust readme. * Update website/docs/advanced-topics/server-side-rendering.md Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com> * Update packages/yew/src/dom_bundle/bnode.rs Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com> * Update packages/yew/src/dom_bundle/bnode.rs Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com> * Once via structopt, now direct clap. * Fix docs and empty fragment. * Remove struct component warning. * Move function router into a separate binary. * Optimise Code Logic. * Fix condition. * Fix rendering behaviour. * Fix comment. Co-authored-by: Muhammad Hamza <muhammadhamza1311@gmail.com>
This commit is contained in:
parent
2db42841a1
commit
e46ae55cab
2
.github/workflows/main-checks.yml
vendored
2
.github/workflows/main-checks.yml
vendored
@ -32,6 +32,7 @@ jobs:
|
||||
cargo clippy -- --deny=warnings
|
||||
cargo clippy --features=ssr -- --deny=warnings
|
||||
cargo clippy --features=csr -- --deny=warnings
|
||||
cargo clippy --features=hydration -- --deny=warnings
|
||||
cargo clippy --all-features --all-targets -- --deny=warnings
|
||||
working-directory: packages/yew
|
||||
|
||||
@ -62,6 +63,7 @@ jobs:
|
||||
cargo clippy --release -- --deny=warnings
|
||||
cargo clippy --release --features=ssr -- --deny=warnings
|
||||
cargo clippy --release --features=csr -- --deny=warnings
|
||||
cargo clippy --release --features=hydration -- --deny=warnings
|
||||
cargo clippy --release --all-features --all-targets -- --deny=warnings
|
||||
working-directory: packages/yew
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ members = [
|
||||
"examples/two_apps",
|
||||
"examples/webgl",
|
||||
"examples/web_worker_fib",
|
||||
"examples/ssr_router",
|
||||
"examples/suspense",
|
||||
|
||||
# Tools
|
||||
|
||||
@ -13,14 +13,11 @@ yew-router = { path = "../../packages/yew-router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
lazy_static = "1.4.0"
|
||||
gloo-timers = "0.2"
|
||||
wasm-logger = "0.2"
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
wasm-logger = "0.2"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
instant = { version = "0.1" }
|
||||
|
||||
[features]
|
||||
csr = ["yew/csr"]
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"
|
||||
/>
|
||||
<link data-trunk rel="sass" href="index.scss" />
|
||||
<link data-trunk rel="rust" data-cargo-features="csr" />
|
||||
<link data-trunk rel="rust" data-cargo-features="csr" data-bin="function_router" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use yew::prelude::*;
|
||||
use yew::virtual_dom::AttrValue;
|
||||
use yew_router::history::{AnyHistory, History, MemoryHistory};
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::components::nav::Nav;
|
||||
@ -47,52 +51,39 @@ pub fn App() -> Html {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod arch_native {
|
||||
use super::*;
|
||||
|
||||
use yew::virtual_dom::AttrValue;
|
||||
use yew_router::history::{AnyHistory, History, MemoryHistory};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Properties, PartialEq, Debug)]
|
||||
pub struct ServerAppProps {
|
||||
pub url: AttrValue,
|
||||
pub queries: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ServerApp(props: &ServerAppProps) -> Html {
|
||||
let history = AnyHistory::from(MemoryHistory::new());
|
||||
history
|
||||
.push_with_query(&*props.url, &props.queries)
|
||||
.unwrap();
|
||||
|
||||
html! {
|
||||
<Router history={history}>
|
||||
<Nav />
|
||||
|
||||
<main>
|
||||
<Switch<Route> render={Switch::render(switch)} />
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="content has-text-centered">
|
||||
{ "Powered by " }
|
||||
<a href="https://yew.rs">{ "Yew" }</a>
|
||||
{ " using " }
|
||||
<a href="https://bulma.io">{ "Bulma" }</a>
|
||||
{ " and images from " }
|
||||
<a href="https://unsplash.com">{ "Unsplash" }</a>
|
||||
</div>
|
||||
</footer>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
#[derive(Properties, PartialEq, Debug)]
|
||||
pub struct ServerAppProps {
|
||||
pub url: AttrValue,
|
||||
pub queries: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use arch_native::*;
|
||||
#[function_component]
|
||||
pub fn ServerApp(props: &ServerAppProps) -> Html {
|
||||
let history = AnyHistory::from(MemoryHistory::new());
|
||||
history
|
||||
.push_with_query(&*props.url, &props.queries)
|
||||
.unwrap();
|
||||
|
||||
html! {
|
||||
<Router history={history}>
|
||||
<Nav />
|
||||
|
||||
<main>
|
||||
<Switch<Route> render={Switch::render(switch)} />
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="content has-text-centered">
|
||||
{ "Powered by " }
|
||||
<a href="https://yew.rs">{ "Yew" }</a>
|
||||
{ " using " }
|
||||
<a href="https://bulma.io">{ "Bulma" }</a>
|
||||
{ " and images from " }
|
||||
<a href="https://unsplash.com">{ "Unsplash" }</a>
|
||||
</div>
|
||||
</footer>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
fn switch(routes: &Route) -> Html {
|
||||
match routes.clone() {
|
||||
|
||||
7
examples/function_router/src/bin/function_router.rs
Normal file
7
examples/function_router/src/bin/function_router.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub use function_router::*;
|
||||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
#[cfg(feature = "csr")]
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
mod app;
|
||||
mod components;
|
||||
mod content;
|
||||
mod generator;
|
||||
mod pages;
|
||||
|
||||
pub use app::*;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
#[cfg(feature = "render")]
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
@ -6,9 +6,20 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
warp = "0.3"
|
||||
yew = { path = "../../packages/yew", features = ["ssr"] }
|
||||
yew = { path = "../../packages/yew", features = ["ssr", "hydration"] }
|
||||
reqwest = { version = "0.11.8", features = ["json"] }
|
||||
serde = { version = "1.0.132", features = ["derive"] }
|
||||
uuid = { version = "0.8.2", features = ["serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen-futures = "0.4"
|
||||
wasm-logger = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
warp = "0.3"
|
||||
num_cpus = "1.13"
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
once_cell = "1.5"
|
||||
clap = { version = "3.1.7", features = ["derive"] }
|
||||
|
||||
@ -2,5 +2,16 @@
|
||||
|
||||
This example demonstrates server-side rendering.
|
||||
|
||||
Run `cargo run -p simple_ssr` and navigate to http://localhost:8080/ to
|
||||
view results.
|
||||
# How to run this example
|
||||
|
||||
1. build hydration bundle
|
||||
|
||||
`trunk build examples/simple_ssr/index.html`
|
||||
|
||||
2. Run the server
|
||||
|
||||
`cargo run --bin simple_ssr_server -- --dir examples/simple_ssr/dist`
|
||||
|
||||
3. Open Browser
|
||||
|
||||
Navigate to http://localhost:8080/ to view results.
|
||||
|
||||
9
examples/simple_ssr/index.html
Normal file
9
examples/simple_ssr/index.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Yew SSR Example</title>
|
||||
|
||||
<link data-trunk rel="rust" data-bin="simple_ssr_hydrate" />
|
||||
</head>
|
||||
</html>
|
||||
7
examples/simple_ssr/src/bin/simple_ssr_hydrate.rs
Normal file
7
examples/simple_ssr/src/bin/simple_ssr_hydrate.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use simple_ssr::App;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
yew::Renderer::<App>::new().hydrate();
|
||||
}
|
||||
53
examples/simple_ssr/src/bin/simple_ssr_server.rs
Normal file
53
examples/simple_ssr/src/bin/simple_ssr_server.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use clap::Parser;
|
||||
use once_cell::sync::Lazy;
|
||||
use simple_ssr::App;
|
||||
use std::path::PathBuf;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
use warp::Filter;
|
||||
|
||||
// We spawn a local pool that is as big as the number of cpu threads.
|
||||
static LOCAL_POOL: Lazy<LocalPoolHandle> = Lazy::new(|| LocalPoolHandle::new(num_cpus::get()));
|
||||
|
||||
/// A basic example
|
||||
#[derive(Parser, Debug)]
|
||||
struct Opt {
|
||||
/// the "dist" created by trunk directory to be served for hydration.
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
async fn render(index_html_s: &str) -> String {
|
||||
let content = LOCAL_POOL
|
||||
.spawn_pinned(move || async move {
|
||||
let renderer = yew::ServerRenderer::<App>::new();
|
||||
|
||||
renderer.render().await
|
||||
})
|
||||
.await
|
||||
.expect("the task has failed.");
|
||||
|
||||
// Good enough for an example, but developers should avoid the replace and extra allocation
|
||||
// here in an actual app.
|
||||
index_html_s.replace("<body>", &format!("<body>{}", content))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let opts = Opt::parse();
|
||||
|
||||
let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html"))
|
||||
.await
|
||||
.expect("failed to read index.html");
|
||||
|
||||
let html = warp::path::end().then(move || {
|
||||
let index_html_s = index_html_s.clone();
|
||||
|
||||
async move { warp::reply::html(render(&index_html_s).await) }
|
||||
});
|
||||
|
||||
let routes = html.or(warp::fs::dir(opts.dir));
|
||||
|
||||
println!("You can view the website at: http://localhost:8080/");
|
||||
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
}
|
||||
@ -2,13 +2,15 @@ use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task::LocalSet;
|
||||
use tokio::task::{spawn_blocking, spawn_local};
|
||||
use uuid::Uuid;
|
||||
use warp::Filter;
|
||||
use yew::prelude::*;
|
||||
use yew::suspense::{Suspension, SuspensionResult};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::task::spawn_local;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UuidResponse {
|
||||
uuid: Uuid,
|
||||
@ -79,7 +81,7 @@ fn Content() -> HtmlResult {
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn App() -> Html {
|
||||
pub fn App() -> Html {
|
||||
let fallback = html! {<div>{"Loading..."}</div>};
|
||||
|
||||
html! {
|
||||
@ -88,43 +90,3 @@ fn App() -> Html {
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
async fn render() -> String {
|
||||
let content = spawn_blocking(move || {
|
||||
use tokio::runtime::Builder;
|
||||
let set = LocalSet::new();
|
||||
|
||||
let rt = Builder::new_current_thread().enable_all().build().unwrap();
|
||||
|
||||
set.block_on(&rt, async {
|
||||
let renderer = yew::ServerRenderer::<App>::new();
|
||||
|
||||
renderer.render().await
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("the thread has failed.");
|
||||
|
||||
format!(
|
||||
r#"<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yew SSR Example</title>
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let routes = warp::any().then(|| async move { warp::reply::html(render().await) });
|
||||
|
||||
println!("You can view the website at: http://localhost:8080/");
|
||||
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
}
|
||||
24
examples/ssr_router/Cargo.toml
Normal file
24
examples/ssr_router/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "ssr_router"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew", features = ["ssr", "hydration", "trace_hydration"] }
|
||||
function_router = { path = "../function_router" }
|
||||
log = "0.4"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen-futures = "0.4"
|
||||
wasm-logger = "0.2"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
warp = "0.3"
|
||||
env_logger = "0.9"
|
||||
num_cpus = "1.13"
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
once_cell = "1.5"
|
||||
clap = { version = "3.1.7", features = ["derive"] }
|
||||
19
examples/ssr_router/README.md
Normal file
19
examples/ssr_router/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# SSR Router Example
|
||||
|
||||
This example is the same as the function router example, but with
|
||||
server-side rendering and hydration support. It reuses the same codebase
|
||||
of the function router example.
|
||||
|
||||
# How to run this example
|
||||
|
||||
1. Build Hydration Bundle
|
||||
|
||||
`trunk build examples/ssr_router/index.html`
|
||||
|
||||
2. Run the server
|
||||
|
||||
`cargo run --bin ssr_router_server -- --dir examples/ssr_router/dist`
|
||||
|
||||
3. Open Browser
|
||||
|
||||
Navigate to http://localhost:8080/ to view results.
|
||||
15
examples/ssr_router/index.html
Normal file
15
examples/ssr_router/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link data-trunk rel="rust" data-bin="ssr_router_hydrate" />
|
||||
|
||||
<title>Yew • SSR Router</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"
|
||||
/>
|
||||
<link data-trunk rel="sass" href="../function_router/index.scss" />
|
||||
</head>
|
||||
</html>
|
||||
7
examples/ssr_router/src/bin/ssr_router_hydrate.rs
Normal file
7
examples/ssr_router/src/bin/ssr_router_hydrate.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use function_router::App;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
yew::Renderer::<App>::new().hydrate();
|
||||
}
|
||||
71
examples/ssr_router/src/bin/ssr_router_server.rs
Normal file
71
examples/ssr_router/src/bin/ssr_router_server.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use clap::Parser;
|
||||
use function_router::{ServerApp, ServerAppProps};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
use warp::Filter;
|
||||
|
||||
// We spawn a local pool that is as big as the number of cpu threads.
|
||||
static LOCAL_POOL: Lazy<LocalPoolHandle> = Lazy::new(|| LocalPoolHandle::new(num_cpus::get()));
|
||||
|
||||
/// A basic example
|
||||
#[derive(Parser, Debug)]
|
||||
struct Opt {
|
||||
/// the "dist" created by trunk directory to be served for hydration.
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
async fn render(index_html_s: &str, url: &str, queries: HashMap<String, String>) -> String {
|
||||
let url = url.to_string();
|
||||
|
||||
let content = LOCAL_POOL
|
||||
.spawn_pinned(move || async move {
|
||||
let server_app_props = ServerAppProps {
|
||||
url: url.into(),
|
||||
queries,
|
||||
};
|
||||
|
||||
let renderer = yew::ServerRenderer::<ServerApp>::with_props(server_app_props);
|
||||
|
||||
renderer.render().await
|
||||
})
|
||||
.await
|
||||
.expect("the task has failed.");
|
||||
|
||||
// Good enough for an example, but developers should avoid the replace and extra allocation
|
||||
// here in an actual app.
|
||||
index_html_s.replace("<body>", &format!("<body>{}", content))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let opts = Opt::parse();
|
||||
|
||||
let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html"))
|
||||
.await
|
||||
.expect("failed to read index.html");
|
||||
|
||||
let render = move |s: warp::filters::path::FullPath, queries: HashMap<String, String>| {
|
||||
let index_html_s = index_html_s.clone();
|
||||
|
||||
async move { warp::reply::html(render(&index_html_s, s.as_str(), queries).await) }
|
||||
};
|
||||
|
||||
let html = warp::path::end().and(
|
||||
warp::path::full()
|
||||
.and(warp::filters::query::query())
|
||||
.then(render.clone()),
|
||||
);
|
||||
|
||||
let routes = html.or(warp::fs::dir(opts.dir)).or(warp::path::full()
|
||||
.and(warp::filters::query::query())
|
||||
.then(render));
|
||||
|
||||
println!("You can view the website at: http://localhost:8080/");
|
||||
|
||||
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
}
|
||||
1
examples/ssr_router/src/lib.rs
Normal file
1
examples/ssr_router/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -58,15 +58,12 @@ impl NavigatorContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Router component.
|
||||
/// The base router.
|
||||
///
|
||||
/// This provides location and navigator context to its children and switches.
|
||||
///
|
||||
/// If you are building a web application, you may want to consider using [`BrowserRouter`] instead.
|
||||
///
|
||||
/// You only need one `<Router />` for each application.
|
||||
#[function_component(Router)]
|
||||
pub fn router(props: &RouterProps) -> Html {
|
||||
/// The implementation is separated to make sure <Router /> has the same virtual dom layout as
|
||||
/// the <BrowserRouter /> and <HashRouter />.
|
||||
#[function_component(BaseRouter)]
|
||||
fn base_router(props: &RouterProps) -> Html {
|
||||
let RouterProps {
|
||||
history,
|
||||
children,
|
||||
@ -117,6 +114,20 @@ pub fn router(props: &RouterProps) -> Html {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Router component.
|
||||
///
|
||||
/// This provides location and navigator context to its children and switches.
|
||||
///
|
||||
/// If you are building a web application, you may want to consider using [`BrowserRouter`] instead.
|
||||
///
|
||||
/// You only need one `<Router />` for each application.
|
||||
#[function_component(Router)]
|
||||
pub fn router(props: &RouterProps) -> Html {
|
||||
html! {
|
||||
<BaseRouter ..{props.clone()} />
|
||||
}
|
||||
}
|
||||
|
||||
/// Props for [`BrowserRouter`] and [`HashRouter`].
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct ConcreteRouterProps {
|
||||
@ -143,9 +154,9 @@ pub fn browser_router(props: &ConcreteRouterProps) -> Html {
|
||||
let basename = basename.map(|m| m.to_string()).or_else(base_url);
|
||||
|
||||
html! {
|
||||
<Router history={(*history).clone()} {basename}>
|
||||
<BaseRouter history={(*history).clone()} {basename}>
|
||||
{children}
|
||||
</Router>
|
||||
</BaseRouter>
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,8 +174,8 @@ pub fn hash_router(props: &ConcreteRouterProps) -> Html {
|
||||
let history = use_state(|| AnyHistory::from(HashHistory::new()));
|
||||
|
||||
html! {
|
||||
<Router history={(*history).clone()} {basename}>
|
||||
<BaseRouter history={(*history).clone()} {basename}>
|
||||
{children}
|
||||
</Router>
|
||||
</BaseRouter>
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ pub fn fetch_base_url() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn compose_path(pathname: &str, query: &str) -> Option<String> {
|
||||
gloo::utils::window()
|
||||
.location()
|
||||
@ -53,6 +54,17 @@ pub fn compose_path(pathname: &str, query: &str) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn compose_path(pathname: &str, query: &str) -> Option<String> {
|
||||
let query = query.trim();
|
||||
|
||||
if !query.is_empty() {
|
||||
Some(format!("{}?{}", pathname, query))
|
||||
} else {
|
||||
Some(pathname.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gloo::utils::document;
|
||||
|
||||
@ -51,6 +51,7 @@ features = [
|
||||
"Location",
|
||||
"MouseEvent",
|
||||
"Node",
|
||||
"NodeList",
|
||||
"PointerEvent",
|
||||
"ProgressEvent",
|
||||
"Text",
|
||||
@ -87,8 +88,10 @@ features = [
|
||||
[features]
|
||||
ssr = ["futures", "html-escape"]
|
||||
csr = []
|
||||
doc_test = ["csr", "ssr"]
|
||||
wasm_test = ["csr"]
|
||||
hydration = ["csr"]
|
||||
trace_hydration = ["hydration"]
|
||||
doc_test = ["csr", "hydration", "ssr"]
|
||||
wasm_test = ["csr", "hydration", "ssr"]
|
||||
default = []
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
|
||||
@ -39,11 +39,13 @@ set -ex
|
||||
cargo clippy -- --deny=warnings
|
||||
cargo clippy --features=ssr -- --deny=warnings
|
||||
cargo clippy --features=csr -- --deny=warnings
|
||||
cargo clippy --features=hydration -- --deny=warnings
|
||||
cargo clippy --all-features --all-targets -- --deny=warnings
|
||||
|
||||
cargo clippy --release -- --deny=warnings
|
||||
cargo clippy --release --features=ssr -- --deny=warnings
|
||||
cargo clippy --release --features=csr -- --deny=warnings
|
||||
cargo clippy --release --features=hydration -- --deny=warnings
|
||||
cargo clippy --release --all-features --all-targets -- --deny=warnings
|
||||
'''
|
||||
|
||||
|
||||
@ -63,3 +63,41 @@ fn clear_element(host: &Element) {
|
||||
host.remove_child(&child).expect("can't remove a child");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "hydration")))]
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::Fragment;
|
||||
|
||||
impl<ICOMP> AppHandle<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent,
|
||||
{
|
||||
pub(crate) fn hydrate_with_props(host: Element, props: Rc<ICOMP::Properties>) -> Self {
|
||||
let app = Self {
|
||||
scope: Scope::new(None),
|
||||
};
|
||||
|
||||
let mut fragment = Fragment::collect_children(&host);
|
||||
let hosting_root = BSubtree::create_root(&host);
|
||||
|
||||
app.scope.hydrate_in_place(
|
||||
hosting_root,
|
||||
host.clone(),
|
||||
&mut fragment,
|
||||
NodeRef::default(),
|
||||
props,
|
||||
);
|
||||
|
||||
// We remove all remaining nodes, this mimics the clear_element behaviour in
|
||||
// mount_with_props.
|
||||
for node in fragment.iter() {
|
||||
host.remove_child(node).unwrap();
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +119,48 @@ impl Reconcilable for VComp {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::{Fragment, Hydratable};
|
||||
|
||||
impl Hydratable for VComp {
|
||||
fn hydrate(
|
||||
self,
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let VComp {
|
||||
type_id,
|
||||
mountable,
|
||||
node_ref,
|
||||
key,
|
||||
} = self;
|
||||
|
||||
let scoped = mountable.hydrate(
|
||||
root.clone(),
|
||||
parent_scope,
|
||||
parent.clone(),
|
||||
fragment,
|
||||
node_ref.clone(),
|
||||
);
|
||||
|
||||
(
|
||||
node_ref.clone(),
|
||||
BComp {
|
||||
type_id,
|
||||
scope: scoped,
|
||||
node_ref,
|
||||
key,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@ -453,6 +453,47 @@ impl Reconcilable for VList {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::{Fragment, Hydratable};
|
||||
|
||||
impl Hydratable for VList {
|
||||
fn hydrate(
|
||||
self,
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let node_ref = NodeRef::default();
|
||||
let mut children = Vec::with_capacity(self.children.len());
|
||||
|
||||
for (index, child) in self.children.into_iter().enumerate() {
|
||||
let (child_node_ref, child) = child.hydrate(root, parent_scope, parent, fragment);
|
||||
|
||||
if index == 0 {
|
||||
node_ref.reuse(child_node_ref);
|
||||
}
|
||||
|
||||
children.push(child);
|
||||
}
|
||||
|
||||
children.reverse();
|
||||
|
||||
(
|
||||
node_ref,
|
||||
BList {
|
||||
rev_children: children,
|
||||
fully_keyed: self.fully_keyed,
|
||||
key: self.key,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
|
||||
@ -233,6 +233,55 @@ impl fmt::Debug for BNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::{Fragment, Hydratable};
|
||||
|
||||
impl Hydratable for VNode {
|
||||
fn hydrate(
|
||||
self,
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
match self {
|
||||
VNode::VTag(vtag) => {
|
||||
let (node_ref, tag) = vtag.hydrate(root, parent_scope, parent, fragment);
|
||||
(node_ref, tag.into())
|
||||
}
|
||||
VNode::VText(vtext) => {
|
||||
let (node_ref, text) = vtext.hydrate(root, parent_scope, parent, fragment);
|
||||
(node_ref, text.into())
|
||||
}
|
||||
VNode::VComp(vcomp) => {
|
||||
let (node_ref, comp) = vcomp.hydrate(root, parent_scope, parent, fragment);
|
||||
(node_ref, comp.into())
|
||||
}
|
||||
VNode::VList(vlist) => {
|
||||
let (node_ref, list) = vlist.hydrate(root, parent_scope, parent, fragment);
|
||||
(node_ref, list.into())
|
||||
}
|
||||
// You cannot hydrate a VRef.
|
||||
VNode::VRef(_) => {
|
||||
panic!("VRef is not hydratable. Try moving it to a component mounted after an effect.")
|
||||
}
|
||||
// You cannot hydrate a VPortal.
|
||||
VNode::VPortal(_) => {
|
||||
panic!("VPortal is not hydratable. Try creating your portal by delaying it with use_effect.")
|
||||
}
|
||||
VNode::VSuspense(vsuspense) => {
|
||||
let (node_ref, suspense) =
|
||||
vsuspense.hydrate(root, parent_scope, parent, fragment);
|
||||
(node_ref, suspense.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
use super::*;
|
||||
|
||||
@ -7,12 +7,24 @@ use crate::NodeRef;
|
||||
use gloo::utils::document;
|
||||
use web_sys::Element;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
use super::Fragment;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Fallback {
|
||||
/// Suspense Fallback with fallback being rendered as placeholder.
|
||||
Bundle(BNode),
|
||||
/// Suspense Fallback with Hydration Fragment being rendered as placeholder.
|
||||
#[cfg(feature = "hydration")]
|
||||
Fragment(Fragment),
|
||||
}
|
||||
|
||||
/// The bundle implementation to [VSuspense]
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BSuspense {
|
||||
children_bundle: BNode,
|
||||
/// The supsense is suspended if fallback contains [Some] bundle
|
||||
fallback_bundle: Option<BNode>,
|
||||
fallback: Option<Fallback>,
|
||||
detached_parent: Element,
|
||||
key: Option<Key>,
|
||||
}
|
||||
@ -22,27 +34,45 @@ impl BSuspense {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
/// Get the bundle node that actually shows up in the dom
|
||||
fn active_node(&self) -> &BNode {
|
||||
self.fallback_bundle
|
||||
.as_ref()
|
||||
.unwrap_or(&self.children_bundle)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReconcileTarget for BSuspense {
|
||||
fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
|
||||
if let Some(fallback) = self.fallback_bundle {
|
||||
fallback.detach(root, parent, parent_to_detach);
|
||||
self.children_bundle
|
||||
.detach(root, &self.detached_parent, false);
|
||||
} else {
|
||||
self.children_bundle.detach(root, parent, parent_to_detach);
|
||||
match self.fallback {
|
||||
Some(m) => {
|
||||
match m {
|
||||
Fallback::Bundle(bundle) => {
|
||||
bundle.detach(root, parent, parent_to_detach);
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
Fallback::Fragment(fragment) => {
|
||||
fragment.detach(root, parent, parent_to_detach);
|
||||
}
|
||||
}
|
||||
|
||||
self.children_bundle
|
||||
.detach(root, &self.detached_parent, false);
|
||||
}
|
||||
None => {
|
||||
self.children_bundle.detach(root, parent, parent_to_detach);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
||||
self.active_node().shift(next_parent, next_sibling)
|
||||
match self.fallback.as_ref() {
|
||||
Some(Fallback::Bundle(bundle)) => {
|
||||
bundle.shift(next_parent, next_sibling);
|
||||
}
|
||||
#[cfg(feature = "hydration")]
|
||||
Some(Fallback::Fragment(fragment)) => {
|
||||
fragment.shift(next_parent, next_sibling);
|
||||
}
|
||||
None => {
|
||||
self.children_bundle.shift(next_parent, next_sibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +107,7 @@ impl Reconcilable for VSuspense {
|
||||
fallback_ref,
|
||||
BSuspense {
|
||||
children_bundle,
|
||||
fallback_bundle: Some(fallback),
|
||||
fallback: Some(Fallback::Bundle(fallback)),
|
||||
detached_parent,
|
||||
key,
|
||||
},
|
||||
@ -89,7 +119,7 @@ impl Reconcilable for VSuspense {
|
||||
child_ref,
|
||||
BSuspense {
|
||||
children_bundle,
|
||||
fallback_bundle: None,
|
||||
fallback: None,
|
||||
detached_parent,
|
||||
key,
|
||||
},
|
||||
@ -124,7 +154,7 @@ impl Reconcilable for VSuspense {
|
||||
) -> NodeRef {
|
||||
let VSuspense {
|
||||
children,
|
||||
fallback,
|
||||
fallback: vfallback,
|
||||
suspended,
|
||||
key: _,
|
||||
} = self;
|
||||
@ -134,9 +164,9 @@ impl Reconcilable for VSuspense {
|
||||
|
||||
// When it's suspended, we render children into an element that is detached from the dom
|
||||
// tree while rendering fallback UI into the original place where children resides in.
|
||||
match (suspended, &mut suspense.fallback_bundle) {
|
||||
match (suspended, &mut suspense.fallback) {
|
||||
// Both suspended, reconcile children into detached_parent, fallback into the DOM
|
||||
(true, Some(fallback_bundle)) => {
|
||||
(true, Some(fallback)) => {
|
||||
children.reconcile_node(
|
||||
root,
|
||||
parent_scope,
|
||||
@ -145,7 +175,20 @@ impl Reconcilable for VSuspense {
|
||||
children_bundle,
|
||||
);
|
||||
|
||||
fallback.reconcile_node(root, parent_scope, parent, next_sibling, fallback_bundle)
|
||||
match fallback {
|
||||
Fallback::Bundle(bundle) => {
|
||||
vfallback.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
|
||||
}
|
||||
#[cfg(feature = "hydration")]
|
||||
Fallback::Fragment(fragment) => {
|
||||
let node_ref = NodeRef::default();
|
||||
match fragment.front().cloned() {
|
||||
Some(m) => node_ref.set(Some(m)),
|
||||
None => node_ref.link(next_sibling),
|
||||
}
|
||||
node_ref
|
||||
}
|
||||
}
|
||||
}
|
||||
// Not suspended, just reconcile the children into the DOM
|
||||
(false, None) => {
|
||||
@ -163,18 +206,26 @@ impl Reconcilable for VSuspense {
|
||||
children_bundle,
|
||||
);
|
||||
// first render of fallback
|
||||
|
||||
let (fallback_ref, fallback) =
|
||||
fallback.attach(root, parent_scope, parent, next_sibling);
|
||||
suspense.fallback_bundle = Some(fallback);
|
||||
vfallback.attach(root, parent_scope, parent, next_sibling);
|
||||
suspense.fallback = Some(Fallback::Bundle(fallback));
|
||||
fallback_ref
|
||||
}
|
||||
// Freshly unsuspended. Detach fallback from the DOM, then shift children into it.
|
||||
(false, Some(_)) => {
|
||||
suspense
|
||||
.fallback_bundle
|
||||
.take()
|
||||
.unwrap() // We just matched Some(_)
|
||||
.detach(root, parent, false);
|
||||
match suspense.fallback.take() {
|
||||
Some(Fallback::Bundle(bundle)) => {
|
||||
bundle.detach(root, parent, false);
|
||||
}
|
||||
#[cfg(feature = "hydration")]
|
||||
Some(Fallback::Fragment(fragment)) => {
|
||||
fragment.detach(root, parent, false);
|
||||
}
|
||||
None => {
|
||||
unreachable!("None condition has been checked before.")
|
||||
}
|
||||
};
|
||||
|
||||
children_bundle.shift(parent, next_sibling.clone());
|
||||
children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle)
|
||||
@ -182,3 +233,62 @@ impl Reconcilable for VSuspense {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::{Fragment, Hydratable};
|
||||
use crate::virtual_dom::Collectable;
|
||||
|
||||
impl Hydratable for VSuspense {
|
||||
fn hydrate(
|
||||
self,
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let detached_parent = document()
|
||||
.create_element("div")
|
||||
.expect("failed to create detached element");
|
||||
|
||||
let collectable = Collectable::Suspense;
|
||||
let fallback_fragment = Fragment::collect_between(fragment, &collectable, parent);
|
||||
|
||||
let mut nodes = fallback_fragment.deep_clone();
|
||||
|
||||
for node in nodes.iter() {
|
||||
detached_parent.append_child(node).unwrap();
|
||||
}
|
||||
|
||||
let (_, children_bundle) =
|
||||
self.children
|
||||
.hydrate(root, parent_scope, &detached_parent, &mut nodes);
|
||||
|
||||
// We trim all leading text nodes before checking as it's likely these are whitespaces.
|
||||
nodes.trim_start_text_nodes(&detached_parent);
|
||||
|
||||
assert!(nodes.is_empty(), "expected end of suspense, found node.");
|
||||
|
||||
let node_ref = fallback_fragment
|
||||
.front()
|
||||
.cloned()
|
||||
.map(NodeRef::new)
|
||||
.unwrap_or_default();
|
||||
|
||||
(
|
||||
node_ref,
|
||||
BSuspense {
|
||||
children_bundle,
|
||||
detached_parent,
|
||||
key: self.key,
|
||||
|
||||
// We start hydration with the BSuspense being suspended.
|
||||
// A subsequent render will resume the BSuspense if not needed to be suspended.
|
||||
fallback: Some(Fallback::Fragment(fallback_fragment)),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,6 +287,98 @@ impl BTag {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::{node_type_str, Fragment, Hydratable};
|
||||
use web_sys::Node;
|
||||
|
||||
impl Hydratable for VTag {
|
||||
fn hydrate(
|
||||
self,
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let tag_name = self.tag().to_owned();
|
||||
|
||||
let Self {
|
||||
inner,
|
||||
listeners,
|
||||
attributes,
|
||||
node_ref,
|
||||
key,
|
||||
} = self;
|
||||
|
||||
// We trim all text nodes as it's likely these are whitespaces.
|
||||
fragment.trim_start_text_nodes(parent);
|
||||
|
||||
let node = fragment
|
||||
.pop_front()
|
||||
.unwrap_or_else(|| panic!("expected element of type {}, found EOF.", tag_name));
|
||||
|
||||
assert_eq!(
|
||||
node.node_type(),
|
||||
Node::ELEMENT_NODE,
|
||||
"expected element, found node type {}.",
|
||||
node_type_str(&node),
|
||||
);
|
||||
let el = node.dyn_into::<Element>().expect("expected an element.");
|
||||
|
||||
assert_eq!(
|
||||
el.tag_name().to_lowercase(),
|
||||
tag_name,
|
||||
"expected element of kind {}, found {}.",
|
||||
tag_name,
|
||||
el.tag_name().to_lowercase(),
|
||||
);
|
||||
|
||||
// We simply registers listeners and updates all attributes.
|
||||
let attributes = attributes.apply(root, &el);
|
||||
let listeners = listeners.apply(root, &el);
|
||||
|
||||
// For input and textarea elements, we update their value anyways.
|
||||
let inner = match inner {
|
||||
VTagInner::Input(f) => {
|
||||
let f = f.apply(root, el.unchecked_ref());
|
||||
BTagInner::Input(f)
|
||||
}
|
||||
VTagInner::Textarea { value } => {
|
||||
let value = value.apply(root, el.unchecked_ref());
|
||||
|
||||
BTagInner::Textarea { value }
|
||||
}
|
||||
VTagInner::Other { children, tag } => {
|
||||
let mut nodes = Fragment::collect_children(&el);
|
||||
let (_, child_bundle) = children.hydrate(root, parent_scope, &el, &mut nodes);
|
||||
|
||||
nodes.trim_start_text_nodes(parent);
|
||||
|
||||
assert!(nodes.is_empty(), "expected EOF, found node.");
|
||||
|
||||
BTagInner::Other { child_bundle, tag }
|
||||
}
|
||||
};
|
||||
|
||||
node_ref.set(Some((*el).clone()));
|
||||
|
||||
(
|
||||
node_ref.clone(),
|
||||
BTag {
|
||||
inner,
|
||||
listeners,
|
||||
attributes,
|
||||
reference: el,
|
||||
node_ref,
|
||||
key,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@ -89,6 +89,66 @@ impl std::fmt::Debug for BText {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use web_sys::Node;
|
||||
|
||||
use crate::dom_bundle::{Fragment, Hydratable};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
impl Hydratable for VText {
|
||||
fn hydrate(
|
||||
self,
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
if let Some(m) = fragment.front().cloned() {
|
||||
// better safe than sorry.
|
||||
if m.node_type() == Node::TEXT_NODE {
|
||||
if let Ok(m) = m.dyn_into::<TextNode>() {
|
||||
// pop current node.
|
||||
fragment.pop_front();
|
||||
|
||||
// TODO: It may make sense to assert the text content in the text node against
|
||||
// the VText when #[cfg(debug_assertions)] is true, but this may be complicated.
|
||||
// We always replace the text value for now.
|
||||
//
|
||||
// Please see the next comment for a detailed explanation.
|
||||
m.set_node_value(Some(self.text.as_ref()));
|
||||
|
||||
return (
|
||||
NodeRef::new(m.clone().into()),
|
||||
BText {
|
||||
text: self.text,
|
||||
text_node: m,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are multiple text nodes placed back-to-back in SSR, it may be parsed as a single
|
||||
// text node by browser, hence we need to add extra text nodes here if the next node is not a text node.
|
||||
// Similarly, the value of the text node may be a combination of multiple VText vnodes.
|
||||
// So we always need to override their values.
|
||||
self.attach(
|
||||
root,
|
||||
parent_scope,
|
||||
parent,
|
||||
fragment
|
||||
.front()
|
||||
.cloned()
|
||||
.map(NodeRef::new)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
extern crate self as yew;
|
||||
|
||||
166
packages/yew/src/dom_bundle/fragment.rs
Normal file
166
packages/yew/src/dom_bundle/fragment.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
use super::BSubtree;
|
||||
use crate::html::NodeRef;
|
||||
use crate::virtual_dom::Collectable;
|
||||
|
||||
/// A Hydration Fragment
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Fragment(VecDeque<Node>);
|
||||
|
||||
impl Deref for Fragment {
|
||||
type Target = VecDeque<Node>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Fragment {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Fragment {
|
||||
/// Collects child nodes of an element into a VecDeque.
|
||||
pub fn collect_children(parent: &Element) -> Self {
|
||||
let mut fragment = VecDeque::with_capacity(parent.child_nodes().length() as usize);
|
||||
|
||||
let mut current_node = parent.first_child();
|
||||
|
||||
// This is easier than iterating child nodes at the moment
|
||||
// as we don't have to downcast iterator values.
|
||||
while let Some(m) = current_node {
|
||||
current_node = m.next_sibling();
|
||||
fragment.push_back(m);
|
||||
}
|
||||
|
||||
Self(fragment)
|
||||
}
|
||||
|
||||
/// Collects nodes for a Component Bundle or a BSuspense.
|
||||
pub fn collect_between(
|
||||
collect_from: &mut Fragment,
|
||||
collect_for: &Collectable,
|
||||
parent: &Element,
|
||||
) -> Self {
|
||||
let is_open_tag = |node: &Node| {
|
||||
let comment_text = node.text_content().unwrap_or_else(|| "".to_string());
|
||||
|
||||
comment_text.starts_with(collect_for.open_start_mark())
|
||||
&& comment_text.ends_with(collect_for.end_mark())
|
||||
};
|
||||
|
||||
let is_close_tag = |node: &Node| {
|
||||
let comment_text = node.text_content().unwrap_or_else(|| "".to_string());
|
||||
|
||||
comment_text.starts_with(collect_for.close_start_mark())
|
||||
&& comment_text.ends_with(collect_for.end_mark())
|
||||
};
|
||||
|
||||
// We trim all leading text nodes as it's likely these are whitespaces.
|
||||
collect_from.trim_start_text_nodes(parent);
|
||||
|
||||
let first_node = collect_from
|
||||
.pop_front()
|
||||
.unwrap_or_else(|| panic!("expected {} opening tag, found EOF", collect_for.name()));
|
||||
|
||||
assert_eq!(
|
||||
first_node.node_type(),
|
||||
Node::COMMENT_NODE,
|
||||
// TODO: improve error message with human readable node type name.
|
||||
"expected {} start, found node type {}",
|
||||
collect_for.name(),
|
||||
first_node.node_type()
|
||||
);
|
||||
|
||||
let mut nodes = VecDeque::new();
|
||||
|
||||
if !is_open_tag(&first_node) {
|
||||
panic!(
|
||||
"expected {} opening tag, found comment node",
|
||||
collect_for.name()
|
||||
);
|
||||
}
|
||||
|
||||
// We remove the opening tag.
|
||||
parent.remove_child(&first_node).unwrap();
|
||||
|
||||
let mut nested_layers = 1;
|
||||
|
||||
loop {
|
||||
let current_node = collect_from.pop_front().unwrap_or_else(|| {
|
||||
panic!("expected {} closing tag, found EOF", collect_for.name())
|
||||
});
|
||||
|
||||
if current_node.node_type() == Node::COMMENT_NODE {
|
||||
if is_open_tag(¤t_node) {
|
||||
// We found another opening tag, we need to increase component counter.
|
||||
nested_layers += 1;
|
||||
} else if is_close_tag(¤t_node) {
|
||||
// We found a closing tag, minus component counter.
|
||||
nested_layers -= 1;
|
||||
if nested_layers == 0 {
|
||||
// We have found the end of the current tag we are collecting, breaking
|
||||
// the loop.
|
||||
|
||||
// We remove the closing tag.
|
||||
parent.remove_child(¤t_node).unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes.push_back(current_node.clone());
|
||||
}
|
||||
|
||||
Self(nodes)
|
||||
}
|
||||
|
||||
/// Remove child nodes until first non-text node.
|
||||
pub fn trim_start_text_nodes(&mut self, parent: &Element) {
|
||||
while let Some(ref m) = self.front().cloned() {
|
||||
if m.node_type() == Node::TEXT_NODE {
|
||||
self.pop_front();
|
||||
|
||||
parent.remove_child(m).unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deeply clones all nodes.
|
||||
pub fn deep_clone(&self) -> Self {
|
||||
let nodes = self
|
||||
.iter()
|
||||
.map(|m| m.clone_node_with_deep(true).expect("failed to clone node."))
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
Self(nodes)
|
||||
}
|
||||
|
||||
// detaches current fragment.
|
||||
pub fn detach(self, _root: &BSubtree, parent: &Element, parent_to_detach: bool) {
|
||||
if !parent_to_detach {
|
||||
for node in self.iter() {
|
||||
parent
|
||||
.remove_child(node)
|
||||
.expect("failed to remove child element");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shift current Fragment into a different position in the dom.
|
||||
pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
||||
for node in self.iter() {
|
||||
next_parent
|
||||
.insert_before(node, next_sibling.get().as_ref())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,12 @@
|
||||
//! In order to efficiently implement updates, and diffing, additional information has to be
|
||||
//! kept around. This information is carried in the bundle.
|
||||
|
||||
use web_sys::Element;
|
||||
|
||||
use crate::html::AnyScope;
|
||||
use crate::html::NodeRef;
|
||||
use crate::virtual_dom::VNode;
|
||||
|
||||
mod bcomp;
|
||||
mod blist;
|
||||
mod bnode;
|
||||
@ -14,31 +20,35 @@ mod btag;
|
||||
mod btext;
|
||||
mod subtree_root;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod fragment;
|
||||
|
||||
mod traits;
|
||||
mod utils;
|
||||
|
||||
use web_sys::Element;
|
||||
|
||||
use crate::html::AnyScope;
|
||||
use crate::html::NodeRef;
|
||||
use crate::virtual_dom::VNode;
|
||||
|
||||
use bcomp::BComp;
|
||||
use blist::BList;
|
||||
use bnode::BNode;
|
||||
use bportal::BPortal;
|
||||
use bsuspense::BSuspense;
|
||||
|
||||
use btag::{BTag, Registry};
|
||||
use btext::BText;
|
||||
use subtree_root::EventDescriptor;
|
||||
use traits::{Reconcilable, ReconcileTarget};
|
||||
use utils::{insert_node, test_log};
|
||||
|
||||
#[doc(hidden)] // Publically exported from crate::events
|
||||
pub use subtree_root::set_event_bubbling;
|
||||
|
||||
pub(crate) use subtree_root::BSubtree;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
pub(crate) use fragment::Fragment;
|
||||
#[cfg(feature = "hydration")]
|
||||
use traits::Hydratable;
|
||||
#[cfg(feature = "hydration")]
|
||||
use utils::node_type_str;
|
||||
|
||||
/// A Bundle.
|
||||
///
|
||||
/// Each component holds a bundle that represents a realised layout, designated by a [VNode].
|
||||
@ -50,6 +60,7 @@ pub(crate) struct Bundle(BNode);
|
||||
|
||||
impl Bundle {
|
||||
/// Creates a new bundle.
|
||||
|
||||
pub const fn new() -> Self {
|
||||
Self(BNode::List(BList::new()))
|
||||
}
|
||||
@ -76,3 +87,22 @@ impl Bundle {
|
||||
self.0.detach(root, parent, parent_to_detach);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
impl Bundle {
|
||||
/// Creates a bundle by hydrating a virtual dom layout.
|
||||
pub fn hydrate(
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
node: VNode,
|
||||
) -> (NodeRef, Self) {
|
||||
let (node_ref, bundle) = node.hydrate(root, parent_scope, parent, fragment);
|
||||
(node_ref, Self(bundle))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ pub(super) trait Reconcilable {
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn attach(
|
||||
self,
|
||||
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
@ -59,6 +60,7 @@ pub(super) trait Reconcilable {
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn reconcile_node(
|
||||
self,
|
||||
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
@ -78,6 +80,7 @@ pub(super) trait Reconcilable {
|
||||
/// Replace an existing bundle by attaching self and detaching the existing one
|
||||
fn replace(
|
||||
self,
|
||||
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
@ -94,3 +97,30 @@ pub(super) trait Reconcilable {
|
||||
self_ref
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::Fragment;
|
||||
|
||||
pub(in crate::dom_bundle) trait Hydratable: Reconcilable {
|
||||
/// hydrates current tree.
|
||||
///
|
||||
/// Returns a reference to the first node of the hydrated tree.
|
||||
///
|
||||
/// # Important
|
||||
///
|
||||
/// DOM tree is hydrated from top to bottom. This is different than [`Reconcilable`].
|
||||
fn hydrate(
|
||||
self,
|
||||
root: &BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
fragment: &mut Fragment,
|
||||
) -> (NodeRef, Self::Bundle);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
pub(in crate::dom_bundle) use feat_hydration::*;
|
||||
|
||||
@ -26,3 +26,41 @@ macro_rules! test_log {
|
||||
/// Log an operation during tests for debugging purposes
|
||||
/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate.
|
||||
pub(super) use test_log;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::Element;
|
||||
|
||||
pub(in crate::dom_bundle) fn node_type_str(node: &Node) -> Cow<'static, str> {
|
||||
match node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
let tag = node
|
||||
.dyn_ref::<Element>()
|
||||
.map(|m| m.tag_name().to_lowercase())
|
||||
.unwrap_or_else(|| "unknown".to_owned());
|
||||
|
||||
format!("{} element node", tag).into()
|
||||
}
|
||||
Node::ATTRIBUTE_NODE => "attribute node".into(),
|
||||
Node::TEXT_NODE => "text node".into(),
|
||||
Node::CDATA_SECTION_NODE => "cdata section node".into(),
|
||||
Node::ENTITY_REFERENCE_NODE => "entity reference node".into(),
|
||||
Node::ENTITY_NODE => "entity node".into(),
|
||||
Node::PROCESSING_INSTRUCTION_NODE => "processing instruction node".into(),
|
||||
Node::COMMENT_NODE => "comment node".into(),
|
||||
Node::DOCUMENT_NODE => "document node".into(),
|
||||
Node::DOCUMENT_TYPE_NODE => "document type node".into(),
|
||||
Node::DOCUMENT_FRAGMENT_NODE => "document fragment node".into(),
|
||||
Node::NOTATION_NODE => "notation node".into(),
|
||||
_ => "unknown node".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
pub(super) use feat_hydration::*;
|
||||
|
||||
@ -26,12 +26,13 @@ use std::cell::RefCell;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod hooks;
|
||||
pub use hooks::*;
|
||||
|
||||
use crate::html::Context;
|
||||
|
||||
use crate::html::sealed::SealedBaseComponent;
|
||||
use crate::html::Context;
|
||||
|
||||
/// This attribute creates a function component from a normal Rust function.
|
||||
///
|
||||
@ -85,6 +86,19 @@ pub struct HookContext {
|
||||
}
|
||||
|
||||
impl HookContext {
|
||||
fn new(scope: AnyScope, re_render: ReRender) -> RefCell<Self> {
|
||||
RefCell::new(HookContext {
|
||||
effects: Vec::new(),
|
||||
scope,
|
||||
re_render,
|
||||
states: Vec::new(),
|
||||
|
||||
counter: 0,
|
||||
#[cfg(debug_assertions)]
|
||||
total_hook_counter: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn next_state<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
|
||||
where
|
||||
T: 'static,
|
||||
@ -103,7 +117,7 @@ impl HookContext {
|
||||
}
|
||||
};
|
||||
|
||||
state.downcast().unwrap()
|
||||
state.downcast().unwrap_throw()
|
||||
}
|
||||
|
||||
pub(crate) fn next_effect<T>(&mut self, initializer: impl FnOnce(ReRender) -> T) -> Rc<T>
|
||||
@ -120,6 +134,59 @@ impl HookContext {
|
||||
|
||||
t
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn prepare_run(&mut self) {
|
||||
self.counter = 0;
|
||||
}
|
||||
|
||||
/// asserts hook counter.
|
||||
///
|
||||
/// This function asserts that the number of hooks matches for every render.
|
||||
#[cfg(debug_assertions)]
|
||||
fn assert_hook_context(&mut self, render_ok: bool) {
|
||||
// Procedural Macros can catch most conditionally called hooks at compile time, but it cannot
|
||||
// detect early return (as the return can be Err(_), Suspension).
|
||||
match (render_ok, self.total_hook_counter) {
|
||||
// First rendered,
|
||||
// we store the hook counter.
|
||||
(true, None) => {
|
||||
self.total_hook_counter = Some(self.counter);
|
||||
}
|
||||
// Component is suspended before it's first rendered.
|
||||
// We don't have a total count to compare with.
|
||||
(false, None) => {}
|
||||
|
||||
// Subsequent render,
|
||||
// we compare stored total count and current render count.
|
||||
(true, Some(total_hook_counter)) => assert_eq!(
|
||||
total_hook_counter, self.counter,
|
||||
"Hooks are called conditionally."
|
||||
),
|
||||
|
||||
// Subsequent suspension,
|
||||
// components can have less hooks called when suspended, but not more.
|
||||
(false, Some(total_hook_counter)) => assert!(
|
||||
self.counter <= total_hook_counter,
|
||||
"Hooks are called conditionally."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_effects(&self) {
|
||||
for effect in self.effects.iter() {
|
||||
effect.rendered();
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_states(&mut self) {
|
||||
// We clear the effects as these are also references to states.
|
||||
self.effects.clear();
|
||||
|
||||
for state in self.states.drain(..) {
|
||||
drop(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HookContext {
|
||||
@ -167,21 +234,14 @@ where
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let scope = AnyScope::from(ctx.link().clone());
|
||||
|
||||
let re_render = {
|
||||
let link = ctx.link().clone();
|
||||
Rc::new(move || link.send_message(()))
|
||||
};
|
||||
|
||||
Self {
|
||||
_never: std::marker::PhantomData::default(),
|
||||
hook_ctx: RefCell::new(HookContext {
|
||||
effects: Vec::new(),
|
||||
scope,
|
||||
re_render: {
|
||||
let link = ctx.link().clone();
|
||||
Rc::new(move || link.send_message(()))
|
||||
},
|
||||
states: Vec::new(),
|
||||
|
||||
counter: 0,
|
||||
#[cfg(debug_assertions)]
|
||||
total_hook_counter: None,
|
||||
}),
|
||||
hook_ctx: HookContext::new(scope, re_render),
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,56 +255,27 @@ where
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> HtmlResult {
|
||||
let props = ctx.props();
|
||||
let mut ctx = self.hook_ctx.borrow_mut();
|
||||
ctx.counter = 0;
|
||||
let mut hook_ctx = self.hook_ctx.borrow_mut();
|
||||
|
||||
hook_ctx.prepare_run();
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
let result = T::run(&mut *ctx, props);
|
||||
let result = T::run(&mut *hook_ctx, props);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Procedural Macros can catch most conditionally called hooks at compile time, but it cannot
|
||||
// detect early return (as the return can be Err(_), Suspension).
|
||||
if result.is_err() {
|
||||
if let Some(m) = ctx.total_hook_counter {
|
||||
// Suspended Components can have less hooks called when suspended, but not more.
|
||||
if m < ctx.counter {
|
||||
panic!("Hooks are called conditionally.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match ctx.total_hook_counter {
|
||||
Some(m) => {
|
||||
if m != ctx.counter {
|
||||
panic!("Hooks are called conditionally.");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ctx.total_hook_counter = Some(ctx.counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hook_ctx.assert_hook_context(result.is_ok());
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||
let hook_ctx = self.hook_ctx.borrow();
|
||||
|
||||
for effect in hook_ctx.effects.iter() {
|
||||
effect.rendered();
|
||||
}
|
||||
hook_ctx.run_effects();
|
||||
}
|
||||
|
||||
fn destroy(&mut self, _ctx: &Context<Self>) {
|
||||
let mut hook_ctx = self.hook_ctx.borrow_mut();
|
||||
// We clear the effects as these are also references to states.
|
||||
hook_ctx.effects.clear();
|
||||
|
||||
for state in hook_ctx.states.drain(..) {
|
||||
drop(state);
|
||||
}
|
||||
hook_ctx.drain_states();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
use crate::html::Html;
|
||||
use crate::virtual_dom::{VChild, VNode};
|
||||
use crate::Properties;
|
||||
use std::fmt;
|
||||
|
||||
/// A type used for accepting children elements in Component::Properties.
|
||||
@ -208,3 +209,11 @@ impl<T> IntoIterator for ChildrenRenderer<T> {
|
||||
self.children.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A [Properties] type with Children being the only property.
|
||||
#[derive(Debug, Properties, PartialEq)]
|
||||
pub struct ChildrenProps {
|
||||
/// The Children of a Component.
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
//! Component lifecycle module
|
||||
|
||||
use super::scope::{AnyScope, Scope};
|
||||
|
||||
use super::BaseComponent;
|
||||
#[cfg(feature = "hydration")]
|
||||
use crate::html::RenderMode;
|
||||
use crate::html::{Html, RenderError};
|
||||
use crate::scheduler::{self, Runnable, Shared};
|
||||
use crate::suspense::{BaseSuspense, Suspension};
|
||||
@ -9,6 +12,8 @@ use crate::{Callback, Context, HtmlResult};
|
||||
use std::any::Any;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
use crate::dom_bundle::Fragment;
|
||||
#[cfg(feature = "csr")]
|
||||
use crate::dom_bundle::{BSubtree, Bundle};
|
||||
#[cfg(feature = "csr")]
|
||||
@ -25,6 +30,14 @@ pub(crate) enum ComponentRenderState {
|
||||
next_sibling: NodeRef,
|
||||
node_ref: NodeRef,
|
||||
},
|
||||
#[cfg(feature = "hydration")]
|
||||
Hydration {
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
node_ref: NodeRef,
|
||||
root: BSubtree,
|
||||
fragment: Fragment,
|
||||
},
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
Ssr {
|
||||
@ -38,7 +51,7 @@ impl std::fmt::Debug for ComponentRenderState {
|
||||
#[cfg(feature = "csr")]
|
||||
Self::Render {
|
||||
ref bundle,
|
||||
ref root,
|
||||
root,
|
||||
ref parent,
|
||||
ref next_sibling,
|
||||
ref node_ref,
|
||||
@ -51,6 +64,22 @@ impl std::fmt::Debug for ComponentRenderState {
|
||||
.field("node_ref", node_ref)
|
||||
.finish(),
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
Self::Hydration {
|
||||
ref fragment,
|
||||
ref parent,
|
||||
ref next_sibling,
|
||||
ref node_ref,
|
||||
ref root,
|
||||
} => f
|
||||
.debug_struct("ComponentRenderState::Hydration")
|
||||
.field("fragment", fragment)
|
||||
.field("root", root)
|
||||
.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 {
|
||||
@ -82,6 +111,18 @@ impl ComponentRenderState {
|
||||
*parent = next_parent;
|
||||
*next_sibling = next_next_sibling;
|
||||
}
|
||||
#[cfg(feature = "hydration")]
|
||||
Self::Hydration {
|
||||
fragment,
|
||||
parent,
|
||||
next_sibling,
|
||||
..
|
||||
} => {
|
||||
fragment.shift(&next_parent, next_next_sibling.clone());
|
||||
|
||||
*parent = next_parent;
|
||||
*next_sibling = next_next_sibling;
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
Self::Ssr { .. } => {
|
||||
@ -192,7 +233,22 @@ impl ComponentState {
|
||||
props: Rc<COMP::Properties>,
|
||||
) -> Self {
|
||||
let comp_id = scope.id;
|
||||
let context = Context { scope, props };
|
||||
#[cfg(feature = "hydration")]
|
||||
let mode = {
|
||||
match initial_render_state {
|
||||
ComponentRenderState::Render { .. } => RenderMode::Render,
|
||||
ComponentRenderState::Hydration { .. } => RenderMode::Hydration,
|
||||
#[cfg(feature = "ssr")]
|
||||
ComponentRenderState::Ssr { .. } => RenderMode::Ssr,
|
||||
}
|
||||
};
|
||||
|
||||
let context = Context {
|
||||
scope,
|
||||
props,
|
||||
#[cfg(feature = "hydration")]
|
||||
mode,
|
||||
};
|
||||
|
||||
let inner = Box::new(CompStateInner {
|
||||
component: COMP::create(&context),
|
||||
@ -280,6 +336,20 @@ impl Runnable for UpdateRunner {
|
||||
state.inner.props_changed(props)
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
ComponentRenderState::Hydration {
|
||||
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)]
|
||||
@ -331,14 +401,27 @@ impl Runnable for DestroyRunner {
|
||||
ComponentRenderState::Render {
|
||||
bundle,
|
||||
ref parent,
|
||||
ref root,
|
||||
ref node_ref,
|
||||
ref root,
|
||||
..
|
||||
} => {
|
||||
bundle.detach(root, parent, self.parent_to_detach);
|
||||
|
||||
node_ref.set(None);
|
||||
}
|
||||
// We need to detach the hydrate fragment if the component is not hydrated.
|
||||
#[cfg(feature = "hydration")]
|
||||
ComponentRenderState::Hydration {
|
||||
ref root,
|
||||
fragment,
|
||||
ref parent,
|
||||
ref node_ref,
|
||||
..
|
||||
} => {
|
||||
fragment.detach(root, parent, self.parent_to_detach);
|
||||
|
||||
node_ref.set(None);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
ComponentRenderState::Ssr { .. } => {}
|
||||
@ -436,6 +519,7 @@ impl RenderRunner {
|
||||
..
|
||||
} => {
|
||||
let scope = state.inner.any_scope();
|
||||
|
||||
let new_node_ref =
|
||||
bundle.reconcile(root, &scope, parent, next_sibling.clone(), new_root);
|
||||
node_ref.link(new_node_ref);
|
||||
@ -453,6 +537,46 @@ impl RenderRunner {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
ComponentRenderState::Hydration {
|
||||
ref mut fragment,
|
||||
ref parent,
|
||||
ref node_ref,
|
||||
ref next_sibling,
|
||||
ref root,
|
||||
} => {
|
||||
// We schedule a "first" render to run immediately after hydration,
|
||||
// to fix NodeRefs (first_node and next_sibling).
|
||||
scheduler::push_component_first_render(
|
||||
state.comp_id,
|
||||
Box::new(RenderRunner {
|
||||
state: self.state.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
let scope = state.inner.any_scope();
|
||||
|
||||
// This first node is not guaranteed to be correct here.
|
||||
// As it may be a comment node that is removed afterwards.
|
||||
// but we link it anyways.
|
||||
let (node, bundle) = Bundle::hydrate(root, &scope, parent, fragment, new_root);
|
||||
|
||||
// We trim all text nodes before checking as it's likely these are whitespaces.
|
||||
fragment.trim_start_text_nodes(parent);
|
||||
|
||||
assert!(fragment.is_empty(), "expected end of component, found node");
|
||||
|
||||
node_ref.link(node);
|
||||
|
||||
state.render_state = ComponentRenderState::Render {
|
||||
root: root.clone(),
|
||||
bundle,
|
||||
parent: parent.clone(),
|
||||
node_ref: node_ref.clone(),
|
||||
next_sibling: next_sibling.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
ComponentRenderState::Ssr { ref mut sender } => {
|
||||
if let Some(tx) = sender.take() {
|
||||
|
||||
147
packages/yew/src/html/component/marker.rs
Normal file
147
packages/yew/src/html/component/marker.rs
Normal file
@ -0,0 +1,147 @@
|
||||
//! Primitive Components & Properties Types
|
||||
|
||||
use crate::function_component;
|
||||
use crate::html;
|
||||
use crate::html::{ChildrenProps, Html, IntoComponent};
|
||||
|
||||
/// A Component to represent a component that does not exist in current implementation.
|
||||
///
|
||||
/// During Hydration, Yew expected the Virtual DOM hierarchy to match the the layout used in server-side
|
||||
/// rendering. However, sometimes it is possible / reasonable to omit certain components from one
|
||||
/// side of the implementation. This component is used to represent a component as if a component "existed"
|
||||
/// in the place it is defined.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// The Real DOM hierarchy must also match the server-side rendered artifact. This component is
|
||||
/// only usable when the original component does not introduce any additional elements. (e.g.: Context
|
||||
/// Providers)
|
||||
///
|
||||
/// A generic parameter is provided to help identify the component to be substituted.
|
||||
/// The type of the generic parameter is not required to be the same component that was in the other
|
||||
/// implementation. However, this behaviour may change in the future if more debug assertions were
|
||||
/// to be introduced. It is recommended that the generic parameter represents the component in the
|
||||
/// other implementation.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use yew::prelude::*;
|
||||
/// # use yew::html::ChildrenProps;
|
||||
/// #
|
||||
/// # #[function_component]
|
||||
/// # fn Comp(props: &ChildrenProps) -> Html {
|
||||
/// # Html::default()
|
||||
/// # }
|
||||
/// #
|
||||
/// # #[function_component]
|
||||
/// # fn Provider(props: &ChildrenProps) -> Html {
|
||||
/// # let children = props.children.clone();
|
||||
/// #
|
||||
/// # html! { <>{children}</> }
|
||||
/// # }
|
||||
/// # type Provider1 = Provider;
|
||||
/// # type Provider2 = Provider;
|
||||
/// # type Provider3 = Provider;
|
||||
/// # type Provider4 = Provider;
|
||||
///
|
||||
/// #[function_component]
|
||||
/// fn ServerApp() -> Html {
|
||||
/// // The Server Side Rendering Application has 3 Providers.
|
||||
/// html! {
|
||||
/// <Provider1>
|
||||
/// <Provider2>
|
||||
/// <Provider3>
|
||||
/// <Comp />
|
||||
/// </Provider3>
|
||||
/// </Provider2>
|
||||
/// </Provider1>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[function_component]
|
||||
/// fn App() -> Html {
|
||||
/// // The Client Side Rendering Application has 4 Providers.
|
||||
/// html! {
|
||||
/// <Provider1>
|
||||
/// <Provider2>
|
||||
/// <Provider3>
|
||||
///
|
||||
/// // This provider does not exist on the server-side
|
||||
/// // Hydration will fail due to Virtual DOM layout mismatch.
|
||||
/// <Provider4>
|
||||
/// <Comp />
|
||||
/// </Provider4>
|
||||
///
|
||||
/// </Provider3>
|
||||
/// </Provider2>
|
||||
/// </Provider1>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To mitigate this, we can use a `PhantomComponent`:
|
||||
///
|
||||
/// ```
|
||||
/// use yew::prelude::*;
|
||||
/// # use yew::html::{PhantomComponent, ChildrenProps};
|
||||
/// #
|
||||
/// # #[function_component]
|
||||
/// # fn Comp(props: &ChildrenProps) -> Html {
|
||||
/// # Html::default()
|
||||
/// # }
|
||||
/// #
|
||||
/// # #[function_component]
|
||||
/// # fn Provider(props: &ChildrenProps) -> Html {
|
||||
/// # let children = props.children.clone();
|
||||
/// #
|
||||
/// # html! { <>{children}</> }
|
||||
/// # }
|
||||
/// # type Provider1 = Provider;
|
||||
/// # type Provider2 = Provider;
|
||||
/// # type Provider3 = Provider;
|
||||
/// # type Provider4 = Provider;
|
||||
///
|
||||
/// #[function_component]
|
||||
/// fn ServerApp() -> Html {
|
||||
/// html! {
|
||||
/// <Provider1>
|
||||
/// <Provider2>
|
||||
/// <Provider3>
|
||||
/// // We add a PhantomComponent for Provider4,
|
||||
/// // it acts if a Provider4 component presents in this position.
|
||||
/// <PhantomComponent<Provider4>>
|
||||
/// <Comp />
|
||||
/// </PhantomComponent<Provider4>>
|
||||
/// </Provider3>
|
||||
/// </Provider2>
|
||||
/// </Provider1>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[function_component]
|
||||
/// fn App() -> Html {
|
||||
/// html! {
|
||||
/// <Provider1>
|
||||
/// <Provider2>
|
||||
/// <Provider3>
|
||||
///
|
||||
/// // Hydration will succeed as the PhantomComponent in the server-side
|
||||
/// // implementation will represent a Provider4 component in this position.
|
||||
/// <Provider4>
|
||||
/// <Comp />
|
||||
/// </Provider4>
|
||||
///
|
||||
/// </Provider3>
|
||||
/// </Provider2>
|
||||
/// </Provider1>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[function_component]
|
||||
pub fn PhantomComponent<T>(props: &ChildrenProps) -> Html
|
||||
where
|
||||
T: IntoComponent,
|
||||
{
|
||||
html! { <>{props.children.clone()}</> }
|
||||
}
|
||||
@ -3,12 +3,15 @@
|
||||
mod children;
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
mod lifecycle;
|
||||
mod marker;
|
||||
mod properties;
|
||||
mod scope;
|
||||
|
||||
use super::{Html, HtmlResult, IntoHtmlResult};
|
||||
pub use children::*;
|
||||
pub use marker::*;
|
||||
pub use properties::*;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub(crate) use scope::Scoped;
|
||||
pub use scope::{AnyScope, Scope, SendAsMessage};
|
||||
@ -44,6 +47,15 @@ mod feat_csr_ssr {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum RenderMode {
|
||||
Hydration,
|
||||
Render,
|
||||
#[cfg(feature = "ssr")]
|
||||
Ssr,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
pub(crate) use feat_csr_ssr::*;
|
||||
@ -54,6 +66,8 @@ pub(crate) use feat_csr_ssr::*;
|
||||
pub struct Context<COMP: BaseComponent> {
|
||||
scope: Scope<COMP>,
|
||||
props: Rc<COMP::Properties>,
|
||||
#[cfg(feature = "hydration")]
|
||||
mode: RenderMode,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Context<COMP> {
|
||||
@ -68,6 +82,11 @@ impl<COMP: BaseComponent> Context<COMP> {
|
||||
pub fn props(&self) -> &COMP::Properties {
|
||||
&*self.props
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
pub(crate) fn mode(&self) -> RenderMode {
|
||||
self.mode
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod sealed {
|
||||
|
||||
@ -43,16 +43,6 @@ impl<COMP: BaseComponent> From<Scope<COMP>> for AnyScope {
|
||||
}
|
||||
|
||||
impl AnyScope {
|
||||
#[cfg(feature = "csr")]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test() -> Self {
|
||||
Self {
|
||||
type_id: TypeId::of::<()>(),
|
||||
parent: None,
|
||||
typed_scope: Rc::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the parent scope
|
||||
pub fn get_parent(&self) -> Option<&AnyScope> {
|
||||
self.parent.as_deref()
|
||||
@ -417,6 +407,17 @@ mod feat_csr {
|
||||
use std::cell::Ref;
|
||||
use web_sys::Element;
|
||||
|
||||
impl AnyScope {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test() -> Self {
|
||||
Self {
|
||||
type_id: TypeId::of::<()>(),
|
||||
parent: None,
|
||||
typed_scope: Rc::new(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP> Scope<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
@ -424,6 +425,7 @@ mod feat_csr {
|
||||
/// Mounts a component with `props` to the specified `element` in the DOM.
|
||||
pub(crate) fn mount_in_place(
|
||||
&self,
|
||||
|
||||
root: BSubtree,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
@ -518,6 +520,83 @@ mod feat_csr {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "hydration")))]
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
use crate::dom_bundle::{BSubtree, Fragment};
|
||||
use crate::html::component::lifecycle::{ComponentRenderState, CreateRunner, RenderRunner};
|
||||
use crate::html::NodeRef;
|
||||
use crate::scheduler;
|
||||
use crate::virtual_dom::Collectable;
|
||||
|
||||
use web_sys::Element;
|
||||
|
||||
impl<COMP> Scope<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
/// Hydrates the component.
|
||||
///
|
||||
/// Returns a pending NodeRef of the next sibling.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This method is expected to collect all the elements belongs to the current component
|
||||
/// immediately.
|
||||
pub(crate) fn hydrate_in_place(
|
||||
&self,
|
||||
root: BSubtree,
|
||||
parent: Element,
|
||||
fragment: &mut Fragment,
|
||||
node_ref: NodeRef,
|
||||
props: Rc<COMP::Properties>,
|
||||
) {
|
||||
// This is very helpful to see which component is failing during hydration
|
||||
// which means this component may not having a stable layout / differs between
|
||||
// client-side and server-side.
|
||||
#[cfg(all(debug_assertions, feature = "trace_hydration"))]
|
||||
gloo::console::trace!(format!(
|
||||
"queuing hydration of: {}(ID: {:?})",
|
||||
std::any::type_name::<COMP>(),
|
||||
self.id
|
||||
));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let collectable = Collectable::Component(std::any::type_name::<COMP>());
|
||||
#[cfg(not(debug_assertions))]
|
||||
let collectable = Collectable::Component;
|
||||
|
||||
let fragment = Fragment::collect_between(fragment, &collectable, &parent);
|
||||
node_ref.set(fragment.front().cloned());
|
||||
let next_sibling = NodeRef::default();
|
||||
|
||||
let state = ComponentRenderState::Hydration {
|
||||
root,
|
||||
parent,
|
||||
node_ref,
|
||||
next_sibling,
|
||||
fragment,
|
||||
};
|
||||
|
||||
scheduler::push_component_create(
|
||||
self.id,
|
||||
Box::new(CreateRunner {
|
||||
initial_render_state: state,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
}),
|
||||
Box::new(RenderRunner {
|
||||
state: self.state.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "csr")]
|
||||
pub(crate) use feat_csr::*;
|
||||
|
||||
|
||||
@ -24,6 +24,9 @@
|
||||
//! - `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
|
||||
//! non-wasm32 targets.)
|
||||
//! - `hydration`: Enables Hydration support.
|
||||
//! - `trace_hydration`: Enables trace logging on hydration. (Implies `hydration`. You may want to enable this if you are
|
||||
//! trying to debug hydration layout mismatch.)
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
@ -280,6 +283,7 @@ pub mod utils;
|
||||
pub mod virtual_dom;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use server_renderer::*;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
mod app_handle;
|
||||
#[cfg(feature = "csr")]
|
||||
@ -317,6 +321,7 @@ pub mod prelude {
|
||||
//! # #![allow(unused_imports)]
|
||||
//! use yew::prelude::*;
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub use crate::app_handle::AppHandle;
|
||||
pub use crate::callback::Callback;
|
||||
|
||||
@ -92,3 +92,20 @@ where
|
||||
AppHandle::<ICOMP>::mount_with_props(self.root, Rc::new(self.props))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "hydration")))]
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
impl<ICOMP> Renderer<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent + 'static,
|
||||
{
|
||||
/// Hydrates the application.
|
||||
pub fn hydrate(self) -> AppHandle<ICOMP> {
|
||||
set_default_panic_hook();
|
||||
AppHandle::<ICOMP>::hydrate_with_props(self.root, Rc::new(self.props))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ mod feat_csr_ssr {
|
||||
with(|s| s.destroy.push(runnable));
|
||||
}
|
||||
|
||||
/// Push a component render and rendered [Runnable]s to be executed
|
||||
/// Push a component render [Runnable]s to be executed
|
||||
pub(crate) fn push_component_render(component_id: usize, render: Box<dyn Runnable>) {
|
||||
with(|s| {
|
||||
s.render.insert(component_id, render);
|
||||
@ -115,6 +115,20 @@ mod feat_csr {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
mod feat_hydration {
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn push_component_first_render(component_id: usize, render: Box<dyn Runnable>) {
|
||||
with(|s| {
|
||||
s.render_first.insert(component_id, render);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
pub(crate) use feat_hydration::*;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub(crate) use feat_csr::*;
|
||||
|
||||
|
||||
@ -13,15 +13,20 @@ pub struct SuspenseProps {
|
||||
mod feat_csr_ssr {
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
use crate::callback::Callback;
|
||||
#[cfg(feature = "hydration")]
|
||||
use crate::html::RenderMode;
|
||||
use crate::html::{Children, Component, Context, Html, Scope};
|
||||
use crate::suspense::Suspension;
|
||||
#[cfg(feature = "hydration")]
|
||||
use crate::suspense::SuspensionHandle;
|
||||
use crate::virtual_dom::{VNode, VSuspense};
|
||||
use crate::{function_component, html};
|
||||
|
||||
#[derive(Properties, PartialEq, Debug, Clone)]
|
||||
pub(crate) struct BaseSuspenseProps {
|
||||
pub children: Children,
|
||||
|
||||
pub fallback: Option<Html>,
|
||||
}
|
||||
|
||||
@ -35,6 +40,8 @@ mod feat_csr_ssr {
|
||||
pub(crate) struct BaseSuspense {
|
||||
link: Scope<Self>,
|
||||
suspensions: Vec<Suspension>,
|
||||
#[cfg(feature = "hydration")]
|
||||
hydration_handle: Option<SuspensionHandle>,
|
||||
}
|
||||
|
||||
impl Component for BaseSuspense {
|
||||
@ -42,9 +49,30 @@ mod feat_csr_ssr {
|
||||
type Message = BaseSuspenseMsg;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
#[cfg(not(feature = "hydration"))]
|
||||
let suspensions = Vec::new();
|
||||
|
||||
// We create a suspension to block suspense until its rendered method is notified.
|
||||
#[cfg(feature = "hydration")]
|
||||
let (suspensions, hydration_handle) = {
|
||||
match ctx.mode() {
|
||||
RenderMode::Hydration => {
|
||||
let link = ctx.link().clone();
|
||||
let (s, handle) = Suspension::new();
|
||||
s.listen(Callback::from(move |s| {
|
||||
link.send_message(BaseSuspenseMsg::Resume(s));
|
||||
}));
|
||||
(vec![s], Some(handle))
|
||||
}
|
||||
_ => (Vec::new(), None),
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
link: ctx.link().clone(),
|
||||
suspensions: Vec::new(),
|
||||
suspensions,
|
||||
#[cfg(feature = "hydration")]
|
||||
hydration_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +122,15 @@ mod feat_csr_ssr {
|
||||
None => children,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
fn rendered(&mut self, _ctx: &Context<Self>, first_render: bool) {
|
||||
if first_render {
|
||||
if let Some(m) = self.hydration_handle.take() {
|
||||
m.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseSuspense {
|
||||
|
||||
@ -179,7 +179,7 @@ mod tests_attr_value {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")] // & feature = "hydration"
|
||||
#[cfg(any(feature = "ssr", feature = "hydration"))]
|
||||
mod feat_ssr_hydration {
|
||||
/// A collectable.
|
||||
///
|
||||
@ -251,10 +251,21 @@ mod feat_ssr_hydration {
|
||||
w.push_str(self.end_mark());
|
||||
w.push_str("-->");
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
pub fn name(&self) -> super::Cow<'static, str> {
|
||||
match self {
|
||||
#[cfg(debug_assertions)]
|
||||
Self::Component(m) => format!("Component({})", m).into(),
|
||||
#[cfg(not(debug_assertions))]
|
||||
Self::Component => "Component".into(),
|
||||
Self::Suspense => "Suspense".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(any(feature = "ssr", feature = "hydration"))]
|
||||
pub(crate) use feat_ssr_hydration::*;
|
||||
|
||||
/// A collection of attributes for an element
|
||||
|
||||
@ -11,6 +11,8 @@ use crate::html::{AnyScope, Scope};
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
use crate::dom_bundle::BSubtree;
|
||||
#[cfg(feature = "hydration")]
|
||||
use crate::dom_bundle::Fragment;
|
||||
#[cfg(feature = "csr")]
|
||||
use crate::html::Scoped;
|
||||
#[cfg(feature = "csr")]
|
||||
@ -72,6 +74,16 @@ pub(crate) trait Mountable {
|
||||
parent_scope: &'a AnyScope,
|
||||
hydratable: bool,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
fn hydrate(
|
||||
self: Box<Self>,
|
||||
root: BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
fragment: &mut Fragment,
|
||||
node_ref: NodeRef,
|
||||
) -> Box<dyn Scoped>;
|
||||
}
|
||||
|
||||
pub(crate) struct PropsWrapper<COMP: BaseComponent> {
|
||||
@ -128,6 +140,21 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
fn hydrate(
|
||||
self: Box<Self>,
|
||||
root: BSubtree,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
fragment: &mut Fragment,
|
||||
node_ref: NodeRef,
|
||||
) -> Box<dyn Scoped> {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.hydrate_in_place(root, parent, fragment, node_ref, self.props);
|
||||
|
||||
Box::new(scope)
|
||||
}
|
||||
}
|
||||
|
||||
/// A virtual child component.
|
||||
@ -258,9 +285,10 @@ mod ssr_tests {
|
||||
}
|
||||
}
|
||||
|
||||
let renderer = ServerRenderer::<Comp>::new();
|
||||
|
||||
let s = renderer.render().await;
|
||||
let s = ServerRenderer::<Comp>::new()
|
||||
.hydratable(false)
|
||||
.render()
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
error[E0277]: the trait bound `Comp: yew::Component` is not satisfied
|
||||
--> tests/failed_tests/base_component_impl-fail.rs:6:6
|
||||
|
|
||||
6 | impl BaseComponent for Comp {
|
||||
| ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp`
|
||||
|
|
||||
= note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp`
|
||||
--> tests/failed_tests/base_component_impl-fail.rs:6:6
|
||||
|
|
||||
6 | impl BaseComponent for Comp {
|
||||
| ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp`
|
||||
|
|
||||
= note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp`
|
||||
note: required by a bound in `BaseComponent`
|
||||
--> src/html/component/mod.rs
|
||||
|
|
||||
| pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`
|
||||
--> src/html/component/mod.rs
|
||||
|
|
||||
| pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`
|
||||
|
||||
542
packages/yew/tests/hydration.rs
Normal file
542
packages/yew/tests/hydration.rs
Normal file
@ -0,0 +1,542 @@
|
||||
#![cfg(feature = "hydration")]
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{obtain_result, obtain_result_by_id};
|
||||
|
||||
use gloo::timers::future::sleep;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::{HtmlElement, HtmlTextAreaElement};
|
||||
use yew::prelude::*;
|
||||
use yew::suspense::{Suspension, SuspensionResult};
|
||||
use yew::{Renderer, ServerRenderer};
|
||||
|
||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn hydration_works() {
|
||||
#[function_component]
|
||||
fn Comp() -> Html {
|
||||
let ctr = use_state_eq(|| 0);
|
||||
|
||||
let onclick = {
|
||||
let ctr = ctr.clone();
|
||||
|
||||
Callback::from(move |_| {
|
||||
ctr.set(*ctr + 1);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div>
|
||||
{"Counter: "}{*ctr}
|
||||
<button {onclick} class="increase">{"+1"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn App() -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<Comp />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let s = ServerRenderer::<App>::new().render().await;
|
||||
|
||||
gloo::utils::document()
|
||||
.query_selector("#output")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.set_inner_html(&s);
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||
.hydrate();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
let result = obtain_result_by_id("output");
|
||||
|
||||
// no placeholders, hydration is successful.
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"<div><div>Counter: 0<button class="increase">+1</button></div></div>"#
|
||||
);
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".increase")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
let result = obtain_result_by_id("output");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"<div><div>Counter: 1<button class="increase">+1</button></div></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn hydration_with_suspense() {
|
||||
#[derive(PartialEq)]
|
||||
pub struct SleepState {
|
||||
s: Suspension,
|
||||
}
|
||||
|
||||
impl SleepState {
|
||||
fn new() -> Self {
|
||||
let (s, handle) = Suspension::new();
|
||||
|
||||
spawn_local(async move {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
handle.resume();
|
||||
});
|
||||
|
||||
Self { s }
|
||||
}
|
||||
}
|
||||
|
||||
impl Reducible for SleepState {
|
||||
type Action = ();
|
||||
|
||||
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
Self::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[hook]
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
let sleep_state = use_reducer(SleepState::new);
|
||||
|
||||
if sleep_state.s.resumed() {
|
||||
Ok(Rc::new(move || sleep_state.dispatch(())))
|
||||
} else {
|
||||
Err(sleep_state.s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Content)]
|
||||
fn content() -> HtmlResult {
|
||||
let resleep = use_sleep()?;
|
||||
|
||||
let value = use_state(|| 0);
|
||||
|
||||
let on_increment = {
|
||||
let value = value.clone();
|
||||
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
value.set(*value + 1);
|
||||
})
|
||||
};
|
||||
|
||||
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
|
||||
|
||||
Ok(html! {
|
||||
<div class="content-area">
|
||||
<div class="actual-result">{*value}</div>
|
||||
<button class="increase" onclick={on_increment}>{"increase"}</button>
|
||||
<div class="action-area">
|
||||
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"wait..."}</div>};
|
||||
|
||||
html! {
|
||||
<div id="result">
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let s = ServerRenderer::<App>::new().render().await;
|
||||
|
||||
gloo::utils::document()
|
||||
.query_selector("#output")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.set_inner_html(&s);
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||
.hydrate();
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
|
||||
// still hydrating, during hydration, the server rendered result is shown.
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<!--<[yew::functional::FunctionComponent<hydration::hydration_with_suspense::{{closure}}::Content>]>--><div class="content-area"><div class="actual-result">0</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div><!--</[yew::functional::FunctionComponent<hydration::hydration_with_suspense::{{closure}}::Content>]>-->"#
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
|
||||
// hydrated.
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">0</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".increase")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">1</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...</div>");
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">1</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn hydration_with_suspense_not_suspended_at_start() {
|
||||
#[derive(PartialEq)]
|
||||
pub struct SleepState {
|
||||
s: Option<Suspension>,
|
||||
}
|
||||
|
||||
impl SleepState {
|
||||
fn new() -> Self {
|
||||
Self { s: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Reducible for SleepState {
|
||||
type Action = ();
|
||||
|
||||
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
let (s, handle) = Suspension::new();
|
||||
|
||||
spawn_local(async move {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
handle.resume();
|
||||
});
|
||||
|
||||
Self { s: Some(s) }.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[hook]
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
let sleep_state = use_reducer(SleepState::new);
|
||||
|
||||
let s = match sleep_state.s.clone() {
|
||||
Some(m) => m,
|
||||
None => return Ok(Rc::new(move || sleep_state.dispatch(()))),
|
||||
};
|
||||
|
||||
if s.resumed() {
|
||||
Ok(Rc::new(move || sleep_state.dispatch(())))
|
||||
} else {
|
||||
Err(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(Content)]
|
||||
fn content() -> HtmlResult {
|
||||
let resleep = use_sleep()?;
|
||||
|
||||
let value = use_state(|| "I am writing a long story...".to_string());
|
||||
|
||||
let on_text_input = {
|
||||
let value = value.clone();
|
||||
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlTextAreaElement = e.target_unchecked_into();
|
||||
|
||||
value.set(input.value());
|
||||
})
|
||||
};
|
||||
|
||||
let on_take_a_break = Callback::from(move |_| (resleep.clone())());
|
||||
|
||||
Ok(html! {
|
||||
<div class="content-area">
|
||||
<textarea value={value.to_string()} oninput={on_text_input}></textarea>
|
||||
<div class="action-area">
|
||||
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"wait..."}</div>};
|
||||
|
||||
html! {
|
||||
<div id="result">
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let s = ServerRenderer::<App>::new().render().await;
|
||||
|
||||
gloo::utils::document()
|
||||
.query_selector("#output")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.set_inner_html(&s);
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||
.hydrate();
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><textarea>I am writing a long story...</textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...</div>");
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><textarea>I am writing a long story...</textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn hydration_nested_suspense_works() {
|
||||
#[derive(PartialEq)]
|
||||
pub struct SleepState {
|
||||
s: Suspension,
|
||||
}
|
||||
|
||||
impl SleepState {
|
||||
fn new() -> Self {
|
||||
let (s, handle) = Suspension::new();
|
||||
|
||||
spawn_local(async move {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
handle.resume();
|
||||
});
|
||||
|
||||
Self { s }
|
||||
}
|
||||
}
|
||||
|
||||
impl Reducible for SleepState {
|
||||
type Action = ();
|
||||
|
||||
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
Self::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[hook]
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
let sleep_state = use_reducer(SleepState::new);
|
||||
|
||||
if sleep_state.s.resumed() {
|
||||
Ok(Rc::new(move || sleep_state.dispatch(())))
|
||||
} else {
|
||||
Err(sleep_state.s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component(InnerContent)]
|
||||
fn inner_content() -> HtmlResult {
|
||||
let resleep = use_sleep()?;
|
||||
|
||||
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
|
||||
|
||||
Ok(html! {
|
||||
<div class="content-area">
|
||||
<div class="action-area">
|
||||
<button class="take-a-break2" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(Content)]
|
||||
fn content() -> HtmlResult {
|
||||
let resleep = use_sleep()?;
|
||||
|
||||
let fallback = html! {<div>{"wait...(inner)"}</div>};
|
||||
|
||||
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
|
||||
|
||||
Ok(html! {
|
||||
<div class="content-area">
|
||||
<div class="action-area">
|
||||
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
<Suspense {fallback}>
|
||||
<InnerContent />
|
||||
</Suspense>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"wait...(outer)"}</div>};
|
||||
|
||||
html! {
|
||||
<div id="result">
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let s = ServerRenderer::<App>::new().render().await;
|
||||
|
||||
gloo::utils::document()
|
||||
.query_selector("#output")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.set_inner_html(&s);
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||
.hydrate();
|
||||
|
||||
// outer suspense is hydrating...
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<!--<[yew::functional::FunctionComponent<hydration::hydration_nested_suspense_works::{{closure}}::Content>]>--><div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><!--<[yew::functional::FunctionComponent<yew::suspense::component::feat_csr_ssr::Suspense>]>--><!--<[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--<?>--><!--<[yew::functional::FunctionComponent<hydration::hydration_nested_suspense_works::{{closure}}::InnerContent>]>--><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div><!--</[yew::functional::FunctionComponent<hydration::hydration_nested_suspense_works::{{closure}}::InnerContent>]>--><!--</?>--><!--</[yew::suspense::component::feat_csr_ssr::BaseSuspense]>--><!--</[yew::functional::FunctionComponent<yew::suspense::component::feat_csr_ssr::Suspense>]>--></div><!--</[yew::functional::FunctionComponent<hydration::hydration_nested_suspense_works::{{closure}}::Content>]>-->"#
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// inner suspense is hydrating...
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><!--<[yew::functional::FunctionComponent<hydration::hydration_nested_suspense_works::{{closure}}::InnerContent>]>--><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div><!--</[yew::functional::FunctionComponent<hydration::hydration_nested_suspense_works::{{closure}}::InnerContent>]>--></div>"#
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// hydrated.
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
||||
);
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...(outer)</div>");
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
||||
);
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div>wait...(inner)</div></div>"#
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
||||
);
|
||||
}
|
||||
@ -126,11 +126,67 @@ suspended.
|
||||
With this approach, developers can build a client-agnostic, SSR ready
|
||||
application with data fetching with very little effort.
|
||||
|
||||
Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/simple_ssr)
|
||||
## SSR Hydration
|
||||
|
||||
Hydration is the process that connects a Yew application to the
|
||||
server-side generated HTML file. By default, `ServerRender` prints
|
||||
hydratable html string which includes additional information to facilitate hydration.
|
||||
When the `Renderer::hydrate` method is called, instead of start rendering from
|
||||
scratch, Yew will reconcile the Virtual DOM generated by the application
|
||||
with the html string generated by the server renderer.
|
||||
|
||||
:::caution
|
||||
|
||||
Server-side rendering is experiemental and currently has no hydration support.
|
||||
However, you can still use it to generate static websites.
|
||||
To successfully hydrate an html representation created by the
|
||||
`ServerRenderer`, the client must produce a Virtual DOM layout that
|
||||
exactly matches the one used for SSR including components that do not
|
||||
contain any elements. If you have any component that is only useful in
|
||||
one implementation, you may want to use a `PhantomComponent` to fill the
|
||||
position of the extra component.
|
||||
:::
|
||||
|
||||
## Component Lifecycle during hydration
|
||||
|
||||
During Hydration, components schedule 2 consecutive renders after it is
|
||||
created. Any effects are called after the second render completes.
|
||||
It is important to make sure that the render function of the your
|
||||
component is side-effect free. It should not mutate any states or trigger
|
||||
additional renders. If your component currently mutates states or triggers
|
||||
additional renders, move them into an `use_effect` hook.
|
||||
|
||||
It's possible to use Struct Components with server-side rendering in
|
||||
hydration, the view function will be called
|
||||
multiple times before the rendered function will be called.
|
||||
The DOM is considered as not connected until rendered function is called,
|
||||
you should prevent any access to rendered nodes
|
||||
until `rendered()` method is called.
|
||||
|
||||
## Example
|
||||
|
||||
```rust ,ignore
|
||||
use yew::prelude::*;
|
||||
use yew::Renderer;
|
||||
|
||||
#[function_component]
|
||||
fn App() -> Html {
|
||||
html! {<div>{"Hello, World!"}</div>}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let renderer = Renderer::<App>::new();
|
||||
|
||||
// hydrates everything under body element, removes trailing
|
||||
// elements (if any).
|
||||
renderer.hydrate();
|
||||
}
|
||||
```
|
||||
|
||||
Example: [simple\_ssr](https://github.com/yewstack/yew/tree/master/examples/simple_ssr)
|
||||
Example: [ssr\_router](https://github.com/yewstack/yew/tree/master/examples/ssr_router)
|
||||
|
||||
:::caution
|
||||
|
||||
Server-side rendering is currently experiemental. If you find a bug, please file
|
||||
an issue on [GitHub](https://github.com/yewstack/yew/issues/new?assignees=&labels=bug&template=bug_report.md&title=).
|
||||
|
||||
:::
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user