#[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:
Kaede Hoshikawa 2022-03-20 00:48:47 +09:00 committed by GitHub
parent 3ad454011d
commit 8bc2212716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 1462 additions and 1085 deletions

View File

@ -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

View File

@ -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"

View File

@ -1,4 +1,4 @@
fn main() {
wasm_logger::init(wasm_logger::Config::default());
yew::start_app::<agents::App>();
yew::Renderer::<agents::App>::new().render();
}

View File

@ -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]

View File

@ -162,5 +162,5 @@ impl App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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" }

View File

@ -19,5 +19,5 @@ pub fn App() -> Html {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -72,5 +72,5 @@ impl Component for App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -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();
}

View File

@ -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]

View File

@ -124,5 +124,5 @@ impl App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -6,5 +6,5 @@ mod state;
use crate::components::app::App;
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -21,3 +21,6 @@ wasm-logger = "0.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
instant = { version = "0.1" }
[features]
csr = ["yew/csr"]

View File

@ -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>

View File

@ -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();
}

View File

@ -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"

View File

@ -145,5 +145,5 @@ fn app() -> Html {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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]

View File

@ -128,5 +128,5 @@ impl Component for App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -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();
}

View File

@ -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]

View File

@ -26,5 +26,5 @@ impl Component for App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -73,5 +73,5 @@ impl Component for App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -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();
}

View File

@ -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]

View File

@ -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();
}

View File

@ -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"] }

View File

@ -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();
}

View File

@ -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"] }

View File

@ -77,5 +77,5 @@ impl Component for App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"] }

View File

@ -8,5 +8,5 @@ mod password;
use app::App;
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -102,5 +102,5 @@ impl Component for App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -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();
}

View File

@ -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"

View File

@ -56,5 +56,5 @@ fn app() -> Html {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -150,5 +150,5 @@ impl Component for App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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]

View File

@ -245,5 +245,5 @@ impl App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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"

View File

@ -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() {

View File

@ -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"

View File

@ -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();
}

View File

@ -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]

View File

@ -134,5 +134,5 @@ impl App {
}
fn main() {
yew::start_app::<App>();
yew::Renderer::<App>::new().render();
}

View File

@ -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`

View File

@ -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"

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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]

View File

@ -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"]

View File

@ -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
}

View File

@ -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> {

View File

@ -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);

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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))

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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;

View File

@ -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),
}
}
}

View 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
}
}

View 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;

View File

@ -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);

View File

@ -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.

View File

@ -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 {

View File

@ -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]

View File

@ -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;

View 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))
}
}

View File

@ -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);
}
}
}

View File

@ -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));

View File

@ -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(),

View File

@ -0,0 +1 @@
pub mod layout_tests;

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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");

View File

@ -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"] }

View File

@ -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();
}
```

View File

@ -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`.

View File

@ -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();
}
```