mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
#[cfg(feature = "render")] and yew::Renderer (#2498)
* Bring changes to this branch. * Bring changes to this branch. * Add feature render and renderer. * Bring changes to this branch. * Migrate examples to Renderer. * Satisfy no any render. * Satisfy ssr. * Satisfy feature render. * Lint feature soundness. * Suppress tests. * Fix pr-flow, update docs. * Add a notice. * Adjust visibility. * Correctly feature gate tests. * make test scope available under feature render. * Fix CI. * Fix CI. * Restore tests module to its original place as well. * Make bundles crate private. * Make most bundle APIs private. * Adjust docs. * Adjust debug implementation. * Replace start_app with Renderer. * Adjust documentation. * Remove unused lint. * Remove start_app from docs. * DomBundle -> ReconcileTarget. * Adjust documentation. * Once render, now csr. * Fix docs as well.
This commit is contained in:
parent
3ad454011d
commit
8bc2212716
17
.github/workflows/main-checks.yml
vendored
17
.github/workflows/main-checks.yml
vendored
@ -27,6 +27,14 @@ jobs:
|
||||
command: clippy
|
||||
args: --all-targets -- -D warnings
|
||||
|
||||
- name: Lint feature soundness
|
||||
run: |
|
||||
cargo clippy -- --deny=warnings
|
||||
cargo clippy --features=ssr -- --deny=warnings
|
||||
cargo clippy --features=csr -- --deny=warnings
|
||||
cargo clippy --all-features --all-targets -- --deny=warnings
|
||||
working-directory: packages/yew
|
||||
|
||||
|
||||
clippy-release:
|
||||
name: Clippy on release profile
|
||||
@ -49,6 +57,14 @@ jobs:
|
||||
command: clippy
|
||||
args: --all-targets --release -- -D warnings
|
||||
|
||||
- name: Lint feature soundness
|
||||
run: |
|
||||
cargo clippy --release -- --deny=warnings
|
||||
cargo clippy --release --features=ssr -- --deny=warnings
|
||||
cargo clippy --release --features=csr -- --deny=warnings
|
||||
cargo clippy --release --all-features --all-targets -- --deny=warnings
|
||||
working-directory: packages/yew
|
||||
|
||||
|
||||
doc_tests:
|
||||
name: Documentation Tests
|
||||
@ -180,4 +196,3 @@ jobs:
|
||||
with:
|
||||
command: test
|
||||
args: -p yew-macro test_html_lints --features lints
|
||||
|
||||
|
||||
@ -9,6 +9,6 @@ license = "MIT OR Apache-2.0"
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-logger = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
yew-agent = { path = "../../packages/yew-agent" }
|
||||
gloo-timers = "0.2"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
yew::start_app::<agents::App>();
|
||||
yew::Renderer::<agents::App>::new().render();
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ anyhow = "1.0"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo = "0.6"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@ -162,5 +162,5 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -6,5 +6,5 @@ license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
yew-agent = { path = "../../packages/yew-agent" }
|
||||
|
||||
@ -19,5 +19,5 @@ pub fn App() -> Html {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -8,5 +8,5 @@ license = "MIT OR Apache-2.0"
|
||||
[dependencies]
|
||||
gloo-console = "0.2"
|
||||
js-sys = "0.3"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@ -72,5 +72,5 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
js-sys = "0.3"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
slab = "0.4.3"
|
||||
gloo = "0.6"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@ -55,14 +55,15 @@ impl Component for App {
|
||||
// Get the key for the entry and create and mount a new CounterModel app
|
||||
// with a callback that destroys the app when emitted
|
||||
let app_key = app_entry.key();
|
||||
let new_counter_app = yew::start_app_with_props_in_element(
|
||||
let new_counter_app = yew::Renderer::<CounterModel>::with_root_and_props(
|
||||
app_div.clone(),
|
||||
CounterProps {
|
||||
destroy_callback: ctx
|
||||
.link()
|
||||
.callback(move |_| Msg::DestroyCounterApp(app_key)),
|
||||
},
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
// Insert the app and the app div to our app collection
|
||||
app_entry.insert((app_div, new_counter_app));
|
||||
@ -107,5 +108,5 @@ impl Component for App {
|
||||
|
||||
fn main() {
|
||||
// Start main app
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
js-sys = "0.3"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo-file = "0.2"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@ -124,5 +124,5 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ gloo = "0.6"
|
||||
nanoid = "0.4"
|
||||
rand = "0.8"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@ -6,5 +6,5 @@ mod state;
|
||||
use crate::components::app::App;
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -21,3 +21,6 @@ wasm-logger = "0.2"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
instant = { version = "0.1" }
|
||||
|
||||
[features]
|
||||
csr = ["yew/csr"]
|
||||
|
||||
@ -11,6 +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" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
|
||||
@ -9,5 +9,6 @@ pub use app::*;
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
yew::start_app::<App>();
|
||||
#[cfg(feature = "render")]
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
gloo = "0.6"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@ -145,5 +145,5 @@ fn app() -> Html {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
|
||||
pulldown-cmark = { version = "0.9", default-features = false }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
yew = { path = "../../packages/yew", features = ["tokio"] }
|
||||
yew = { path = "../../packages/yew", features = ["tokio", "csr"] }
|
||||
gloo-utils = "0.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@ -128,5 +128,5 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -14,5 +14,5 @@ getrandom = { version = "0.2", features = ["js"] }
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
wasm-logger = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo-timers = "0.2"
|
||||
|
||||
@ -226,5 +226,5 @@ fn wrap(coord: isize, range: isize) -> usize {
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
log::trace!("Initializing yew...");
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo-utils = "0.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@ -26,5 +26,5 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@ -73,5 +73,5 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
wasm-logger = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@ -279,5 +279,5 @@ impl App {
|
||||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo-utils = "0.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@ -73,5 +73,5 @@ fn main() {
|
||||
|
||||
body.append_child(&mount_point).unwrap();
|
||||
|
||||
yew::start_app_in_element::<App>(mount_point);
|
||||
yew::Renderer::<App>::with_root(mount_point).render();
|
||||
}
|
||||
|
||||
@ -8,4 +8,4 @@ license = "MIT OR Apache-2.0"
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
wasm-logger = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
|
||||
@ -63,5 +63,5 @@ impl fmt::Display for Hovered {
|
||||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
yew::start_app::<app::App>();
|
||||
yew::Renderer::<app::App>::new().render();
|
||||
}
|
||||
|
||||
@ -6,5 +6,5 @@ edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
web-sys = { version = "0.3", features = ["HtmlElement", "HtmlInputElement", "Node"] }
|
||||
|
||||
@ -77,5 +77,5 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
zxcvbn = "2.1.2, <2.2.0"
|
||||
js-sys = "0.3.46"
|
||||
web-sys = { version = "0.3", features = ["Event","EventTarget","InputEvent"] }
|
||||
|
||||
@ -8,5 +8,5 @@ mod password;
|
||||
use app::App;
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo-utils = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
|
||||
@ -102,5 +102,5 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ log = "0.4"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
rand = { version = "0.8", features = ["small_rng"] }
|
||||
wasm-logger = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
yew-router = { path = "../../packages/yew-router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@ -147,5 +147,5 @@ fn switch(routes: &Route) -> Html {
|
||||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew", features = ["tokio"] }
|
||||
yew = { path = "../../packages/yew", features = ["tokio", "csr"] }
|
||||
gloo-timers = { version = "0.2.2", features = ["futures"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@ -56,5 +56,5 @@ fn app() -> Html {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
js-sys = "0.3"
|
||||
gloo = "0.6"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@ -150,5 +150,5 @@ impl Component for App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo = "0.6"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@ -245,5 +245,5 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -6,5 +6,5 @@ edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo-utils = "0.1"
|
||||
|
||||
@ -72,7 +72,7 @@ impl Component for App {
|
||||
fn mount_app(selector: &'static str) -> AppHandle<App> {
|
||||
let document = gloo_utils::document();
|
||||
let element = document.query_selector(selector).unwrap().unwrap();
|
||||
yew::start_app_in_element(element)
|
||||
yew::Renderer::<App>::with_root(element).render()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@ -8,7 +8,7 @@ authors = ["Shrey Somaiya", "Zac Kologlu"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
yew-agent = { path = "../../packages/yew-agent" }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
|
||||
@ -12,7 +12,7 @@ pub fn start() {
|
||||
use js_sys::{global, Reflect};
|
||||
|
||||
if Reflect::has(&global(), &JsValue::from_str("window")).unwrap() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
} else {
|
||||
agent::Worker::register();
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0"
|
||||
[dependencies]
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2"
|
||||
yew = { path = "../../packages/yew" }
|
||||
yew = { path = "../../packages/yew", features = ["csr"] }
|
||||
gloo-render = "0.1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@ -134,5 +134,5 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
@ -483,6 +483,6 @@ error[E0277]: the trait bound `Cow<'static, str>: From<{integer}>` is not satisf
|
||||
<Cow<'a, CStr> as From<&'a CString>>
|
||||
<Cow<'a, CStr> as From<CString>>
|
||||
<Cow<'a, OsStr> as From<&'a OsStr>>
|
||||
and 14 others
|
||||
and 11 others
|
||||
= note: required because of the requirements on the impl of `Into<Cow<'static, str>>` for `{integer}`
|
||||
note: required by `into`
|
||||
|
||||
@ -36,6 +36,7 @@ features = [
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
yew = { version = "0.19.3", path = "../yew", features = ["csr"] }
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
@ -115,7 +115,8 @@ fn root() -> Html {
|
||||
// - 404 redirects
|
||||
#[test]
|
||||
async fn router_works() {
|
||||
yew::start_app_in_element::<Root>(gloo::utils::document().get_element_by_id("output").unwrap());
|
||||
yew::Renderer::<Root>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
|
||||
@ -115,7 +115,8 @@ fn root() -> Html {
|
||||
// - 404 redirects
|
||||
#[test]
|
||||
async fn router_works() {
|
||||
yew::start_app_in_element::<Root>(gloo::utils::document().get_element_by_id("output").unwrap());
|
||||
yew::Renderer::<Root>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
|
||||
@ -115,7 +115,8 @@ fn root() -> Html {
|
||||
// - 404 redirects
|
||||
#[test]
|
||||
async fn router_works() {
|
||||
yew::start_app_in_element::<Root>(gloo::utils::document().get_element_by_id("output").unwrap());
|
||||
yew::Renderer::<Root>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ async fn link_in_browser_router() {
|
||||
let div = gloo::utils::document().create_element("div").unwrap();
|
||||
let _ = div.set_attribute("id", "browser-router");
|
||||
let _ = gloo::utils::body().append_child(&div);
|
||||
yew::start_app_in_element::<RootForBrowserRouter>(div);
|
||||
yew::Renderer::<RootForBrowserRouter>::with_root(div).render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
@ -128,7 +128,7 @@ async fn link_with_basename() {
|
||||
let div = gloo::utils::document().create_element("div").unwrap();
|
||||
let _ = div.set_attribute("id", "with-basename");
|
||||
let _ = gloo::utils::body().append_child(&div);
|
||||
yew::start_app_in_element::<RootForBasename>(div);
|
||||
yew::Renderer::<RootForBasename>::with_root(div).render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
@ -166,7 +166,7 @@ async fn link_in_hash_router() {
|
||||
let div = gloo::utils::document().create_element("div").unwrap();
|
||||
let _ = div.set_attribute("id", "hash-router");
|
||||
let _ = gloo::utils::body().append_child(&div);
|
||||
yew::start_app_in_element::<RootForHashRouter>(div);
|
||||
yew::Renderer::<RootForHashRouter>::with_root(div).render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
|
||||
@ -77,9 +77,10 @@ rustversion = "1"
|
||||
trybuild = "1"
|
||||
|
||||
[features]
|
||||
doc_test = []
|
||||
wasm_test = []
|
||||
ssr = ["futures", "html-escape"]
|
||||
csr = []
|
||||
doc_test = ["csr"]
|
||||
wasm_test = ["csr"]
|
||||
default = []
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
|
||||
@ -31,3 +31,21 @@ args = [
|
||||
[tasks.ssr-test]
|
||||
command = "cargo"
|
||||
args = ["test", "ssr_tests", "--features", "ssr"]
|
||||
|
||||
[tasks.clippy-feature-soundness]
|
||||
script = '''
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
cargo clippy -- --deny=warnings
|
||||
cargo clippy --features=ssr -- --deny=warnings
|
||||
cargo clippy --features=csr -- --deny=warnings
|
||||
cargo clippy --all-features --all-targets -- --deny=warnings
|
||||
|
||||
cargo clippy --release -- --deny=warnings
|
||||
cargo clippy --release --features=ssr -- --deny=warnings
|
||||
cargo clippy --release --features=csr -- --deny=warnings
|
||||
cargo clippy --release --all-features --all-targets -- --deny=warnings
|
||||
'''
|
||||
|
||||
[tasks.lint-flow]
|
||||
dependencies = ["clippy-feature-soundness"]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope.
|
||||
|
||||
use super::{ComponentRenderState, Scoped};
|
||||
use crate::html::Scoped;
|
||||
use crate::html::{IntoComponent, NodeRef, Scope};
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
@ -8,6 +8,7 @@ use web_sys::Element;
|
||||
|
||||
/// An instance of an application.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "csr")))]
|
||||
pub struct AppHandle<ICOMP: IntoComponent> {
|
||||
/// `Scope` holder
|
||||
pub(crate) scope: Scope<<ICOMP as IntoComponent>::Component>,
|
||||
@ -26,11 +27,9 @@ where
|
||||
let app = Self {
|
||||
scope: Scope::new(None),
|
||||
};
|
||||
let node_ref = NodeRef::default();
|
||||
let initial_render_state =
|
||||
ComponentRenderState::new(element, NodeRef::default(), &node_ref);
|
||||
|
||||
app.scope
|
||||
.mount_in_place(initial_render_state, node_ref, props);
|
||||
.mount_in_place(element, NodeRef::default(), NodeRef::default(), props);
|
||||
|
||||
app
|
||||
}
|
||||
@ -1,21 +1,16 @@
|
||||
//! This module contains the bundle implementation of a virtual component [BComp].
|
||||
|
||||
use super::{insert_node, BNode, DomBundle, Reconcilable};
|
||||
use crate::html::{AnyScope, BaseComponent, Scope};
|
||||
use crate::virtual_dom::{Key, VComp, VNode};
|
||||
use super::{BNode, Reconcilable, ReconcileTarget};
|
||||
use crate::html::AnyScope;
|
||||
use crate::html::Scoped;
|
||||
use crate::virtual_dom::{Key, VComp};
|
||||
use crate::NodeRef;
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::channel::oneshot;
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
use gloo_utils::document;
|
||||
use std::cell::Ref;
|
||||
use std::fmt;
|
||||
use std::{any::TypeId, borrow::Borrow};
|
||||
use std::{fmt, rc::Rc};
|
||||
use web_sys::{Element, Node};
|
||||
use web_sys::Element;
|
||||
|
||||
/// A virtual component. Compare with [VComp].
|
||||
pub struct BComp {
|
||||
pub(super) struct BComp {
|
||||
type_id: TypeId,
|
||||
scope: Box<dyn Scoped>,
|
||||
node_ref: NodeRef,
|
||||
@ -24,22 +19,20 @@ pub struct BComp {
|
||||
|
||||
impl BComp {
|
||||
/// Get the key of the underlying component
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BComp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"BComp {{ root: {:?} }}",
|
||||
self.scope.as_ref().render_state(),
|
||||
)
|
||||
f.debug_struct("BComp")
|
||||
.field("root", &self.scope.as_ref().render_state())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl DomBundle for BComp {
|
||||
impl ReconcileTarget for BComp {
|
||||
fn detach(self, _parent: &Element, parent_to_detach: bool) {
|
||||
self.scope.destroy_boxed(parent_to_detach);
|
||||
}
|
||||
@ -123,173 +116,11 @@ impl Reconcilable for VComp {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Mountable {
|
||||
fn copy(&self) -> Box<dyn Mountable>;
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped>;
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
}
|
||||
|
||||
pub struct PropsWrapper<COMP: BaseComponent> {
|
||||
props: Rc<COMP::Properties>,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> PropsWrapper<COMP> {
|
||||
pub fn new(props: Rc<COMP::Properties>) -> Self {
|
||||
Self { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
fn copy(&self) -> Box<dyn Mountable> {
|
||||
let wrapper: PropsWrapper<COMP> = PropsWrapper {
|
||||
props: Rc::clone(&self.props),
|
||||
};
|
||||
Box::new(wrapper)
|
||||
}
|
||||
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped> {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
let initial_render_state = ComponentRenderState::new(parent, next_sibling, &node_ref);
|
||||
scope.mount_in_place(initial_render_state, node_ref, self.props);
|
||||
|
||||
Box::new(scope)
|
||||
}
|
||||
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
|
||||
let scope: Scope<COMP> = scope.to_any().downcast::<COMP>();
|
||||
scope.reuse(self.props, node_ref, next_sibling);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.render_to_string(w, self.props.clone()).await;
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ComponentRenderState {
|
||||
root_node: BNode,
|
||||
/// When a component has no parent, it means that it should not be rendered.
|
||||
parent: Option<Element>,
|
||||
next_sibling: NodeRef,
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: Option<oneshot::Sender<VNode>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ComponentRenderState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.root_node.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentRenderState {
|
||||
/// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component
|
||||
pub(crate) fn new(parent: Element, next_sibling: NodeRef, node_ref: &NodeRef) -> Self {
|
||||
let placeholder = {
|
||||
let placeholder: Node = document().create_text_node("").into();
|
||||
insert_node(&placeholder, &parent, next_sibling.get().as_ref());
|
||||
node_ref.set(Some(placeholder.clone()));
|
||||
BNode::Ref(placeholder)
|
||||
};
|
||||
Self {
|
||||
root_node: placeholder,
|
||||
parent: Some(parent),
|
||||
next_sibling,
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: None,
|
||||
}
|
||||
}
|
||||
/// Set up server-side rendering of a component
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) fn new_ssr(tx: oneshot::Sender<VNode>) -> Self {
|
||||
use super::blist::BList;
|
||||
|
||||
Self {
|
||||
root_node: BNode::List(BList::new()),
|
||||
parent: None,
|
||||
next_sibling: NodeRef::default(),
|
||||
html_sender: Some(tx),
|
||||
}
|
||||
}
|
||||
/// Reuse the render state, asserting a new next_sibling
|
||||
pub(crate) fn reuse(&mut self, next_sibling: NodeRef) {
|
||||
self.next_sibling = next_sibling;
|
||||
}
|
||||
/// Shift the rendered content to a new DOM position
|
||||
pub(crate) fn shift(&mut self, new_parent: Element, next_sibling: NodeRef) {
|
||||
self.root_node.shift(&new_parent, next_sibling.clone());
|
||||
|
||||
self.parent = Some(new_parent);
|
||||
self.next_sibling = next_sibling;
|
||||
}
|
||||
/// Reconcile the rendered content with a new [VNode]
|
||||
pub(crate) fn reconcile(&mut self, root: VNode, scope: &AnyScope) -> NodeRef {
|
||||
if let Some(ref parent) = self.parent {
|
||||
let next_sibling = self.next_sibling.clone();
|
||||
|
||||
root.reconcile_node(scope, parent, next_sibling, &mut self.root_node)
|
||||
} else {
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(tx) = self.html_sender.take() {
|
||||
tx.send(root).unwrap();
|
||||
}
|
||||
NodeRef::default()
|
||||
}
|
||||
}
|
||||
/// Detach the rendered content from the DOM
|
||||
pub(crate) fn detach(self, parent_to_detach: bool) {
|
||||
if let Some(ref m) = self.parent {
|
||||
self.root_node.detach(m, parent_to_detach);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_trigger_rendered(&self) -> bool {
|
||||
self.parent.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Scoped {
|
||||
fn to_any(&self) -> AnyScope;
|
||||
/// Get the render state if it hasn't already been destroyed
|
||||
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>>;
|
||||
/// Shift the node associated with this scope to a new place
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
|
||||
/// Process an event to destroy a component
|
||||
fn destroy(self, parent_to_detach: bool);
|
||||
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool);
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dom_bundle::{DomBundle, Reconcilable};
|
||||
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
|
||||
use crate::scheduler;
|
||||
use crate::{
|
||||
html,
|
||||
@ -301,10 +132,8 @@ mod tests {
|
||||
use web_sys::Element;
|
||||
use web_sys::Node;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
struct Comp;
|
||||
@ -576,6 +405,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
@ -585,10 +415,8 @@ mod layout_tests {
|
||||
use crate::{Children, Component, Context, Html, Properties};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
struct Comp<T> {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! This module contains fragments bundles, a [BList]
|
||||
use super::{test_log, BNode};
|
||||
use crate::dom_bundle::{DomBundle, Reconcilable};
|
||||
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use crate::virtual_dom::{Key, VList, VNode, VText};
|
||||
use std::borrow::Borrow;
|
||||
@ -12,7 +12,7 @@ use web_sys::Element;
|
||||
|
||||
/// This struct represents a mounted [VList]
|
||||
#[derive(Debug)]
|
||||
pub struct BList {
|
||||
pub(super) struct BList {
|
||||
/// The reverse (render order) list of child [BNode]s
|
||||
rev_children: Vec<BNode>,
|
||||
/// All [BNode]s in the BList have keys
|
||||
@ -120,7 +120,7 @@ impl BNode {
|
||||
|
||||
impl BList {
|
||||
/// Create a new empty [BList]
|
||||
pub(super) const fn new() -> BList {
|
||||
pub const fn new() -> BList {
|
||||
BList {
|
||||
rev_children: vec![],
|
||||
fully_keyed: true,
|
||||
@ -129,7 +129,7 @@ impl BList {
|
||||
}
|
||||
|
||||
/// Get the key of the underlying fragment
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
@ -353,7 +353,7 @@ impl BList {
|
||||
}
|
||||
}
|
||||
|
||||
impl DomBundle for BList {
|
||||
impl ReconcileTarget for BList {
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
for child in self.rev_children.into_iter() {
|
||||
child.detach(parent, parent_to_detach);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//! This module contains the bundle version of an abstract node [BNode]
|
||||
|
||||
use super::{BComp, BList, BPortal, BSuspense, BTag, BText};
|
||||
use crate::dom_bundle::{DomBundle, Reconcilable};
|
||||
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use crate::virtual_dom::{Key, VNode};
|
||||
use gloo::console;
|
||||
@ -9,7 +9,7 @@ use std::fmt;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// The bundle implementation to [VNode].
|
||||
pub enum BNode {
|
||||
pub(super) enum BNode {
|
||||
/// A bind between `VTag` and `Element`.
|
||||
Tag(Box<BTag>),
|
||||
/// A bind between `VText` and `TextNode`.
|
||||
@ -28,7 +28,7 @@ pub enum BNode {
|
||||
|
||||
impl BNode {
|
||||
/// Get the key of the underlying node
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
match self {
|
||||
Self::Comp(bsusp) => bsusp.key(),
|
||||
Self::List(blist) => blist.key(),
|
||||
@ -41,7 +41,7 @@ impl BNode {
|
||||
}
|
||||
}
|
||||
|
||||
impl DomBundle for BNode {
|
||||
impl ReconcileTarget for BNode {
|
||||
/// Remove VNode from parent.
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
match self {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
use super::test_log;
|
||||
use super::BNode;
|
||||
use crate::dom_bundle::{DomBundle, Reconcilable};
|
||||
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use crate::virtual_dom::Key;
|
||||
use crate::virtual_dom::VPortal;
|
||||
@ -10,7 +10,7 @@ use web_sys::Element;
|
||||
|
||||
/// The bundle implementation to [VPortal].
|
||||
#[derive(Debug)]
|
||||
pub struct BPortal {
|
||||
pub(super) struct BPortal {
|
||||
/// The element under which the content is inserted.
|
||||
host: Element,
|
||||
/// The next sibling after the inserted content
|
||||
@ -19,7 +19,7 @@ pub struct BPortal {
|
||||
node: Box<BNode>,
|
||||
}
|
||||
|
||||
impl DomBundle for BPortal {
|
||||
impl ReconcileTarget for BPortal {
|
||||
fn detach(self, _: &Element, _parent_to_detach: bool) {
|
||||
test_log!("Detaching portal from host{:?}", self.host.outer_html());
|
||||
self.node.detach(&self.host, false);
|
||||
@ -99,7 +99,7 @@ impl Reconcilable for VPortal {
|
||||
|
||||
impl BPortal {
|
||||
/// Get the key of the underlying portal
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
self.node.key()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! This module contains the bundle version of a supsense [BSuspense]
|
||||
|
||||
use super::{BNode, DomBundle, Reconcilable};
|
||||
use super::{BNode, Reconcilable, ReconcileTarget};
|
||||
use crate::html::AnyScope;
|
||||
use crate::virtual_dom::{Key, VSuspense};
|
||||
use crate::NodeRef;
|
||||
@ -8,7 +8,7 @@ use web_sys::Element;
|
||||
|
||||
/// The bundle implementation to [VSuspense]
|
||||
#[derive(Debug)]
|
||||
pub struct BSuspense {
|
||||
pub(super) struct BSuspense {
|
||||
children_bundle: BNode,
|
||||
/// The supsense is suspended if fallback contains [Some] bundle
|
||||
fallback_bundle: Option<BNode>,
|
||||
@ -18,7 +18,7 @@ pub struct BSuspense {
|
||||
|
||||
impl BSuspense {
|
||||
/// Get the key of the underlying suspense
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
/// Get the bundle node that actually shows up in the dom
|
||||
@ -29,7 +29,7 @@ impl BSuspense {
|
||||
}
|
||||
}
|
||||
|
||||
impl DomBundle for BSuspense {
|
||||
impl ReconcileTarget for BSuspense {
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
if let Some(fallback) = self.fallback_bundle {
|
||||
fallback.detach(parent, parent_to_detach);
|
||||
|
||||
@ -53,7 +53,7 @@ macro_rules! impl_access_value {
|
||||
impl_access_value! {InputElement TextAreaElement}
|
||||
|
||||
/// Able to have its value read or set
|
||||
pub trait AccessValue {
|
||||
pub(super) trait AccessValue {
|
||||
fn value(&self) -> String;
|
||||
fn set_value(&self, v: &str);
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true);
|
||||
/// handler has no effect.
|
||||
///
|
||||
/// This function should be called before any component is mounted.
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "render")))]
|
||||
pub fn set_event_bubbling(bubble: bool) {
|
||||
BUBBLE_EVENTS.store(bubble, Ordering::Relaxed);
|
||||
}
|
||||
@ -105,7 +106,7 @@ impl ListenerRegistration {
|
||||
}
|
||||
|
||||
/// Remove any registered event listeners from the global registry
|
||||
pub(super) fn unregister(&self) {
|
||||
pub fn unregister(&self) {
|
||||
if let Self::Registered(id) = self {
|
||||
Registry::with(|r| r.unregister(id));
|
||||
}
|
||||
@ -406,7 +407,7 @@ mod tests {
|
||||
|
||||
let root = document().create_element("div").unwrap();
|
||||
document().body().unwrap().append_child(&root).unwrap();
|
||||
let app = crate::start_app_in_element::<Comp<M>>(root);
|
||||
let app = crate::Renderer::<Comp<M>>::with_root(root).render();
|
||||
scheduler::start_now();
|
||||
|
||||
(app, get_el_by_tag(tag))
|
||||
|
||||
@ -5,7 +5,7 @@ mod listeners;
|
||||
|
||||
pub use listeners::set_event_bubbling;
|
||||
|
||||
use super::{insert_node, BList, BNode, DomBundle, Reconcilable};
|
||||
use super::{insert_node, BList, BNode, Reconcilable, ReconcileTarget};
|
||||
use crate::html::AnyScope;
|
||||
use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE};
|
||||
use crate::virtual_dom::{Attributes, Key, VTag};
|
||||
@ -56,7 +56,7 @@ enum BTagInner {
|
||||
|
||||
/// The bundle implementation to [VTag]
|
||||
#[derive(Debug)]
|
||||
pub struct BTag {
|
||||
pub(super) struct BTag {
|
||||
/// [BTag] fields that are specific to different [BTag] kinds.
|
||||
inner: BTagInner,
|
||||
listeners: ListenerRegistration,
|
||||
@ -68,7 +68,7 @@ pub struct BTag {
|
||||
key: Option<Key>,
|
||||
}
|
||||
|
||||
impl DomBundle for BTag {
|
||||
impl ReconcileTarget for BTag {
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
self.listeners.unregister();
|
||||
|
||||
@ -247,15 +247,17 @@ impl VTag {
|
||||
|
||||
impl BTag {
|
||||
/// Get the key of the underlying tag
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
fn reference(&self) -> &Element {
|
||||
&self.reference
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
fn children(&self) -> &[BNode] {
|
||||
match &self.inner {
|
||||
@ -264,6 +266,7 @@ impl BTag {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
fn tag(&self) -> &str {
|
||||
match &self.inner {
|
||||
@ -274,10 +277,11 @@ impl BTag {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dom_bundle::{BNode, DomBundle, Reconcilable};
|
||||
use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget};
|
||||
use crate::html;
|
||||
use crate::html::AnyScope;
|
||||
use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE};
|
||||
@ -287,10 +291,8 @@ mod tests {
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement as InputElement;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
fn test_scope() -> AnyScope {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! This module contains the bundle implementation of text [BText].
|
||||
|
||||
use super::{insert_node, BNode, DomBundle, Reconcilable};
|
||||
use super::{insert_node, BNode, Reconcilable, ReconcileTarget};
|
||||
use crate::html::AnyScope;
|
||||
use crate::virtual_dom::{AttrValue, VText};
|
||||
use crate::NodeRef;
|
||||
@ -9,12 +9,12 @@ use gloo_utils::document;
|
||||
use web_sys::{Element, Text as TextNode};
|
||||
|
||||
/// The bundle implementation to [VText]
|
||||
pub struct BText {
|
||||
pub(super) struct BText {
|
||||
text: AttrValue,
|
||||
text_node: TextNode,
|
||||
}
|
||||
|
||||
impl DomBundle for BText {
|
||||
impl ReconcileTarget for BText {
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
if !parent_to_detach {
|
||||
let result = parent.remove_child(&self.text_node);
|
||||
@ -81,7 +81,7 @@ impl Reconcilable for VText {
|
||||
|
||||
impl std::fmt::Debug for BText {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BText {{ text: \"{}\" }}", self.text)
|
||||
f.debug_struct("BText").field("text", &self.text).finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
//! In order to efficiently implement updates, and diffing, additional information has to be
|
||||
//! kept around. This information is carried in the bundle.
|
||||
|
||||
mod app_handle;
|
||||
mod bcomp;
|
||||
mod blist;
|
||||
mod bnode;
|
||||
@ -13,139 +12,65 @@ mod bportal;
|
||||
mod bsuspense;
|
||||
mod btag;
|
||||
mod btext;
|
||||
mod traits;
|
||||
mod utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use self::bcomp::BComp;
|
||||
use self::blist::BList;
|
||||
use self::bnode::BNode;
|
||||
use self::bportal::BPortal;
|
||||
use self::bsuspense::BSuspense;
|
||||
use self::btag::BTag;
|
||||
use self::btext::BText;
|
||||
|
||||
pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped};
|
||||
|
||||
#[doc(hidden)] // Publically exported from crate::app_handle
|
||||
pub use self::app_handle::AppHandle;
|
||||
#[doc(hidden)] // Publically exported from crate::events
|
||||
pub use self::btag::set_event_bubbling;
|
||||
#[cfg(test)]
|
||||
#[doc(hidden)] // Publically exported from crate::tests
|
||||
pub use self::tests::layout_tests;
|
||||
|
||||
use crate::html::AnyScope;
|
||||
use crate::NodeRef;
|
||||
use gloo::utils::document;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
trait DomBundle {
|
||||
/// Remove self from parent.
|
||||
///
|
||||
/// Parent to detach is `true` if the parent element will also be detached.
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool);
|
||||
use crate::html::AnyScope;
|
||||
use crate::html::NodeRef;
|
||||
use crate::virtual_dom::VNode;
|
||||
|
||||
/// Move elements from one parent to another parent.
|
||||
/// This is for example used by `VSuspense` to preserve component state without detaching
|
||||
/// (which destroys component state).
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef);
|
||||
}
|
||||
use bcomp::BComp;
|
||||
use blist::BList;
|
||||
use bnode::BNode;
|
||||
use bportal::BPortal;
|
||||
use bsuspense::BSuspense;
|
||||
use btag::BTag;
|
||||
use btext::BText;
|
||||
use traits::{Reconcilable, ReconcileTarget};
|
||||
use utils::{insert_node, test_log};
|
||||
|
||||
/// This trait provides features to update a tree by calculating a difference against another tree.
|
||||
trait Reconcilable {
|
||||
type Bundle: DomBundle;
|
||||
#[doc(hidden)] // Publically exported from crate::events
|
||||
pub use self::btag::set_event_bubbling;
|
||||
|
||||
/// Attach a virtual node to the DOM tree.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `parent_scope`: the parent `Scope` used for passing messages to the
|
||||
/// parent `Component`.
|
||||
/// - `parent`: the parent node in the DOM.
|
||||
/// - `next_sibling`: to find where to put the node.
|
||||
///
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn attach(
|
||||
self,
|
||||
/// A Bundle.
|
||||
///
|
||||
/// Each component holds a bundle that represents a realised layout, designated by a [VNode].
|
||||
///
|
||||
/// This is not to be confused with [BComp], which represents a component in the position of a
|
||||
/// bundle layout.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Bundle(BNode);
|
||||
|
||||
impl Bundle {
|
||||
/// Creates a new bundle.
|
||||
pub fn new(parent: &Element, next_sibling: &NodeRef, node_ref: &NodeRef) -> Self {
|
||||
let placeholder: Node = document().create_text_node("").into();
|
||||
insert_node(&placeholder, parent, next_sibling.get().as_ref());
|
||||
node_ref.set(Some(placeholder.clone()));
|
||||
Self(BNode::Ref(placeholder))
|
||||
}
|
||||
|
||||
/// Shifts the bundle into a different position.
|
||||
pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
||||
self.0.shift(next_parent, next_sibling);
|
||||
}
|
||||
|
||||
/// Applies a virtual dom layout to current bundle.
|
||||
pub fn reconcile(
|
||||
&mut self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle);
|
||||
next_node: VNode,
|
||||
) -> NodeRef {
|
||||
next_node.reconcile_node(parent_scope, parent, next_sibling, &mut self.0)
|
||||
}
|
||||
|
||||
/// Scoped diff apply to other tree.
|
||||
///
|
||||
/// Virtual rendering for the node. It uses parent node and existing
|
||||
/// children (virtual and DOM) to check the difference and apply patches to
|
||||
/// the actual DOM representation.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `parent_scope`: the parent `Scope` used for passing messages to the
|
||||
/// parent `Component`.
|
||||
/// - `parent`: the parent node in the DOM.
|
||||
/// - `next_sibling`: the next sibling, used to efficiently find where to
|
||||
/// put the node.
|
||||
/// - `bundle`: the node that this node will be replacing in the DOM. This
|
||||
/// method will remove the `bundle` from the `parent` if it is of the wrong
|
||||
/// kind, and otherwise reuse it.
|
||||
///
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef;
|
||||
|
||||
fn reconcile(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut Self::Bundle,
|
||||
) -> NodeRef;
|
||||
|
||||
/// Replace an existing bundle by attaching self and detaching the existing one
|
||||
fn replace(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef
|
||||
where
|
||||
Self: Sized,
|
||||
Self::Bundle: Into<BNode>,
|
||||
{
|
||||
let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling);
|
||||
let ancestor = std::mem::replace(bundle, self_.into());
|
||||
ancestor.detach(parent, false);
|
||||
self_ref
|
||||
/// Detaches current bundle.
|
||||
pub fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
self.0.detach(parent, parent_to_detach);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a concrete [Node] into the DOM
|
||||
fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) {
|
||||
match next_sibling {
|
||||
Some(next_sibling) => parent
|
||||
.insert_before(node, Some(next_sibling))
|
||||
.expect("failed to insert tag before next sibling"),
|
||||
None => parent.append_child(node).expect("failed to append child"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wasm_test", verbose_tests))]
|
||||
macro_rules! test_log {
|
||||
($fmt:literal, $($arg:expr),* $(,)?) => {
|
||||
::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*);
|
||||
};
|
||||
}
|
||||
#[cfg(not(all(test, feature = "wasm_test", verbose_tests)))]
|
||||
macro_rules! test_log {
|
||||
($fmt:literal, $($arg:expr),* $(,)?) => {
|
||||
// Only type-check the format expression, do not run any side effects
|
||||
let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); };
|
||||
};
|
||||
}
|
||||
/// Log an operation during tests for debugging purposes
|
||||
/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate.
|
||||
pub(self) use test_log;
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
pub mod layout_tests;
|
||||
|
||||
use super::Reconcilable;
|
||||
|
||||
use crate::virtual_dom::VNode;
|
||||
use crate::{dom_bundle::BNode, html::AnyScope, NodeRef};
|
||||
use web_sys::Element;
|
||||
|
||||
impl VNode {
|
||||
fn reconcile_sequentially(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut Option<BNode>,
|
||||
) -> NodeRef {
|
||||
match bundle {
|
||||
None => {
|
||||
let (self_ref, node) = self.attach(parent_scope, parent, next_sibling);
|
||||
*bundle = Some(node);
|
||||
self_ref
|
||||
}
|
||||
Some(bundle) => self.reconcile_node(parent_scope, parent, next_sibling, bundle),
|
||||
}
|
||||
}
|
||||
}
|
||||
92
packages/yew/src/dom_bundle/traits.rs
Normal file
92
packages/yew/src/dom_bundle/traits.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use super::BNode;
|
||||
use crate::html::AnyScope;
|
||||
use crate::html::NodeRef;
|
||||
use web_sys::Element;
|
||||
|
||||
/// A Reconcile Target.
|
||||
///
|
||||
/// When a [Reconcilable] is attached, a reconcile target is created to store additional
|
||||
/// information.
|
||||
pub(super) trait ReconcileTarget {
|
||||
/// Remove self from parent.
|
||||
///
|
||||
/// Parent to detach is `true` if the parent element will also be detached.
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool);
|
||||
|
||||
/// Move elements from one parent to another parent.
|
||||
/// This is for example used by `VSuspense` to preserve component state without detaching
|
||||
/// (which destroys component state).
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef);
|
||||
}
|
||||
|
||||
/// This trait provides features to update a tree by calculating a difference against another tree.
|
||||
pub(super) trait Reconcilable {
|
||||
type Bundle: ReconcileTarget;
|
||||
|
||||
/// Attach a virtual node to the DOM tree.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `parent_scope`: the parent `Scope` used for passing messages to the
|
||||
/// parent `Component`.
|
||||
/// - `parent`: the parent node in the DOM.
|
||||
/// - `next_sibling`: to find where to put the node.
|
||||
///
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn attach(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle);
|
||||
|
||||
/// Scoped diff apply to other tree.
|
||||
///
|
||||
/// Virtual rendering for the node. It uses parent node and existing
|
||||
/// children (virtual and DOM) to check the difference and apply patches to
|
||||
/// the actual DOM representation.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `parent_scope`: the parent `Scope` used for passing messages to the
|
||||
/// parent `Component`.
|
||||
/// - `parent`: the parent node in the DOM.
|
||||
/// - `next_sibling`: the next sibling, used to efficiently find where to
|
||||
/// put the node.
|
||||
/// - `bundle`: the node that this node will be replacing in the DOM. This
|
||||
/// method will remove the `bundle` from the `parent` if it is of the wrong
|
||||
/// kind, and otherwise reuse it.
|
||||
///
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef;
|
||||
|
||||
fn reconcile(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut Self::Bundle,
|
||||
) -> NodeRef;
|
||||
|
||||
/// Replace an existing bundle by attaching self and detaching the existing one
|
||||
fn replace(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef
|
||||
where
|
||||
Self: Sized,
|
||||
Self::Bundle: Into<BNode>,
|
||||
{
|
||||
let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling);
|
||||
let ancestor = std::mem::replace(bundle, self_.into());
|
||||
ancestor.detach(parent, false);
|
||||
self_ref
|
||||
}
|
||||
}
|
||||
28
packages/yew/src/dom_bundle/utils.rs
Normal file
28
packages/yew/src/dom_bundle/utils.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// Insert a concrete [Node] into the DOM
|
||||
pub(super) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) {
|
||||
match next_sibling {
|
||||
Some(next_sibling) => parent
|
||||
.insert_before(node, Some(next_sibling))
|
||||
.expect("failed to insert tag before next sibling"),
|
||||
None => parent.append_child(node).expect("failed to append child"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wasm_test", verbose_tests))]
|
||||
macro_rules! test_log {
|
||||
($fmt:literal, $($arg:expr),* $(,)?) => {
|
||||
::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*);
|
||||
};
|
||||
}
|
||||
#[cfg(not(all(test, feature = "wasm_test", verbose_tests)))]
|
||||
macro_rules! test_log {
|
||||
($fmt:literal, $($arg:expr),* $(,)?) => {
|
||||
// Only type-check the format expression, do not run any side effects
|
||||
let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); };
|
||||
};
|
||||
}
|
||||
/// Log an operation during tests for debugging purposes
|
||||
/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate.
|
||||
pub(super) use test_log;
|
||||
@ -2,15 +2,68 @@
|
||||
|
||||
use super::scope::{AnyScope, Scope};
|
||||
use super::BaseComponent;
|
||||
use crate::dom_bundle::ComponentRenderState;
|
||||
use crate::html::RenderError;
|
||||
use crate::html::{Html, RenderError};
|
||||
use crate::scheduler::{self, Runnable, Shared};
|
||||
use crate::suspense::{Suspense, Suspension};
|
||||
use crate::{Callback, Context, HtmlResult, NodeRef};
|
||||
use crate::{Callback, Context, HtmlResult};
|
||||
use std::any::Any;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct CompStateInner<COMP>
|
||||
#[cfg(feature = "csr")]
|
||||
use crate::dom_bundle::Bundle;
|
||||
#[cfg(feature = "csr")]
|
||||
use crate::html::NodeRef;
|
||||
#[cfg(feature = "csr")]
|
||||
use web_sys::Element;
|
||||
|
||||
pub(crate) enum ComponentRenderState {
|
||||
#[cfg(feature = "csr")]
|
||||
Render {
|
||||
bundle: Bundle,
|
||||
parent: web_sys::Element,
|
||||
next_sibling: NodeRef,
|
||||
node_ref: NodeRef,
|
||||
},
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
Ssr {
|
||||
sender: Option<futures::channel::oneshot::Sender<Html>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ComponentRenderState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(feature = "csr")]
|
||||
Self::Render {
|
||||
ref bundle,
|
||||
ref parent,
|
||||
ref next_sibling,
|
||||
ref node_ref,
|
||||
} => f
|
||||
.debug_struct("ComponentRenderState::Render")
|
||||
.field("bundle", bundle)
|
||||
.field("parent", parent)
|
||||
.field("next_sibling", next_sibling)
|
||||
.field("node_ref", node_ref)
|
||||
.finish(),
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
Self::Ssr { ref sender } => {
|
||||
let sender_repr = match sender {
|
||||
Some(_) => "Some(_)",
|
||||
None => "None",
|
||||
};
|
||||
|
||||
f.debug_struct("ComponentRenderState::Ssr")
|
||||
.field("sender", &sender_repr)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CompStateInner<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
@ -23,7 +76,7 @@ where
|
||||
///
|
||||
/// Mostly a thin wrapper that passes the context to a component's lifecycle
|
||||
/// methods.
|
||||
pub trait Stateful {
|
||||
pub(crate) trait Stateful {
|
||||
fn view(&self) -> HtmlResult;
|
||||
fn rendered(&mut self, first_render: bool);
|
||||
fn destroy(&mut self);
|
||||
@ -90,11 +143,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ComponentState {
|
||||
pub(crate) struct ComponentState {
|
||||
pub(super) inner: Box<dyn Stateful>,
|
||||
|
||||
pub(super) render_state: ComponentRenderState,
|
||||
node_ref: NodeRef,
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
has_rendered: bool,
|
||||
|
||||
suspension: Option<Suspension>,
|
||||
@ -107,7 +161,6 @@ pub struct ComponentState {
|
||||
impl ComponentState {
|
||||
pub(crate) fn new<COMP: BaseComponent>(
|
||||
initial_render_state: ComponentRenderState,
|
||||
node_ref: NodeRef,
|
||||
scope: Scope<COMP>,
|
||||
props: Rc<COMP::Properties>,
|
||||
) -> Self {
|
||||
@ -123,19 +176,29 @@ impl ComponentState {
|
||||
Self {
|
||||
inner,
|
||||
render_state: initial_render_state,
|
||||
node_ref,
|
||||
suspension: None,
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
has_rendered: false,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn downcast_comp_ref<COMP>(&self) -> Option<&COMP>
|
||||
where
|
||||
COMP: BaseComponent + 'static,
|
||||
{
|
||||
self.inner
|
||||
.as_any()
|
||||
.downcast_ref::<CompStateInner<COMP>>()
|
||||
.map(|m| &m.component)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateRunner<COMP: BaseComponent> {
|
||||
pub(crate) struct CreateRunner<COMP: BaseComponent> {
|
||||
pub initial_render_state: ComponentRenderState,
|
||||
pub node_ref: NodeRef,
|
||||
pub props: Rc<COMP::Properties>,
|
||||
pub scope: Scope<COMP>,
|
||||
}
|
||||
@ -149,7 +212,6 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
||||
|
||||
*current_state = Some(ComponentState::new(
|
||||
self.initial_render_state,
|
||||
self.node_ref,
|
||||
self.scope.clone(),
|
||||
self.props,
|
||||
));
|
||||
@ -157,31 +219,79 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UpdateEvent {
|
||||
pub(crate) enum UpdateEvent {
|
||||
/// Drain messages for a component.
|
||||
Message,
|
||||
/// Wraps properties, node ref, and next sibling for a component.
|
||||
/// Wraps properties, node ref, and next sibling for a component
|
||||
#[cfg(feature = "csr")]
|
||||
Properties(Rc<dyn Any>, NodeRef, NodeRef),
|
||||
/// Shift Scope.
|
||||
#[cfg(feature = "csr")]
|
||||
Shift(Element, NodeRef),
|
||||
}
|
||||
|
||||
pub struct UpdateRunner {
|
||||
pub(crate) struct UpdateRunner {
|
||||
pub state: Shared<Option<ComponentState>>,
|
||||
pub event: UpdateEvent,
|
||||
}
|
||||
|
||||
impl Runnable for UpdateRunner {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(mut state) = self.state.borrow_mut().as_mut() {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
let schedule_render = match self.event {
|
||||
UpdateEvent::Message => state.inner.flush_messages(),
|
||||
UpdateEvent::Properties(props, node_ref, next_sibling) => {
|
||||
// When components are updated, a new node ref could have been passed in
|
||||
state.node_ref = node_ref;
|
||||
// When components are updated, their siblings were likely also updated
|
||||
state.render_state.reuse(next_sibling);
|
||||
// Only trigger changed if props were changed
|
||||
#[cfg(feature = "csr")]
|
||||
UpdateEvent::Properties(props, next_node_ref, next_sibling) => {
|
||||
match state.render_state {
|
||||
#[cfg(feature = "csr")]
|
||||
ComponentRenderState::Render {
|
||||
ref mut node_ref,
|
||||
next_sibling: ref mut current_next_sibling,
|
||||
..
|
||||
} => {
|
||||
// When components are updated, a new node ref could have been passed in
|
||||
*node_ref = next_node_ref;
|
||||
// When components are updated, their siblings were likely also updated
|
||||
*current_next_sibling = next_sibling;
|
||||
// Only trigger changed if props were changed
|
||||
state.inner.props_changed(props)
|
||||
}
|
||||
|
||||
state.inner.props_changed(props)
|
||||
#[cfg(feature = "ssr")]
|
||||
ComponentRenderState::Ssr { .. } => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("properties do not change during SSR");
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
UpdateEvent::Shift(next_parent, next_sibling) => {
|
||||
match state.render_state {
|
||||
ComponentRenderState::Render {
|
||||
ref bundle,
|
||||
ref mut parent,
|
||||
next_sibling: ref mut current_next_sibling,
|
||||
..
|
||||
} => {
|
||||
bundle.shift(&next_parent, next_sibling.clone());
|
||||
|
||||
*parent = next_parent;
|
||||
*current_next_sibling = next_sibling;
|
||||
}
|
||||
|
||||
// Shifting is not possible during SSR.
|
||||
#[cfg(feature = "ssr")]
|
||||
ComponentRenderState::Ssr { .. } => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("shifting is not possible during SSR");
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
@ -204,8 +314,10 @@ impl Runnable for UpdateRunner {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DestroyRunner {
|
||||
pub(crate) struct DestroyRunner {
|
||||
pub state: Shared<Option<ComponentState>>,
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub parent_to_detach: bool,
|
||||
}
|
||||
|
||||
@ -216,13 +328,28 @@ impl Runnable for DestroyRunner {
|
||||
super::log_event(state.vcomp_id, "destroy");
|
||||
|
||||
state.inner.destroy();
|
||||
state.render_state.detach(self.parent_to_detach);
|
||||
state.node_ref.set(None);
|
||||
|
||||
match state.render_state {
|
||||
#[cfg(feature = "csr")]
|
||||
ComponentRenderState::Render {
|
||||
bundle,
|
||||
ref parent,
|
||||
ref node_ref,
|
||||
..
|
||||
} => {
|
||||
bundle.detach(parent, self.parent_to_detach);
|
||||
|
||||
node_ref.set(None);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
ComponentRenderState::Ssr { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderRunner {
|
||||
pub(crate) struct RenderRunner {
|
||||
pub state: Shared<Option<ComponentState>>,
|
||||
}
|
||||
|
||||
@ -233,120 +360,147 @@ impl Runnable for RenderRunner {
|
||||
super::log_event(state.vcomp_id, "render");
|
||||
|
||||
match state.inner.view() {
|
||||
Ok(root) => {
|
||||
// Currently not suspended, we remove any previous suspension and update
|
||||
// normally.
|
||||
if let Some(m) = state.suspension.take() {
|
||||
let comp_scope = state.inner.any_scope();
|
||||
|
||||
let suspense_scope = comp_scope.find_parent_scope::<Suspense>().unwrap();
|
||||
let suspense = suspense_scope.get_component().unwrap();
|
||||
|
||||
suspense.resume(m);
|
||||
}
|
||||
|
||||
let scope = state.inner.any_scope();
|
||||
let node = state.render_state.reconcile(root, &scope);
|
||||
state.node_ref.link(node);
|
||||
|
||||
if state.render_state.should_trigger_rendered() {
|
||||
let first_render = !state.has_rendered;
|
||||
state.has_rendered = true;
|
||||
|
||||
scheduler::push_component_rendered(
|
||||
self.state.as_ptr() as usize,
|
||||
RenderedRunner {
|
||||
state: self.state.clone(),
|
||||
first_render,
|
||||
},
|
||||
first_render,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Err(RenderError::Suspended(m)) => {
|
||||
// Currently suspended, we re-use previous root node and send
|
||||
// suspension to parent element.
|
||||
let shared_state = self.state.clone();
|
||||
|
||||
if m.resumed() {
|
||||
// schedule a render immediately if suspension is resumed.
|
||||
|
||||
scheduler::push_component_render(
|
||||
shared_state.as_ptr() as usize,
|
||||
RenderRunner {
|
||||
state: shared_state.clone(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// We schedule a render after current suspension is resumed.
|
||||
|
||||
let comp_scope = state.inner.any_scope();
|
||||
|
||||
let suspense_scope = comp_scope
|
||||
.find_parent_scope::<Suspense>()
|
||||
.expect("To suspend rendering, a <Suspense /> component is required.");
|
||||
let suspense = suspense_scope.get_component().unwrap();
|
||||
|
||||
m.listen(Callback::from(move |_| {
|
||||
scheduler::push_component_render(
|
||||
shared_state.as_ptr() as usize,
|
||||
RenderRunner {
|
||||
state: shared_state.clone(),
|
||||
},
|
||||
);
|
||||
scheduler::start();
|
||||
}));
|
||||
|
||||
if let Some(ref last_m) = state.suspension {
|
||||
if &m != last_m {
|
||||
// We remove previous suspension from the suspense.
|
||||
suspense.resume(last_m.clone());
|
||||
}
|
||||
}
|
||||
state.suspension = Some(m.clone());
|
||||
|
||||
suspense.suspend(m);
|
||||
}
|
||||
}
|
||||
Ok(m) => self.render(state, m),
|
||||
Err(RenderError::Suspended(m)) => self.suspend(state, m),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderedRunner {
|
||||
state: Shared<Option<ComponentState>>,
|
||||
first_render: bool,
|
||||
impl RenderRunner {
|
||||
fn suspend(&self, state: &mut ComponentState, suspension: Suspension) {
|
||||
// Currently suspended, we re-use previous root node and send
|
||||
// suspension to parent element.
|
||||
let shared_state = self.state.clone();
|
||||
|
||||
if suspension.resumed() {
|
||||
// schedule a render immediately if suspension is resumed.
|
||||
|
||||
scheduler::push_component_render(
|
||||
shared_state.as_ptr() as usize,
|
||||
RenderRunner {
|
||||
state: shared_state,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// We schedule a render after current suspension is resumed.
|
||||
let comp_scope = state.inner.any_scope();
|
||||
|
||||
let suspense_scope = comp_scope
|
||||
.find_parent_scope::<Suspense>()
|
||||
.expect("To suspend rendering, a <Suspense /> component is required.");
|
||||
let suspense = suspense_scope.get_component().unwrap();
|
||||
|
||||
suspension.listen(Callback::from(move |_| {
|
||||
scheduler::push_component_render(
|
||||
shared_state.as_ptr() as usize,
|
||||
RenderRunner {
|
||||
state: shared_state.clone(),
|
||||
},
|
||||
);
|
||||
scheduler::start();
|
||||
}));
|
||||
|
||||
if let Some(ref last_suspension) = state.suspension {
|
||||
if &suspension != last_suspension {
|
||||
// We remove previous suspension from the suspense.
|
||||
suspense.resume(last_suspension.clone());
|
||||
}
|
||||
}
|
||||
state.suspension = Some(suspension.clone());
|
||||
|
||||
suspense.suspend(suspension);
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, state: &mut ComponentState, new_root: Html) {
|
||||
// Currently not suspended, we remove any previous suspension and update
|
||||
// normally.
|
||||
if let Some(m) = state.suspension.take() {
|
||||
let comp_scope = state.inner.any_scope();
|
||||
|
||||
let suspense_scope = comp_scope.find_parent_scope::<Suspense>().unwrap();
|
||||
let suspense = suspense_scope.get_component().unwrap();
|
||||
|
||||
suspense.resume(m);
|
||||
}
|
||||
|
||||
match state.render_state {
|
||||
#[cfg(feature = "csr")]
|
||||
ComponentRenderState::Render {
|
||||
ref mut bundle,
|
||||
ref parent,
|
||||
ref next_sibling,
|
||||
ref node_ref,
|
||||
..
|
||||
} => {
|
||||
let scope = state.inner.any_scope();
|
||||
let new_node_ref = bundle.reconcile(&scope, parent, next_sibling.clone(), new_root);
|
||||
node_ref.link(new_node_ref);
|
||||
|
||||
let first_render = !state.has_rendered;
|
||||
state.has_rendered = true;
|
||||
|
||||
scheduler::push_component_rendered(
|
||||
self.state.as_ptr() as usize,
|
||||
RenderedRunner {
|
||||
state: self.state.clone(),
|
||||
first_render,
|
||||
},
|
||||
first_render,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
ComponentRenderState::Ssr { ref mut sender } => {
|
||||
if let Some(tx) = sender.take() {
|
||||
tx.send(new_root).unwrap();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Runnable for RenderedRunner {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
super::log_event(state.vcomp_id, "rendered");
|
||||
#[cfg(feature = "csr")]
|
||||
mod feat_csr {
|
||||
use super::*;
|
||||
|
||||
if state.suspension.is_none() {
|
||||
state.inner.rendered(self.first_render);
|
||||
pub(crate) struct RenderedRunner {
|
||||
pub state: Shared<Option<ComponentState>>,
|
||||
pub first_render: bool,
|
||||
}
|
||||
|
||||
impl Runnable for RenderedRunner {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
super::super::log_event(state.vcomp_id, "rendered");
|
||||
|
||||
if state.suspension.is_none() {
|
||||
state.inner.rendered(self.first_render);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
use feat_csr::*;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::dom_bundle::ComponentRenderState;
|
||||
use super::*;
|
||||
use crate::html;
|
||||
use crate::html::*;
|
||||
use crate::Properties;
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[derive(Clone, Properties, Default, PartialEq)]
|
||||
@ -461,11 +615,10 @@ mod tests {
|
||||
let scope = Scope::<Comp>::new(None);
|
||||
let el = document.create_element("div").unwrap();
|
||||
let node_ref = NodeRef::default();
|
||||
let render_state = ComponentRenderState::new(el, NodeRef::default(), &node_ref);
|
||||
let lifecycle = props.lifecycle.clone();
|
||||
|
||||
lifecycle.borrow_mut().clear();
|
||||
scope.mount_in_place(render_state, node_ref, Rc::new(props));
|
||||
scope.mount_in_place(el, NodeRef::default(), node_ref, Rc::new(props));
|
||||
crate::scheduler::start_now();
|
||||
|
||||
assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
//! Components wrapped with context including properties, state, and link
|
||||
|
||||
mod children;
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
mod lifecycle;
|
||||
mod properties;
|
||||
mod scope;
|
||||
@ -8,45 +9,54 @@ mod scope;
|
||||
use super::{Html, HtmlResult, IntoHtmlResult};
|
||||
pub use children::*;
|
||||
pub use properties::*;
|
||||
#[cfg(feature = "csr")]
|
||||
pub(crate) use scope::Scoped;
|
||||
pub use scope::{AnyScope, Scope, SendAsMessage};
|
||||
use std::rc::Rc;
|
||||
#[cfg(debug_assertions)]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {
|
||||
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<usize, Vec<String>>>
|
||||
= Default::default();
|
||||
static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
}
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
mod feat_csr_ssr {
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {
|
||||
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<usize, Vec<String>>>
|
||||
= Default::default();
|
||||
static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
}
|
||||
|
||||
/// Push [Component] event to lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow_mut()
|
||||
.entry(vcomp_id)
|
||||
.or_default()
|
||||
.push(event.to_string())
|
||||
});
|
||||
}
|
||||
/// Push [Component] event to lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow_mut()
|
||||
.entry(vcomp_id)
|
||||
.or_default()
|
||||
.push(event.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
/// Get [Component] event log from lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_event_log(vcomp_id: usize) -> Vec<String> {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow()
|
||||
.get(&vcomp_id)
|
||||
.map(|l| (*l).clone())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
/// Get [Component] event log from lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_event_log(vcomp_id: usize) -> Vec<String> {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow()
|
||||
.get(&vcomp_id)
|
||||
.map(|l| (*l).clone())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn next_id() -> usize {
|
||||
COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn next_id() -> usize {
|
||||
COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
pub(crate) use feat_csr_ssr::*;
|
||||
|
||||
/// The [`Component`]'s context. This contains component's [`Scope`] and and props and
|
||||
/// is passed to every lifecycle method.
|
||||
|
||||
@ -1,63 +1,21 @@
|
||||
//! Component scope module
|
||||
|
||||
use super::{
|
||||
lifecycle::{
|
||||
CompStateInner, ComponentState, CreateRunner, DestroyRunner, RenderRunner, UpdateEvent,
|
||||
UpdateRunner,
|
||||
},
|
||||
BaseComponent,
|
||||
};
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
use crate::scheduler::Shared;
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
use super::lifecycle::{ComponentState, UpdateEvent, UpdateRunner};
|
||||
use super::BaseComponent;
|
||||
use crate::callback::Callback;
|
||||
use crate::context::{ContextHandle, ContextProvider};
|
||||
use crate::dom_bundle::{ComponentRenderState, Scoped};
|
||||
use crate::html::IntoComponent;
|
||||
use crate::html::NodeRef;
|
||||
use crate::scheduler::{self, Shared};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::{fmt, iter};
|
||||
use web_sys::Element;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MsgQueue<Msg>(Shared<Vec<Msg>>);
|
||||
|
||||
impl<Msg> MsgQueue<Msg> {
|
||||
pub fn new() -> Self {
|
||||
MsgQueue(Rc::default())
|
||||
}
|
||||
|
||||
pub fn push(&self, msg: Msg) -> usize {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.push(msg);
|
||||
|
||||
inner.len()
|
||||
}
|
||||
|
||||
pub fn append(&self, other: &mut Vec<Msg>) -> usize {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.append(other);
|
||||
|
||||
inner.len()
|
||||
}
|
||||
|
||||
pub fn drain(&self) -> Vec<Msg> {
|
||||
let mut other_queue = Vec::new();
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
std::mem::swap(&mut *inner, &mut other_queue);
|
||||
|
||||
other_queue
|
||||
}
|
||||
}
|
||||
|
||||
impl<Msg> Clone for MsgQueue<Msg> {
|
||||
fn clone(&self) -> Self {
|
||||
MsgQueue(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Untyped scope used for accessing parent scope
|
||||
#[derive(Clone)]
|
||||
@ -84,6 +42,7 @@ impl<COMP: BaseComponent> From<Scope<COMP>> for AnyScope {
|
||||
}
|
||||
|
||||
impl AnyScope {
|
||||
#[cfg(feature = "csr")]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test() -> Self {
|
||||
Self {
|
||||
@ -142,49 +101,15 @@ impl AnyScope {
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Scoped for Scope<COMP> {
|
||||
fn to_any(&self) -> AnyScope {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>> {
|
||||
let state_ref = self.state.borrow();
|
||||
|
||||
// check that component hasn't been destroyed
|
||||
state_ref.as_ref()?;
|
||||
|
||||
Some(Ref::map(state_ref, |state_ref| {
|
||||
&state_ref.as_ref().unwrap().render_state
|
||||
}))
|
||||
}
|
||||
|
||||
/// Process an event to destroy a component
|
||||
fn destroy(self, parent_to_detach: bool) {
|
||||
scheduler::push_component_destroy(DestroyRunner {
|
||||
state: self.state,
|
||||
parent_to_detach,
|
||||
});
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool) {
|
||||
self.destroy(parent_to_detach)
|
||||
}
|
||||
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
|
||||
let mut state_ref = self.state.borrow_mut();
|
||||
if let Some(render_state) = state_ref.as_mut() {
|
||||
render_state.render_state.shift(parent, next_sibling)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A context which allows sending messages to a component.
|
||||
pub struct Scope<COMP: BaseComponent> {
|
||||
_marker: PhantomData<COMP>,
|
||||
parent: Option<Rc<AnyScope>>,
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
pub(crate) pending_messages: MsgQueue<COMP::Message>,
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
pub(crate) state: Shared<Option<ComponentState>>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -201,8 +126,12 @@ impl<COMP: BaseComponent> Clone for Scope<COMP> {
|
||||
fn clone(&self) -> Self {
|
||||
Scope {
|
||||
_marker: PhantomData,
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
pending_messages: self.pending_messages.clone(),
|
||||
parent: self.parent.clone(),
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
state: self.state.clone(),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -217,107 +146,6 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
||||
self.parent.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the linked component if available
|
||||
pub fn get_component(&self) -> Option<impl Deref<Target = COMP> + '_> {
|
||||
self.state.try_borrow().ok().and_then(|state_ref| {
|
||||
state_ref.as_ref()?;
|
||||
Some(Ref::map(state_ref, |state| {
|
||||
&state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.inner
|
||||
.as_any()
|
||||
.downcast_ref::<CompStateInner<COMP>>()
|
||||
.unwrap()
|
||||
.component
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/// Crate a scope with an optional parent scope
|
||||
pub(crate) fn new(parent: Option<AnyScope>) -> Self {
|
||||
let parent = parent.map(Rc::new);
|
||||
let state = Rc::new(RefCell::new(None));
|
||||
let pending_messages = MsgQueue::new();
|
||||
|
||||
Scope {
|
||||
_marker: PhantomData,
|
||||
pending_messages,
|
||||
state,
|
||||
parent,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id: super::next_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mounts a component with `props` to the specified `element` in the DOM.
|
||||
pub(crate) fn mount_in_place(
|
||||
&self,
|
||||
initial_render_state: ComponentRenderState,
|
||||
node_ref: NodeRef,
|
||||
props: Rc<COMP::Properties>,
|
||||
) {
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
initial_render_state,
|
||||
node_ref,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
pub(crate) fn reuse(
|
||||
&self,
|
||||
props: Rc<COMP::Properties>,
|
||||
node_ref: NodeRef,
|
||||
next_sibling: NodeRef,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
super::log_event(self.vcomp_id, "reuse");
|
||||
|
||||
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling));
|
||||
}
|
||||
|
||||
fn push_update(&self, event: UpdateEvent) {
|
||||
scheduler::push_component_update(UpdateRunner {
|
||||
state: self.state.clone(),
|
||||
event,
|
||||
});
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
/// Send a message to the component.
|
||||
pub fn send_message<T>(&self, msg: T)
|
||||
where
|
||||
T: Into<COMP::Message>,
|
||||
{
|
||||
// We are the first message in queue, so we queue the update.
|
||||
if self.pending_messages.push(msg.into()) == 1 {
|
||||
self.push_update(UpdateEvent::Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a batch of messages to the component.
|
||||
///
|
||||
/// This is slightly more efficient than calling [`send_message`](Self::send_message)
|
||||
/// in a loop.
|
||||
pub fn send_message_batch(&self, mut messages: Vec<COMP::Message>) {
|
||||
let msg_len = messages.len();
|
||||
|
||||
// The queue was empty, so we queue the update
|
||||
if self.pending_messages.append(&mut messages) == msg_len {
|
||||
self.push_update(UpdateEvent::Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Callback` which will send a message to the linked
|
||||
/// component's update method when invoked.
|
||||
pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
|
||||
@ -363,29 +191,46 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
||||
&self,
|
||||
callback: Callback<T>,
|
||||
) -> Option<(T, ContextHandle<T>)> {
|
||||
self.to_any().context(callback)
|
||||
AnyScope::from(self.clone()).context(callback)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use crate::scheduler;
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use crate::html::component::lifecycle::{
|
||||
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
|
||||
};
|
||||
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
pub(crate) async fn render_to_string(self, w: &mut String, props: Rc<COMP::Properties>) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let initial_render_state = ComponentRenderState::new_ssr(tx);
|
||||
let state = ComponentRenderState::Ssr { sender: Some(tx) };
|
||||
|
||||
self.mount_in_place(initial_render_state, NodeRef::default(), props);
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
initial_render_state: state,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
scheduler::start();
|
||||
|
||||
let html = rx.await.unwrap();
|
||||
|
||||
let self_any_scope = self.to_any();
|
||||
let self_any_scope = AnyScope::from(self.clone());
|
||||
html.render_to_string(w, &self_any_scope).await;
|
||||
|
||||
scheduler::push_component_destroy(DestroyRunner {
|
||||
state: self.state.clone(),
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
parent_to_detach: false,
|
||||
});
|
||||
scheduler::start();
|
||||
@ -393,6 +238,262 @@ mod feat_ssr {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "ssr", feature = "csr")))]
|
||||
mod feat_no_render_ssr {
|
||||
use super::*;
|
||||
|
||||
// Skeleton code to provide public methods when no renderer are enabled.
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
/// Returns the linked component if available
|
||||
pub fn get_component(&self) -> Option<impl Deref<Target = COMP> + '_> {
|
||||
Option::<&COMP>::None
|
||||
}
|
||||
|
||||
/// Send a message to the component.
|
||||
pub fn send_message<T>(&self, _msg: T)
|
||||
where
|
||||
T: Into<COMP::Message>,
|
||||
{
|
||||
}
|
||||
|
||||
/// Send a batch of messages to the component.
|
||||
///
|
||||
/// This is slightly more efficient than calling [`send_message`](Self::send_message)
|
||||
/// in a loop.
|
||||
pub fn send_message_batch(&self, _messages: Vec<COMP::Message>) {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
mod feat_csr_ssr {
|
||||
use super::*;
|
||||
use crate::scheduler::{self, Shared};
|
||||
use std::cell::Ref;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MsgQueue<Msg>(Shared<Vec<Msg>>);
|
||||
|
||||
impl<Msg> MsgQueue<Msg> {
|
||||
pub fn new() -> Self {
|
||||
MsgQueue(Rc::default())
|
||||
}
|
||||
|
||||
pub fn push(&self, msg: Msg) -> usize {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.push(msg);
|
||||
|
||||
inner.len()
|
||||
}
|
||||
|
||||
pub fn append(&self, other: &mut Vec<Msg>) -> usize {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.append(other);
|
||||
|
||||
inner.len()
|
||||
}
|
||||
|
||||
pub fn drain(&self) -> Vec<Msg> {
|
||||
let mut other_queue = Vec::new();
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
std::mem::swap(&mut *inner, &mut other_queue);
|
||||
|
||||
other_queue
|
||||
}
|
||||
}
|
||||
|
||||
impl<Msg> Clone for MsgQueue<Msg> {
|
||||
fn clone(&self) -> Self {
|
||||
MsgQueue(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
/// Crate a scope with an optional parent scope
|
||||
pub(crate) fn new(parent: Option<AnyScope>) -> Self {
|
||||
let parent = parent.map(Rc::new);
|
||||
|
||||
let state = Rc::new(RefCell::new(None));
|
||||
|
||||
let pending_messages = MsgQueue::new();
|
||||
|
||||
Scope {
|
||||
_marker: PhantomData,
|
||||
|
||||
pending_messages,
|
||||
|
||||
state,
|
||||
parent,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id: super::super::next_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the linked component if available
|
||||
pub fn get_component(&self) -> Option<impl Deref<Target = COMP> + '_> {
|
||||
self.state.try_borrow().ok().and_then(|state_ref| {
|
||||
state_ref.as_ref()?;
|
||||
// TODO: Replace unwrap with Ref::filter_map once it becomes stable.
|
||||
Some(Ref::map(state_ref, |state| {
|
||||
state
|
||||
.as_ref()
|
||||
.and_then(|m| m.downcast_comp_ref::<COMP>())
|
||||
.unwrap()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn push_update(&self, event: UpdateEvent) {
|
||||
scheduler::push_component_update(UpdateRunner {
|
||||
state: self.state.clone(),
|
||||
event,
|
||||
});
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
/// Send a message to the component.
|
||||
pub fn send_message<T>(&self, msg: T)
|
||||
where
|
||||
T: Into<COMP::Message>,
|
||||
{
|
||||
// We are the first message in queue, so we queue the update.
|
||||
if self.pending_messages.push(msg.into()) == 1 {
|
||||
self.push_update(UpdateEvent::Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a batch of messages to the component.
|
||||
///
|
||||
/// This is slightly more efficient than calling [`send_message`](Self::send_message)
|
||||
/// in a loop.
|
||||
pub fn send_message_batch(&self, mut messages: Vec<COMP::Message>) {
|
||||
let msg_len = messages.len();
|
||||
|
||||
// The queue was empty, so we queue the update
|
||||
if self.pending_messages.append(&mut messages) == msg_len {
|
||||
self.push_update(UpdateEvent::Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
pub(crate) use feat_csr_ssr::*;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
mod feat_csr {
|
||||
use super::*;
|
||||
use crate::dom_bundle::Bundle;
|
||||
use crate::html::component::lifecycle::{
|
||||
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
|
||||
};
|
||||
use crate::html::NodeRef;
|
||||
use crate::scheduler;
|
||||
use std::cell::Ref;
|
||||
use web_sys::Element;
|
||||
|
||||
impl<COMP> Scope<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
/// Mounts a component with `props` to the specified `element` in the DOM.
|
||||
pub(crate) fn mount_in_place(
|
||||
&self,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
node_ref: NodeRef,
|
||||
props: Rc<COMP::Properties>,
|
||||
) {
|
||||
let bundle = Bundle::new(&parent, &next_sibling, &node_ref);
|
||||
let state = ComponentRenderState::Render {
|
||||
bundle,
|
||||
node_ref,
|
||||
parent,
|
||||
next_sibling,
|
||||
};
|
||||
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
initial_render_state: state,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
pub(crate) fn reuse(
|
||||
&self,
|
||||
props: Rc<COMP::Properties>,
|
||||
node_ref: NodeRef,
|
||||
next_sibling: NodeRef,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
super::super::log_event(self.vcomp_id, "reuse");
|
||||
|
||||
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Scoped {
|
||||
fn to_any(&self) -> AnyScope;
|
||||
/// Get the render state if it hasn't already been destroyed
|
||||
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>>;
|
||||
/// Shift the node associated with this scope to a new place
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
|
||||
/// Process an event to destroy a component
|
||||
fn destroy(self, parent_to_detach: bool);
|
||||
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool);
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Scoped for Scope<COMP> {
|
||||
fn to_any(&self) -> AnyScope {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>> {
|
||||
let state_ref = self.state.borrow();
|
||||
|
||||
// check that component hasn't been destroyed
|
||||
state_ref.as_ref()?;
|
||||
|
||||
Some(Ref::map(state_ref, |state_ref| {
|
||||
&state_ref.as_ref().unwrap().render_state
|
||||
}))
|
||||
}
|
||||
|
||||
/// Process an event to destroy a component
|
||||
fn destroy(self, parent_to_detach: bool) {
|
||||
scheduler::push_component_destroy(DestroyRunner {
|
||||
state: self.state,
|
||||
parent_to_detach,
|
||||
});
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool) {
|
||||
self.destroy(parent_to_detach)
|
||||
}
|
||||
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
|
||||
scheduler::push_component_update(UpdateRunner {
|
||||
state: self.state.clone(),
|
||||
event: UpdateEvent::Shift(parent, next_sibling),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub(crate) use feat_csr::*;
|
||||
|
||||
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
|
||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||
mod feat_io {
|
||||
|
||||
@ -125,43 +125,50 @@ impl NodeRef {
|
||||
node.map(Into::into).map(INTO::from)
|
||||
}
|
||||
|
||||
/// Wrap an existing `Node` in a `NodeRef`
|
||||
pub(crate) fn new(node: Node) -> Self {
|
||||
let node_ref = NodeRef::default();
|
||||
node_ref.set(Some(node));
|
||||
node_ref
|
||||
}
|
||||
|
||||
/// Place a Node in a reference for later use
|
||||
pub(crate) fn set(&self, node: Option<Node>) {
|
||||
let mut this = self.0.borrow_mut();
|
||||
this.node = node;
|
||||
this.link = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a downstream `NodeRef`
|
||||
pub(crate) fn link(&self, node_ref: Self) {
|
||||
// Avoid circular references
|
||||
if self == &node_ref {
|
||||
return;
|
||||
#[cfg(feature = "csr")]
|
||||
mod feat_csr {
|
||||
use super::*;
|
||||
|
||||
impl NodeRef {
|
||||
/// Reuse an existing `NodeRef`
|
||||
pub(crate) fn reuse(&self, node_ref: Self) {
|
||||
// Avoid circular references
|
||||
if self == &node_ref {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut this = self.0.borrow_mut();
|
||||
let existing = node_ref.0.borrow();
|
||||
this.node = existing.node.clone();
|
||||
this.link = existing.link.clone();
|
||||
}
|
||||
|
||||
let mut this = self.0.borrow_mut();
|
||||
this.node = None;
|
||||
this.link = Some(node_ref);
|
||||
}
|
||||
/// Link a downstream `NodeRef`
|
||||
pub(crate) fn link(&self, node_ref: Self) {
|
||||
// Avoid circular references
|
||||
if self == &node_ref {
|
||||
return;
|
||||
}
|
||||
|
||||
/// Reuse an existing `NodeRef`
|
||||
pub(crate) fn reuse(&self, node_ref: Self) {
|
||||
// Avoid circular references
|
||||
if self == &node_ref {
|
||||
return;
|
||||
let mut this = self.0.borrow_mut();
|
||||
this.node = None;
|
||||
this.link = Some(node_ref);
|
||||
}
|
||||
|
||||
let mut this = self.0.borrow_mut();
|
||||
let existing = node_ref.0.borrow();
|
||||
this.node = existing.node.clone();
|
||||
this.link = existing.link.clone();
|
||||
/// Wrap an existing `Node` in a `NodeRef`
|
||||
pub(crate) fn new(node: Node) -> Self {
|
||||
let node_ref = NodeRef::default();
|
||||
node_ref.set(Some(node));
|
||||
node_ref
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,15 +180,14 @@ pub fn create_portal(child: Html, host: Element) -> Html {
|
||||
VNode::VPortal(VPortal::new(child, host))
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gloo_utils::document;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
//! Server-Side Rendering should work on all targets when feature `ssr` is enabled.
|
||||
//!
|
||||
//! ### Supported Features:
|
||||
//! - `csr`: Enables Client-side Rendering support and [`Renderer`].
|
||||
//! Only enable this feature if you are making a Yew application (not a library).
|
||||
//! - `ssr`: Enables Server-side Rendering support and [`ServerRenderer`].
|
||||
//! - `tokio`: Enables future-based APIs on non-wasm32 targets with tokio runtime. (You may want to
|
||||
//! enable this if your application uses future-based APIs and it does not compile / lint on
|
||||
@ -67,7 +69,7 @@
|
||||
//!
|
||||
//!# fn dont_execute() {
|
||||
//! fn main() {
|
||||
//! yew::start_app::<App>();
|
||||
//! yew::Renderer::<App>::new().render();
|
||||
//! }
|
||||
//!# }
|
||||
//! ```
|
||||
@ -84,8 +86,6 @@
|
||||
#![recursion_limit = "512"]
|
||||
extern crate self as yew;
|
||||
|
||||
use std::{cell::Cell, panic::PanicInfo};
|
||||
|
||||
/// This macro provides a convenient way to create [`Classes`].
|
||||
///
|
||||
/// The macro takes a list of items similar to the [`vec!`] macro and returns a [`Classes`] instance.
|
||||
@ -265,6 +265,8 @@ pub mod macros {
|
||||
|
||||
pub mod callback;
|
||||
pub mod context;
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "csr")))]
|
||||
#[cfg(feature = "csr")]
|
||||
mod dom_bundle;
|
||||
pub mod functional;
|
||||
pub mod html;
|
||||
@ -278,16 +280,20 @@ pub mod utils;
|
||||
pub mod virtual_dom;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use server_renderer::*;
|
||||
#[cfg(feature = "csr")]
|
||||
mod app_handle;
|
||||
#[cfg(feature = "csr")]
|
||||
mod renderer;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
pub use crate::dom_bundle::layout_tests;
|
||||
}
|
||||
pub mod tests;
|
||||
|
||||
/// The module that contains all events available in the framework.
|
||||
pub mod events {
|
||||
pub use crate::html::TargetCast;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub use crate::dom_bundle::set_event_bubbling;
|
||||
|
||||
#[doc(no_inline)]
|
||||
@ -297,89 +303,24 @@ pub mod events {
|
||||
};
|
||||
}
|
||||
|
||||
pub use crate::dom_bundle::AppHandle;
|
||||
use web_sys::Element;
|
||||
#[cfg(feature = "csr")]
|
||||
pub use crate::app_handle::AppHandle;
|
||||
#[cfg(feature = "csr")]
|
||||
pub use crate::renderer::{set_custom_panic_hook, Renderer};
|
||||
|
||||
use crate::html::IntoComponent;
|
||||
|
||||
thread_local! {
|
||||
static PANIC_HOOK_IS_SET: Cell<bool> = Cell::new(false);
|
||||
}
|
||||
|
||||
/// Set a custom panic hook.
|
||||
/// Unless a panic hook is set through this function, Yew will
|
||||
/// overwrite any existing panic hook when one of the `start_app*` functions are called.
|
||||
pub fn set_custom_panic_hook(hook: Box<dyn Fn(&PanicInfo<'_>) + Sync + Send + 'static>) {
|
||||
std::panic::set_hook(hook);
|
||||
PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true));
|
||||
}
|
||||
|
||||
fn set_default_panic_hook() {
|
||||
if !PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.replace(true)) {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
}
|
||||
}
|
||||
|
||||
/// The main entry point of a Yew application.
|
||||
/// If you would like to pass props, use the `start_app_with_props_in_element` method.
|
||||
pub fn start_app_in_element<ICOMP>(element: Element) -> AppHandle<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent,
|
||||
ICOMP::Properties: Default,
|
||||
{
|
||||
start_app_with_props_in_element(element, ICOMP::Properties::default())
|
||||
}
|
||||
|
||||
/// Starts an yew app mounted to the body of the document.
|
||||
/// Alias to start_app_in_element(Body)
|
||||
pub fn start_app<ICOMP>() -> AppHandle<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent,
|
||||
ICOMP::Properties: Default,
|
||||
{
|
||||
start_app_with_props(ICOMP::Properties::default())
|
||||
}
|
||||
|
||||
/// The main entry point of a Yew application. This function does the
|
||||
/// same as `start_app_in_element(...)` but allows to start an Yew application with properties.
|
||||
pub fn start_app_with_props_in_element<ICOMP>(
|
||||
element: Element,
|
||||
props: ICOMP::Properties,
|
||||
) -> AppHandle<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent,
|
||||
{
|
||||
set_default_panic_hook();
|
||||
AppHandle::<ICOMP>::mount_with_props(element, Rc::new(props))
|
||||
}
|
||||
|
||||
/// The main entry point of a Yew application.
|
||||
/// This function does the same as `start_app(...)` but allows to start an Yew application with properties.
|
||||
pub fn start_app_with_props<ICOMP>(props: ICOMP::Properties) -> AppHandle<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent,
|
||||
{
|
||||
start_app_with_props_in_element(
|
||||
gloo_utils::document()
|
||||
.body()
|
||||
.expect("no body node found")
|
||||
.into(),
|
||||
props,
|
||||
)
|
||||
}
|
||||
|
||||
/// The Yew Prelude
|
||||
///
|
||||
/// The purpose of this module is to alleviate imports of many common types:
|
||||
///
|
||||
/// ```
|
||||
/// # #![allow(unused_imports)]
|
||||
/// use yew::prelude::*;
|
||||
/// ```
|
||||
pub mod prelude {
|
||||
//! The Yew Prelude
|
||||
//!
|
||||
//! The purpose of this module is to alleviate imports of many common types:
|
||||
//!
|
||||
//! ```
|
||||
//! # #![allow(unused_imports)]
|
||||
//! use yew::prelude::*;
|
||||
//! ```
|
||||
#[cfg(feature = "csr")]
|
||||
pub use crate::app_handle::AppHandle;
|
||||
pub use crate::callback::Callback;
|
||||
pub use crate::context::{ContextHandle, ContextProvider};
|
||||
pub use crate::dom_bundle::AppHandle;
|
||||
pub use crate::events::*;
|
||||
pub use crate::html::{
|
||||
create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context,
|
||||
@ -393,4 +334,3 @@ pub mod prelude {
|
||||
}
|
||||
|
||||
pub use self::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
94
packages/yew/src/renderer.rs
Normal file
94
packages/yew/src/renderer.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use std::cell::Cell;
|
||||
use std::panic::PanicInfo;
|
||||
use std::rc::Rc;
|
||||
|
||||
use web_sys::Element;
|
||||
|
||||
use crate::app_handle::AppHandle;
|
||||
use crate::html::IntoComponent;
|
||||
|
||||
thread_local! {
|
||||
static PANIC_HOOK_IS_SET: Cell<bool> = Cell::new(false);
|
||||
}
|
||||
|
||||
/// Set a custom panic hook.
|
||||
/// Unless a panic hook is set through this function, Yew will
|
||||
/// overwrite any existing panic hook when an application is rendered with [Renderer].
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "csr")))]
|
||||
pub fn set_custom_panic_hook(hook: Box<dyn Fn(&PanicInfo<'_>) + Sync + Send + 'static>) {
|
||||
std::panic::set_hook(hook);
|
||||
PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true));
|
||||
}
|
||||
|
||||
fn set_default_panic_hook() {
|
||||
if !PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.replace(true)) {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
}
|
||||
}
|
||||
|
||||
/// The Yew Renderer.
|
||||
///
|
||||
/// This is the main entry point of a Yew application.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "csr")))]
|
||||
#[must_use = "Renderer does nothing unless render() is called."]
|
||||
pub struct Renderer<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent + 'static,
|
||||
{
|
||||
root: Element,
|
||||
props: ICOMP::Properties,
|
||||
}
|
||||
|
||||
impl<ICOMP> Default for Renderer<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent + 'static,
|
||||
ICOMP::Properties: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::with_props(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<ICOMP> Renderer<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent + 'static,
|
||||
ICOMP::Properties: Default,
|
||||
{
|
||||
/// Creates a [Renderer] that renders into the document body with default properties.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Creates a [Renderer] that renders into a custom root with default properties.
|
||||
pub fn with_root(root: Element) -> Self {
|
||||
Self::with_root_and_props(root, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<ICOMP> Renderer<ICOMP>
|
||||
where
|
||||
ICOMP: IntoComponent + 'static,
|
||||
{
|
||||
/// Creates a [Renderer] that renders into the document body with custom properties.
|
||||
pub fn with_props(props: ICOMP::Properties) -> Self {
|
||||
Self::with_root_and_props(
|
||||
gloo_utils::document()
|
||||
.body()
|
||||
.expect("no body node found")
|
||||
.into(),
|
||||
props,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a [Renderer] that renders into a custom root with custom properties.
|
||||
pub fn with_root_and_props(root: Element, props: ICOMP::Properties) -> Self {
|
||||
Self { root, props }
|
||||
}
|
||||
|
||||
/// Renders the application.
|
||||
pub fn render(self) -> AppHandle<ICOMP> {
|
||||
set_default_panic_hook();
|
||||
AppHandle::<ICOMP>::mount_with_props(self.root, Rc::new(self.props))
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
//! This module contains a scheduler.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{hash_map::Entry, HashMap, VecDeque};
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Alias for Rc<RefCell<T>>
|
||||
@ -25,10 +25,13 @@ struct Scheduler {
|
||||
create: Vec<Box<dyn Runnable>>,
|
||||
update: Vec<Box<dyn Runnable>>,
|
||||
render_first: VecDeque<Box<dyn Runnable>>,
|
||||
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
render: RenderScheduler,
|
||||
|
||||
/// Stacks to ensure child calls are always before parent calls
|
||||
rendered_first: Vec<Box<dyn Runnable>>,
|
||||
#[cfg(feature = "csr")]
|
||||
rendered: RenderedScheduler,
|
||||
}
|
||||
|
||||
@ -54,50 +57,155 @@ pub fn push(runnable: Box<dyn Runnable>) {
|
||||
start();
|
||||
}
|
||||
|
||||
/// Push a component creation, first render and first rendered [Runnable]s to be executed
|
||||
pub(crate) fn push_component_create(
|
||||
create: impl Runnable + 'static,
|
||||
first_render: impl Runnable + 'static,
|
||||
) {
|
||||
with(|s| {
|
||||
s.create.push(Box::new(create));
|
||||
s.render_first.push_back(Box::new(first_render));
|
||||
});
|
||||
}
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
mod feat_csr_ssr {
|
||||
use super::*;
|
||||
|
||||
/// Push a component destruction [Runnable] to be executed
|
||||
pub(crate) fn push_component_destroy(runnable: impl Runnable + 'static) {
|
||||
with(|s| s.destroy.push(Box::new(runnable)));
|
||||
}
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
|
||||
/// Push a component render and rendered [Runnable]s to be executed
|
||||
pub(crate) fn push_component_render(component_id: usize, render: impl Runnable + 'static) {
|
||||
with(|s| {
|
||||
s.render.schedule(component_id, Box::new(render));
|
||||
});
|
||||
}
|
||||
/// Push a component creation, first render and first rendered [Runnable]s to be executed
|
||||
pub(crate) fn push_component_create(
|
||||
create: impl Runnable + 'static,
|
||||
first_render: impl Runnable + 'static,
|
||||
) {
|
||||
with(|s| {
|
||||
s.create.push(Box::new(create));
|
||||
s.render_first.push_back(Box::new(first_render));
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn push_component_rendered(
|
||||
component_id: usize,
|
||||
rendered: impl Runnable + 'static,
|
||||
first_render: bool,
|
||||
) {
|
||||
with(|s| {
|
||||
let rendered = Box::new(rendered);
|
||||
/// Push a component destruction [Runnable] to be executed
|
||||
pub(crate) fn push_component_destroy(runnable: impl Runnable + 'static) {
|
||||
with(|s| s.destroy.push(Box::new(runnable)));
|
||||
}
|
||||
|
||||
if first_render {
|
||||
s.rendered_first.push(rendered);
|
||||
} else {
|
||||
s.rendered.schedule(component_id, rendered);
|
||||
/// Push a component render and rendered [Runnable]s to be executed
|
||||
pub(crate) fn push_component_render(component_id: usize, render: impl Runnable + 'static) {
|
||||
with(|s| {
|
||||
s.render.schedule(component_id, Box::new(render));
|
||||
});
|
||||
}
|
||||
|
||||
/// Push a component update [Runnable] to be executed
|
||||
pub(crate) fn push_component_update(runnable: impl Runnable + 'static) {
|
||||
with(|s| s.update.push(Box::new(runnable)));
|
||||
}
|
||||
|
||||
/// Task to be executed for specific component
|
||||
struct QueueTask {
|
||||
/// Tasks in the queue to skip for this component
|
||||
skip: usize,
|
||||
|
||||
/// Runnable to execute
|
||||
runnable: Box<dyn Runnable>,
|
||||
}
|
||||
|
||||
/// Scheduler for non-first component renders with deduplication
|
||||
#[derive(Default)]
|
||||
pub(super) struct RenderScheduler {
|
||||
/// Task registry by component ID
|
||||
tasks: HashMap<usize, QueueTask>,
|
||||
|
||||
/// Task queue by component ID
|
||||
queue: VecDeque<usize>,
|
||||
}
|
||||
|
||||
impl RenderScheduler {
|
||||
/// Schedule render task execution
|
||||
pub fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
|
||||
self.queue.push_back(component_id);
|
||||
match self.tasks.entry(component_id) {
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(QueueTask { skip: 0, runnable });
|
||||
}
|
||||
Entry::Occupied(mut e) => {
|
||||
let v = e.get_mut();
|
||||
v.skip += 1;
|
||||
|
||||
// Technically the 2 runners should be functionally identical, but might as well
|
||||
// overwrite it for good measure, accounting for future changes. We have it here
|
||||
// anyway.
|
||||
v.runnable = runnable;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// Try to pop a task from the queue, if any
|
||||
pub fn pop(&mut self) -> Option<Box<dyn Runnable>> {
|
||||
while let Some(id) = self.queue.pop_front() {
|
||||
match self.tasks.entry(id) {
|
||||
Entry::Occupied(mut e) => {
|
||||
let v = e.get_mut();
|
||||
if v.skip == 0 {
|
||||
return Some(e.remove().runnable);
|
||||
}
|
||||
v.skip -= 1;
|
||||
}
|
||||
Entry::Vacant(_) => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a component update [Runnable] to be executed
|
||||
pub(crate) fn push_component_update(runnable: impl Runnable + 'static) {
|
||||
with(|s| s.update.push(Box::new(runnable)));
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
pub(crate) use feat_csr_ssr::*;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
mod feat_csr {
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn push_component_rendered(
|
||||
component_id: usize,
|
||||
rendered: impl Runnable + 'static,
|
||||
first_render: bool,
|
||||
) {
|
||||
with(|s| {
|
||||
let rendered = Box::new(rendered);
|
||||
|
||||
if first_render {
|
||||
s.rendered_first.push(rendered);
|
||||
} else {
|
||||
s.rendered.schedule(component_id, rendered);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Deduplicating scheduler for component rendered calls with deduplication
|
||||
#[derive(Default)]
|
||||
pub(super) struct RenderedScheduler {
|
||||
/// Task registry by component ID
|
||||
tasks: HashMap<usize, Box<dyn Runnable>>,
|
||||
|
||||
/// Task stack by component ID
|
||||
stack: Vec<usize>,
|
||||
}
|
||||
|
||||
impl RenderedScheduler {
|
||||
/// Schedule rendered task execution
|
||||
pub fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
|
||||
if self.tasks.insert(component_id, runnable).is_none() {
|
||||
self.stack.push(component_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain all tasks into `dst`, if any
|
||||
pub fn drain_into(&mut self, dst: &mut Vec<Box<dyn Runnable>>) {
|
||||
for id in self.stack.drain(..).rev() {
|
||||
if let Some(t) = self.tasks.remove(&id) {
|
||||
dst.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
pub(crate) use feat_csr::*;
|
||||
|
||||
/// Execute any pending [Runnable]s
|
||||
pub(crate) fn start_now() {
|
||||
thread_local! {
|
||||
@ -195,107 +303,29 @@ impl Scheduler {
|
||||
// Likely to cause duplicate renders via component updates, so placed before them
|
||||
to_run.append(&mut self.main);
|
||||
|
||||
// Run after all possible updates to avoid duplicate renders.
|
||||
//
|
||||
// Should be processed one at time, because they can spawn more create and first render
|
||||
// events for their children.
|
||||
if !to_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(r) = self.render.pop() {
|
||||
to_run.push(r);
|
||||
}
|
||||
|
||||
// These typically do nothing and don't spawn any other events - can be batched.
|
||||
// Should be run only after all renders have finished.
|
||||
if !to_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.rendered.drain_into(to_run);
|
||||
}
|
||||
}
|
||||
|
||||
/// Task to be executed for specific component
|
||||
struct QueueTask {
|
||||
/// Tasks in the queue to skip for this component
|
||||
skip: usize,
|
||||
|
||||
/// Runnable to execute
|
||||
runnable: Box<dyn Runnable>,
|
||||
}
|
||||
|
||||
/// Scheduler for non-first component renders with deduplication
|
||||
#[derive(Default)]
|
||||
struct RenderScheduler {
|
||||
/// Task registry by component ID
|
||||
tasks: HashMap<usize, QueueTask>,
|
||||
|
||||
/// Task queue by component ID
|
||||
queue: VecDeque<usize>,
|
||||
}
|
||||
|
||||
impl RenderScheduler {
|
||||
/// Schedule render task execution
|
||||
fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
|
||||
self.queue.push_back(component_id);
|
||||
match self.tasks.entry(component_id) {
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(QueueTask { skip: 0, runnable });
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
{
|
||||
// Run after all possible updates to avoid duplicate renders.
|
||||
//
|
||||
// Should be processed one at time, because they can spawn more create and first render
|
||||
// events for their children.
|
||||
if !to_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
Entry::Occupied(mut e) => {
|
||||
let v = e.get_mut();
|
||||
v.skip += 1;
|
||||
|
||||
// Technically the 2 runners should be functionally identical, but might as well
|
||||
// overwrite it for good measure, accounting for future changes. We have it here
|
||||
// anyway.
|
||||
v.runnable = runnable;
|
||||
if let Some(r) = self.render.pop() {
|
||||
to_run.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to pop a task from the queue, if any
|
||||
fn pop(&mut self) -> Option<Box<dyn Runnable>> {
|
||||
while let Some(id) = self.queue.pop_front() {
|
||||
match self.tasks.entry(id) {
|
||||
Entry::Occupied(mut e) => {
|
||||
let v = e.get_mut();
|
||||
if v.skip == 0 {
|
||||
return Some(e.remove().runnable);
|
||||
}
|
||||
v.skip -= 1;
|
||||
}
|
||||
Entry::Vacant(_) => (),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Deduplicating scheduler for component rendered calls with deduplication
|
||||
#[derive(Default)]
|
||||
struct RenderedScheduler {
|
||||
/// Task registry by component ID
|
||||
tasks: HashMap<usize, Box<dyn Runnable>>,
|
||||
|
||||
/// Task stack by component ID
|
||||
stack: Vec<usize>,
|
||||
}
|
||||
|
||||
impl RenderedScheduler {
|
||||
/// Schedule rendered task execution
|
||||
fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
|
||||
if self.tasks.insert(component_id, runnable).is_none() {
|
||||
self.stack.push(component_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain all tasks into `dst`, if any
|
||||
fn drain_into(&mut self, dst: &mut Vec<Box<dyn Runnable>>) {
|
||||
for id in self.stack.drain(..).rev() {
|
||||
if let Some(t) = self.tasks.remove(&id) {
|
||||
dst.push(t);
|
||||
#[cfg(feature = "csr")]
|
||||
{
|
||||
// These typically do nothing and don't spawn any other events - can be batched.
|
||||
// Should be run only after all renders have finished.
|
||||
if !to_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.rendered.drain_into(to_run);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ impl Component for Suspense {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "ssr"))]
|
||||
impl Suspense {
|
||||
pub(crate) fn suspend(&self, s: Suspension) {
|
||||
self.link.send_message(SuspenseMsg::Suspend(s));
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::dom_bundle::{BNode, DomBundle, Reconcilable};
|
||||
use crate::dom_bundle::Bundle;
|
||||
use crate::html::AnyScope;
|
||||
use crate::scheduler;
|
||||
use crate::virtual_dom::VNode;
|
||||
@ -51,7 +51,10 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
let vnode = layout.node.clone();
|
||||
log!("Independently apply layout '{}'", layout.name);
|
||||
|
||||
let (_, mut bundle) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone());
|
||||
let node_ref = NodeRef::default();
|
||||
|
||||
let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref);
|
||||
bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode);
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
@ -65,12 +68,7 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
|
||||
log!("Independently reapply layout '{}'", layout.name);
|
||||
|
||||
vnode.reconcile_node(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
next_sibling.clone(),
|
||||
&mut bundle,
|
||||
);
|
||||
bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode);
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
@ -91,17 +89,19 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
}
|
||||
|
||||
// Sequentially apply each layout
|
||||
let mut bundle: Option<BNode> = None;
|
||||
let node_ref = NodeRef::default();
|
||||
let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref);
|
||||
for layout in layouts.iter() {
|
||||
let next_vnode = layout.node.clone();
|
||||
|
||||
log!("Sequentially apply layout '{}'", layout.name);
|
||||
next_vnode.reconcile_sequentially(
|
||||
bundle.reconcile(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
next_sibling.clone(),
|
||||
&mut bundle,
|
||||
next_vnode,
|
||||
);
|
||||
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
@ -116,12 +116,13 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
let next_vnode = layout.node.clone();
|
||||
|
||||
log!("Sequentially detach layout '{}'", layout.name);
|
||||
next_vnode.reconcile_sequentially(
|
||||
bundle.reconcile(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
next_sibling.clone(),
|
||||
&mut bundle,
|
||||
next_vnode,
|
||||
);
|
||||
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
@ -132,9 +133,7 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
}
|
||||
|
||||
// Detach last layout
|
||||
if let Some(bundle) = bundle {
|
||||
bundle.detach(&parent_element, false);
|
||||
}
|
||||
bundle.detach(&parent_element, false);
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
1
packages/yew/src/tests/mod.rs
Normal file
1
packages/yew/src/tests/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod layout_tests;
|
||||
@ -1,14 +1,21 @@
|
||||
//! This module contains the implementation of a virtual component (`VComp`).
|
||||
|
||||
use super::Key;
|
||||
use crate::dom_bundle::{Mountable, PropsWrapper};
|
||||
use crate::html::{BaseComponent, IntoComponent, NodeRef};
|
||||
use std::any::TypeId;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {}
|
||||
#[cfg(any(feature = "ssr", feature = "csr"))]
|
||||
use crate::html::{AnyScope, Scope};
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
use crate::html::Scoped;
|
||||
#[cfg(feature = "csr")]
|
||||
use web_sys::Element;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
/// A virtual component.
|
||||
pub struct VComp {
|
||||
@ -40,6 +47,81 @@ impl Clone for VComp {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Mountable {
|
||||
fn copy(&self) -> Box<dyn Mountable>;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped>;
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
}
|
||||
|
||||
pub(crate) struct PropsWrapper<COMP: BaseComponent> {
|
||||
props: Rc<COMP::Properties>,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> PropsWrapper<COMP> {
|
||||
pub fn new(props: Rc<COMP::Properties>) -> Self {
|
||||
Self { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
fn copy(&self) -> Box<dyn Mountable> {
|
||||
let wrapper: PropsWrapper<COMP> = PropsWrapper {
|
||||
props: Rc::clone(&self.props),
|
||||
};
|
||||
Box::new(wrapper)
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped> {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.mount_in_place(parent, next_sibling, node_ref, self.props);
|
||||
|
||||
Box::new(scope)
|
||||
}
|
||||
|
||||
#[cfg(feature = "csr")]
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
|
||||
let scope: Scope<COMP> = scope.to_any().downcast::<COMP>();
|
||||
scope.reuse(self.props, node_ref, next_sibling);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.render_to_string(w, self.props.clone()).await;
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
/// A virtual child component.
|
||||
pub struct VChild<ICOMP: IntoComponent> {
|
||||
/// The component properties
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::obtain_result;
|
||||
@ -25,12 +27,13 @@ async fn props_are_passed() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_with_props_in_element::<PropsComponent>(
|
||||
yew::Renderer::<PropsComponent>::with_root_and_props(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
PropsPassedFunctionProps {
|
||||
value: "props".to_string(),
|
||||
},
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
let result = obtain_result();
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::obtain_result;
|
||||
@ -95,7 +97,8 @@ async fn suspense_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
|
||||
yew::Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||
.render();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
@ -244,7 +247,8 @@ async fn suspense_not_suspended_at_start() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
|
||||
yew::Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||
.render();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
|
||||
@ -362,7 +366,8 @@ async fn suspense_nested_suspense_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
|
||||
yew::Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||
.render();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
@ -517,10 +522,11 @@ async fn effects_not_run_when_suspended() {
|
||||
counter: counter.clone(),
|
||||
};
|
||||
|
||||
yew::start_app_with_props_in_element::<App>(
|
||||
yew::Renderer::<App>::with_root_and_props(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
props,
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::obtain_result_by_id;
|
||||
@ -61,9 +63,10 @@ async fn use_context_scoping_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseContextComponent>(
|
||||
yew::Renderer::<UseContextComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
@ -143,9 +146,10 @@ async fn use_context_works_with_multiple_types() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<TestComponent>(
|
||||
yew::Renderer::<TestComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
}
|
||||
@ -242,9 +246,10 @@ async fn use_context_update_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<TestComponent>(
|
||||
yew::Renderer::<TestComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::obtain_result;
|
||||
@ -64,12 +66,13 @@ async fn use_effect_destroys_on_component_drop() {
|
||||
|
||||
let destroy_counter = Rc::new(std::cell::RefCell::new(0));
|
||||
let destroy_counter_c = destroy_counter.clone();
|
||||
yew::start_app_with_props_in_element::<UseEffectWrapperComponent>(
|
||||
yew::Renderer::<UseEffectWrapperComponent>::with_root_and_props(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
WrapperProps {
|
||||
destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1),
|
||||
},
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
@ -102,9 +105,10 @@ async fn use_effect_works_many_times() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseEffectComponent>(
|
||||
yew::Renderer::<UseEffectComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
let result = obtain_result();
|
||||
@ -135,9 +139,10 @@ async fn use_effect_works_once() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseEffectComponent>(
|
||||
yew::Renderer::<UseEffectComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
let result = obtain_result();
|
||||
@ -182,9 +187,10 @@ async fn use_effect_refires_on_dependency_change() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseEffectComponent>(
|
||||
yew::Renderer::<UseEffectComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
let result: String = obtain_result();
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
mod common;
|
||||
@ -46,9 +48,10 @@ async fn use_memo_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseMemoComponent>(
|
||||
yew::Renderer::<UseMemoComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::rc::Rc;
|
||||
|
||||
@ -54,9 +56,10 @@ async fn use_reducer_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseReducerComponent>(
|
||||
yew::Renderer::<UseReducerComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
sleep(Duration::ZERO).await;
|
||||
let result = obtain_result();
|
||||
|
||||
@ -113,9 +116,10 @@ async fn use_reducer_eq_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseReducerComponent>(
|
||||
yew::Renderer::<UseReducerComponent>::with_root(
|
||||
document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
let result = obtain_result();
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::obtain_result;
|
||||
@ -28,9 +30,10 @@ async fn use_ref_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseRefComponent>(
|
||||
yew::Renderer::<UseRefComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
let result = obtain_result();
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "wasm_test")]
|
||||
|
||||
mod common;
|
||||
|
||||
use common::obtain_result;
|
||||
@ -25,9 +27,10 @@ async fn use_state_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseComponent>(
|
||||
yew::Renderer::<UseComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
sleep(Duration::ZERO).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "5");
|
||||
@ -67,9 +70,10 @@ async fn multiple_use_state_setters() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseComponent>(
|
||||
yew::Renderer::<UseComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
sleep(Duration::ZERO).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "11");
|
||||
@ -95,9 +99,10 @@ async fn use_state_eq_works() {
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<UseComponent>(
|
||||
yew::Renderer::<UseComponent>::with_root(
|
||||
gloo_utils::document().get_element_by_id("output").unwrap(),
|
||||
);
|
||||
)
|
||||
.render();
|
||||
sleep(Duration::ZERO).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "1");
|
||||
|
||||
@ -16,7 +16,7 @@ js-sys = "0.3"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
weblog = "0.3.0"
|
||||
yew = { path = "../../packages/yew/", features = ["ssr"] }
|
||||
yew = { path = "../../packages/yew/", features = ["ssr", "csr"] }
|
||||
yew-router = { path = "../../packages/yew-router/" }
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
|
||||
|
||||
@ -55,18 +55,31 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
# you can check the latest version here: https://crates.io/crates/yew
|
||||
yew = "0.19"
|
||||
yew = { version = "0.19", features = ["csr"] }
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
You only need feature `csr` if you are building an application.
|
||||
It will enable the `Renderer` and all client-side rendering related code.
|
||||
|
||||
If you are making a library, do not enable this feature as it will pull in
|
||||
client-side rendering logic into the server-side rendering bundle.
|
||||
|
||||
If you need the Renderer for testing or examples, you should enable it
|
||||
in the `dev-dependencies` instead.
|
||||
|
||||
:::
|
||||
|
||||
#### Update main.rs
|
||||
|
||||
We need to generate a template which sets up a root Component called `App` which renders a button
|
||||
that updates its value when clicked. Replace the contents of `src/main.rs` with the following code.
|
||||
|
||||
:::note
|
||||
The call to `yew::start_app::<App>()` inside the `main` function starts your application and mounts
|
||||
The call to `yew::Renderer::<App>::new().render()` inside the `main` function starts your application and mounts
|
||||
it to the page's `<body>` tag. If you would like to start your application with any dynamic
|
||||
properties, you can instead use `yew::start_app_with_props::<App>(..)`.
|
||||
properties, you can instead use `yew::Renderer::<App>::with_props(..).render()`.
|
||||
:::
|
||||
|
||||
```rust ,no_run, title=main.rs
|
||||
@ -92,7 +105,7 @@ fn App() -> Html {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -43,3 +43,9 @@ will be sent to the reducer function in the same order as they are dispatched.
|
||||
The reducer function can see all previous changes at the time they are run.
|
||||
|
||||
:::
|
||||
|
||||
## Yew Renderer
|
||||
|
||||
`start_app*` has been replaced by `yew::Renderer`.
|
||||
|
||||
You need to enable feature `render` to use `yew::Renderer`.
|
||||
|
||||
@ -72,9 +72,22 @@ version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
yew = { git = "https://github.com/yewstack/yew/" }
|
||||
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
You only need feature `csr` if you are building an application.
|
||||
It will enable the `Renderer` and all client-side rendering related code.
|
||||
|
||||
If you are making a library, do not enable this feature as it will pull in
|
||||
client-side rendering logic into the server-side rendering bundle.
|
||||
|
||||
If you need the Renderer for testing or examples, you should enable it
|
||||
in the `dev-dependencies` instead.
|
||||
|
||||
:::
|
||||
|
||||
```rust ,no_run title="src/main.rs"
|
||||
use yew::prelude::*;
|
||||
|
||||
@ -86,7 +99,7 @@ fn app() -> Html {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user