",
};
diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs
index c6f8aa658ab..cdfee1f80d5 100644
--- a/packages/yew/src/virtual_dom/vtext.rs
+++ b/packages/yew/src/virtual_dom/vtext.rs
@@ -106,13 +106,15 @@ mod test {
#[test]
fn text_as_root() {
- html! {
+ (html! {
"Text Node As Root"
- };
+ })
+ .unwrap();
- html! {
+ (html! {
{ "Text Node As Root" }
- };
+ })
+ .unwrap();
}
}
@@ -133,13 +135,13 @@ mod layout_tests {
fn diff() {
let layout1 = TestLayout {
name: "1",
- node: html! { "a" },
+ node: html! { "a" }.unwrap(),
expected: "a",
};
let layout2 = TestLayout {
name: "2",
- node: html! { "b" },
+ node: html! { "b" }.unwrap(),
expected: "b",
};
@@ -150,7 +152,8 @@ mod layout_tests {
{"a"}
{"b"}
>
- },
+ }
+ .unwrap(),
expected: "ab",
};
@@ -161,7 +164,8 @@ mod layout_tests {
{"b"}
{"a"}
>
- },
+ }
+ .unwrap(),
expected: "ba",
};
diff --git a/website/docs/concepts/components.md b/website/docs/concepts/components.md
index 579751f2522..025a90bf948 100644
--- a/website/docs/concepts/components.md
+++ b/website/docs/concepts/components.md
@@ -19,8 +19,8 @@ in the lifecycle of a component.
### Create
-When a component is created, it receives properties from its parent component and is stored within
-the `Context` thats passed down to the `create` method. The properties can be used to
+When a component is created, it receives properties from its parent component and is stored within
+the `Context` thats passed down to the `create` method. The properties can be used to
initialize the component's state and the "link" can be used to register callbacks or send messages to the component.
```rust
@@ -195,7 +195,7 @@ impl Component for MyComponent {
html! {
// impl
}
- }
+ }
}
```
@@ -221,7 +221,7 @@ the same component after every render when that update also requests the compone
A simple example can be seen below:
```rust
-use yew::{Context, Component, Html};
+use yew::{Context, Component, Html, HtmlDefault};
struct Comp;
@@ -302,4 +302,3 @@ enum Msg {
All component lifecycle methods take a context object. This object provides a reference to component's scope, which
allows sending messages to a component and the props passed to the component.
-
diff --git a/website/docs/concepts/components/children.md b/website/docs/concepts/components/children.md
index 4bdb93f202d..585e1955b75 100644
--- a/website/docs/concepts/components/children.md
+++ b/website/docs/concepts/components/children.md
@@ -4,7 +4,7 @@ title: "Children"
## General usage
-_Most of the time,_ when allowing a component to have children, you don't care
+_Most of the time,_ when allowing a component to have children, you don't care
what type of children the component has. In such cases, the below example will
suffice.
@@ -99,8 +99,8 @@ for better ergonomics. If you don't want to use it, you can manually implement
```rust
use yew::{
- html, html::ChildrenRenderer, virtual_dom::VChild, Component,
- Context, Html, Properties,
+ html, html::ChildrenRenderer, virtual_dom::VChild, Component,
+ Context, Html, Properties, virtual_dom::VNode
};
pub struct Primary;
@@ -145,8 +145,8 @@ pub enum Item {
// Now, we implement `Into` so that yew knows how to render `Item`.
#[allow(clippy::from_over_into)]
-impl Into for Item {
- fn into(self) -> Html {
+impl Into for Item {
+ fn into(self) -> VNode {
match self {
Self::Primary(child) => child.into(),
Self::Secondary(child) => child.into(),
@@ -181,12 +181,12 @@ impl Component for List {
```
### Optional typed child
-You can also have a single optional child component of a specific type too:
+You can also have a single optional child component of a specific type too:
```rust
use yew::{
- html, html_nested, virtual_dom::VChild, Component,
- Context, Html, Properties
+ html, html_nested, virtual_dom::VChild, Component,
+ Context, Html, Properties, virtual_dom::VNode
};
pub struct PageSideBar;
@@ -225,14 +225,14 @@ impl Component for Page {
fn view(&self, ctx: &Context) -> Html {
html! {
}
}
}
-// The page component can be called either with the sidebar or without:
+// The page component can be called either with the sidebar or without:
pub fn render_page(with_sidebar: bool) -> Html {
if with_sidebar {
diff --git a/website/docs/concepts/html/elements.md b/website/docs/concepts/html/elements.md
index ab0a71e0daf..977604d018b 100644
--- a/website/docs/concepts/html/elements.md
+++ b/website/docs/concepts/html/elements.md
@@ -8,12 +8,13 @@ description: "Both HTML and SVG elements are supported"
There are many reasons why you might want to create or manage DOM nodes manually in Yew, such as
when integrating with JS libraries that can cause conflicts with managed components.
-Using `web-sys`, you can create DOM elements and convert them into a `Node` - which can then be
+Using `web-sys`, you can create DOM elements and convert them into a `Node` - which can then be
used as a `Html` value using `VRef`:
```rust
use web_sys::{Element, Node};
use yew::{Component, Context, html, Html};
+use yew::virtual_dom::VNode;
use gloo_utils::document;
struct Comp;
@@ -34,7 +35,7 @@ impl Component for Comp {
// Convert Element into a Node
let node: Node = div.into();
// Return that Node as a Html value
- Html::VRef(node)
+ Ok(VNode::VRef(node))
}
}
```
@@ -57,9 +58,9 @@ html! {
};
```
-## Boolean Attributes
+## Boolean Attributes
-Some content attributes (e.g checked, hidden, required) are called boolean attributes. In Yew,
+Some content attributes (e.g checked, hidden, required) are called boolean attributes. In Yew,
boolean attributes need to be set to a bool value:
```rust
@@ -77,7 +78,7 @@ This will result in **HTML** that's functionally equivalent to this:
This div is hidden.
```
-Setting a boolean attribute to false is equivalent to not using the attribute at all; values from
+Setting a boolean attribute to false is equivalent to not using the attribute at all; values from
boolean expressions can be used:
```rust
diff --git a/website/docs/concepts/html/lists.md b/website/docs/concepts/html/lists.md
index c28003c7f1a..97da2d9ea7d 100644
--- a/website/docs/concepts/html/lists.md
+++ b/website/docs/concepts/html/lists.md
@@ -11,12 +11,13 @@ list that Yew can display.
```rust
use yew::{html, Html};
+use yew::virtual_dom::VNode;
let items = (1..=10).collect::>();
html! {
};
```
From 0f5f307f82a4a135c1ca47c84abf5ee72c29d03e Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Sat, 27 Nov 2021 23:43:42 +0900
Subject: [PATCH 03/27] Implement Suspense.
---
packages/yew/src/html/component/lifecycle.rs | 43 ++++++-
packages/yew/src/html/component/scope.rs | 2 +-
packages/yew/src/html/error.rs | 4 +-
packages/yew/src/lib.rs | 1 +
packages/yew/src/suspense/component.rs | 90 +++++++++++++
packages/yew/src/suspense/mod.rs | 7 ++
packages/yew/src/suspense/suspension.rs | 102 +++++++++++++++
packages/yew/src/virtual_dom/mod.rs | 4 +
packages/yew/src/virtual_dom/vnode.rs | 22 +++-
packages/yew/src/virtual_dom/vsuspense.rs | 125 +++++++++++++++++++
10 files changed, 392 insertions(+), 8 deletions(-)
create mode 100644 packages/yew/src/suspense/component.rs
create mode 100644 packages/yew/src/suspense/mod.rs
create mode 100644 packages/yew/src/suspense/suspension.rs
create mode 100644 packages/yew/src/virtual_dom/vsuspense.rs
diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs
index 2ca6a4fed01..9a64d660678 100644
--- a/packages/yew/src/html/component/lifecycle.rs
+++ b/packages/yew/src/html/component/lifecycle.rs
@@ -1,7 +1,9 @@
//! Component lifecycle module
-use super::{Component, Scope};
+use super::{AnyScope, Component, Scope};
+use crate::html::RenderError;
use crate::scheduler::{self, Runnable, Shared};
+use crate::suspense::{Suspense, Suspension};
use crate::virtual_dom::{VDiff, VNode};
use crate::{Context, NodeRef};
use std::rc::Rc;
@@ -17,6 +19,8 @@ pub(crate) struct ComponentState {
node_ref: NodeRef,
has_rendered: bool,
+ suspension: Option,
+
// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: u64,
@@ -47,6 +51,7 @@ impl ComponentState {
parent,
next_sibling,
node_ref,
+ suspension: None,
has_rendered: false,
#[cfg(debug_assertions)]
@@ -171,10 +176,38 @@ impl Runnable for RenderRunner {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render");
- // TODO: Error?
- let mut new_root = state.component.view(&state.context).unwrap();
- std::mem::swap(&mut new_root, &mut state.root_node);
- let ancestor = Some(new_root);
+ let old_root = match state.component.view(&state.context) {
+ Ok(m) => {
+ // Currently not suspended, we cancel any previous suspension and update
+ // normally.
+ let mut root = m;
+ std::mem::swap(&mut root, &mut state.root_node);
+
+ if let Some(ref m) = state.suspension {
+ m.resume_by_ref();
+ state.suspension = None;
+ }
+
+ root
+ }
+
+ Err(RenderError::Suspended(m)) => {
+ // Currently suspended, we re-use previous root node and send
+ // suspension to parent element.
+ let comp_scope = AnyScope::from(state.context.scope.clone());
+
+ let suspense_scope = comp_scope.find_parent_scope::().unwrap();
+ let suspense = suspense_scope.get_component().unwrap();
+
+ state.suspension = Some(m.clone());
+
+ suspense.suspend(m);
+
+ state.root_node.clone()
+ }
+ };
+
+ let ancestor = Some(old_root);
let new_root = &mut state.root_node;
let scope = state.context.scope.clone().into();
let next_sibling = state.next_sibling.clone();
diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs
index ef88d11ad1d..93afcf80449 100644
--- a/packages/yew/src/html/component/scope.rs
+++ b/packages/yew/src/html/component/scope.rs
@@ -93,7 +93,7 @@ impl AnyScope {
}
}
- fn find_parent_scope(&self) -> Option> {
+ pub(crate) fn find_parent_scope(&self) -> Option> {
let expected_type_id = TypeId::of::();
iter::successors(Some(self), |scope| scope.get_parent())
.filter(|scope| scope.get_type_id() == &expected_type_id)
diff --git a/packages/yew/src/html/error.rs b/packages/yew/src/html/error.rs
index b3ff9d27203..f96cac9003d 100644
--- a/packages/yew/src/html/error.rs
+++ b/packages/yew/src/html/error.rs
@@ -1,11 +1,13 @@
use thiserror::Error;
+use crate::suspense::Suspension;
+
/// Render Error.
#[derive(Error, Debug, Clone, PartialEq)]
pub enum RenderError {
/// Component Rendering Suspended
#[error("component rendering is suspended.")]
- Suspended,
+ Suspended(#[from] Suspension),
}
/// Render Result.
diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs
index e91a1b7be0d..843f7c90bf7 100644
--- a/packages/yew/src/lib.rs
+++ b/packages/yew/src/lib.rs
@@ -261,6 +261,7 @@ pub mod context;
pub mod functional;
pub mod html;
pub mod scheduler;
+pub mod suspense;
pub mod utils;
pub mod virtual_dom;
diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs
new file mode 100644
index 00000000000..30676f35595
--- /dev/null
+++ b/packages/yew/src/suspense/component.rs
@@ -0,0 +1,90 @@
+use crate::html::{Children, Component, Context, Html, Properties, Scope};
+use crate::virtual_dom::{Key, VList, VNode, VSuspense};
+
+use gloo_utils::document;
+use web_sys::Element;
+
+use super::Suspension;
+
+#[derive(Properties, PartialEq, Debug)]
+pub struct SuspenseProps {
+ #[prop_or_default]
+ children: Children,
+
+ #[prop_or_default]
+ fallback: Children,
+
+ #[prop_or_default]
+ key: Option,
+}
+
+#[derive(Debug)]
+pub enum SuspenseMsg {
+ Suspend(Suspension),
+ Resume(Suspension),
+}
+
+/// Suspend rendering and show a fallback UI until the underlying task completes.
+#[derive(Debug)]
+pub struct Suspense {
+ link: Scope,
+ suspensions: Vec,
+ detached_parent: Element,
+}
+
+impl Component for Suspense {
+ type Properties = SuspenseProps;
+ type Message = SuspenseMsg;
+
+ fn create(ctx: &Context) -> Self {
+ Self {
+ link: ctx.link().clone(),
+ suspensions: Vec::new(),
+ detached_parent: document().create_element("div").unwrap(),
+ }
+ }
+
+ fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool {
+ match msg {
+ Self::Message::Suspend(m) => {
+ if m.resumed() {
+ return false;
+ }
+
+ m.listen(self.link.callback(Self::Message::Resume));
+ }
+ Self::Message::Resume(ref m) => {
+ self.suspensions.retain(|n| m != n);
+ }
+ }
+
+ true
+ }
+
+ fn view(&self, ctx: &Context) -> Html {
+ let children_vnode = VNode::from(VList::with_children(
+ ctx.props().children.clone().into_iter().collect(),
+ None,
+ ));
+ let fallback_vnode = VNode::from(VList::with_children(
+ ctx.props().fallback.clone().into_iter().collect(),
+ None,
+ ));
+
+ let vsuspense = VSuspense::new(
+ children_vnode,
+ fallback_vnode,
+ self.detached_parent.clone(),
+ !self.suspensions.is_empty(),
+ ctx.props().key.clone(),
+ );
+
+ Ok(VNode::from(vsuspense))
+ }
+}
+
+impl Suspense {
+ pub(crate) fn suspend(&self, s: Suspension) {
+ self.link.send_message(SuspenseMsg::Suspend(s));
+ }
+}
diff --git a/packages/yew/src/suspense/mod.rs b/packages/yew/src/suspense/mod.rs
new file mode 100644
index 00000000000..174ec99f8b1
--- /dev/null
+++ b/packages/yew/src/suspense/mod.rs
@@ -0,0 +1,7 @@
+//! This module provides suspense support.
+
+mod component;
+mod suspension;
+
+pub use component::Suspense;
+pub use suspension::{Suspension, SuspensionHandle};
diff --git a/packages/yew/src/suspense/suspension.rs b/packages/yew/src/suspense/suspension.rs
new file mode 100644
index 00000000000..f841cb176d1
--- /dev/null
+++ b/packages/yew/src/suspense/suspension.rs
@@ -0,0 +1,102 @@
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use thiserror::Error;
+
+use crate::Callback;
+
+thread_local! {
+ static SUSPENSION_ID: RefCell = RefCell::default();
+}
+
+/// A Suspension.
+///
+/// This type can be sent back as an `Err(_)` to suspend a component until the underlying task
+/// completes.
+#[derive(Error, Debug, Clone)]
+#[error("suspend component rendering")]
+pub struct Suspension {
+ id: usize,
+ listeners: Rc>>>,
+
+ resumed: Rc,
+}
+
+impl PartialEq for Suspension {
+ fn eq(&self, rhs: &Self) -> bool {
+ self.id == rhs.id
+ }
+}
+
+impl Suspension {
+ /// Creates a Suspension.
+ pub fn new() -> (Self, SuspensionHandle) {
+ let id = SUSPENSION_ID.with(|m| {
+ let mut m = m.borrow_mut();
+ *m += 1;
+
+ *m
+ });
+
+ let self_ = Suspension {
+ id,
+ listeners: Rc::default(),
+ resumed: Rc::default(),
+ };
+
+ (self_.clone(), SuspensionHandle { inner: self_ })
+ }
+
+ pub(crate) fn resumed(&self) -> bool {
+ self.resumed.load(Ordering::Relaxed)
+ }
+
+ pub(crate) fn listen(&self, cb: Callback) {
+ assert!(
+ !self.resumed.load(Ordering::Relaxed),
+ "You are attempting to add a callback after it's resumed."
+ );
+
+ let mut listeners = self.listeners.borrow_mut();
+
+ listeners.push(cb);
+ }
+
+ pub(crate) fn resume_by_ref(&self) {
+ // The component can resume rendering by returning a non-suspended result after a state is
+ // updated, so we always need to check here.
+ if !self.resumed.load(Ordering::Relaxed) {
+ self.resumed.store(true, Ordering::Relaxed);
+ let listeners = self.listeners.borrow();
+
+ for listener in listeners.iter() {
+ listener.emit(self.clone());
+ }
+ }
+ }
+}
+
+/// A Suspension Handle.
+///
+/// This type is used to control the corresponding [`Suspension`].
+///
+/// When the current struct is dropped or `resume` is called, it will resume rendering of current
+/// component.
+#[derive(Debug, PartialEq)]
+pub struct SuspensionHandle {
+ inner: Suspension,
+}
+
+impl SuspensionHandle {
+ /// Resumes component rendering.
+ pub fn resume(self) {
+ self.inner.resume_by_ref();
+ }
+}
+
+impl Drop for SuspensionHandle {
+ fn drop(&mut self) {
+ self.inner.resume_by_ref();
+ }
+}
diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs
index e4ab84f044d..9871deb1d04 100644
--- a/packages/yew/src/virtual_dom/mod.rs
+++ b/packages/yew/src/virtual_dom/mod.rs
@@ -13,6 +13,8 @@ pub mod vnode;
#[doc(hidden)]
pub mod vportal;
#[doc(hidden)]
+pub mod vsuspense;
+#[doc(hidden)]
pub mod vtag;
#[doc(hidden)]
pub mod vtext;
@@ -36,6 +38,8 @@ pub use self::vnode::VNode;
#[doc(inline)]
pub use self::vportal::VPortal;
#[doc(inline)]
+pub use self::vsuspense::VSuspense;
+#[doc(inline)]
pub use self::vtag::VTag;
#[doc(inline)]
pub use self::vtext::VText;
diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs
index c1d19df7c0e..876d01bb034 100644
--- a/packages/yew/src/virtual_dom/vnode.rs
+++ b/packages/yew/src/virtual_dom/vnode.rs
@@ -1,6 +1,6 @@
//! This module contains the implementation of abstract virtual node.
-use super::{Key, VChild, VComp, VDiff, VList, VPortal, VTag, VText};
+use super::{Key, VChild, VComp, VDiff, VList, VPortal, VSuspense, VTag, VText};
use crate::html::{AnyScope, Component, NodeRef};
use gloo::console;
use std::cmp::PartialEq;
@@ -25,6 +25,8 @@ pub enum VNode {
VPortal(VPortal),
/// A holder for any `Node` (necessary for replacing node).
VRef(Node),
+ /// A suspendible document fragment.
+ VSuspense(VSuspense),
}
impl VNode {
@@ -36,6 +38,7 @@ impl VNode {
VNode::VTag(vtag) => vtag.key.clone(),
VNode::VText(_) => None,
VNode::VPortal(vportal) => vportal.node.key(),
+ VNode::VSuspense(vsuspense) => vsuspense.key.clone(),
}
}
@@ -47,6 +50,7 @@ impl VNode {
VNode::VRef(_) | VNode::VText(_) => false,
VNode::VTag(vtag) => vtag.key.is_some(),
VNode::VPortal(vportal) => vportal.node.has_key(),
+ VNode::VSuspense(vsuspense) => vsuspense.key.is_some(),
}
}
@@ -63,6 +67,7 @@ impl VNode {
VNode::VList(vlist) => vlist.get(0).and_then(VNode::first_node),
VNode::VRef(node) => Some(node.clone()),
VNode::VPortal(vportal) => vportal.next_sibling(),
+ VNode::VSuspense(vsuspense) => vsuspense.first_node(),
}
}
@@ -94,6 +99,9 @@ impl VNode {
.unchecked_first_node(),
VNode::VRef(node) => node.clone(),
VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"),
+ VNode::VSuspense(vsuspense) => {
+ vsuspense.first_node().expect("VSuspense is not mounted")
+ }
}
}
@@ -130,6 +138,7 @@ impl VDiff for VNode {
}
}
VNode::VPortal(ref mut vportal) => vportal.detach(parent),
+ VNode::VSuspense(ref mut vsuspense) => vsuspense.detach(parent),
}
}
@@ -166,6 +175,9 @@ impl VDiff for VNode {
VNode::VPortal(ref mut vportal) => {
vportal.apply(parent_scope, parent, next_sibling, ancestor)
}
+ VNode::VSuspense(ref mut vsuspense) => {
+ vsuspense.apply(parent_scope, parent, next_sibling, ancestor)
+ }
}
}
}
@@ -204,6 +216,13 @@ impl From for VNode {
}
}
+impl From for VNode {
+ #[inline]
+ fn from(vsuspense: VSuspense) -> Self {
+ VNode::VSuspense(vsuspense)
+ }
+}
+
impl From> for VNode
where
COMP: Component,
@@ -237,6 +256,7 @@ impl fmt::Debug for VNode {
VNode::VList(ref vlist) => vlist.fmt(f),
VNode::VRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
VNode::VPortal(ref vportal) => vportal.fmt(f),
+ VNode::VSuspense(ref vsuspense) => vsuspense.fmt(f),
}
}
}
diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs
new file mode 100644
index 00000000000..ff476ecd0dc
--- /dev/null
+++ b/packages/yew/src/virtual_dom/vsuspense.rs
@@ -0,0 +1,125 @@
+use super::{Key, VDiff, VNode};
+use crate::html::{AnyScope, NodeRef};
+use web_sys::{Element, Node};
+
+/// This struct represents a suspendable DOM fragment.
+#[derive(Clone, Debug, PartialEq)]
+pub struct VSuspense {
+ /// Child nodes.
+ children: Box,
+
+ /// Fallback nodes when suspended.
+ fallback: Box,
+
+ /// The element to attach to when children is not attached to DOM
+ detached_parent: Element,
+
+ /// Whether the current status is suspended.
+ suspended: bool,
+
+ /// The Key.
+ pub(crate) key: Option,
+}
+
+impl VSuspense {
+ pub(crate) fn new(
+ children: VNode,
+ fallback: VNode,
+ detached_parent: Element,
+ suspended: bool,
+ key: Option,
+ ) -> Self {
+ Self {
+ children: children.into(),
+ fallback: fallback.into(),
+ detached_parent,
+ suspended,
+ key,
+ }
+ }
+
+ pub(crate) fn first_node(&self) -> Option {
+ if self.suspended {
+ self.fallback.first_node()
+ } else {
+ self.children.first_node()
+ }
+ }
+}
+
+impl VDiff for VSuspense {
+ fn detach(&mut self, parent: &Element) {
+ if self.suspended {
+ self.fallback.detach(parent);
+ self.children.detach(&self.detached_parent);
+ } else {
+ self.children.detach(parent);
+ }
+ }
+
+ fn apply(
+ &mut self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ ancestor: Option,
+ ) -> NodeRef {
+ let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor {
+ Some(VNode::VSuspense(mut m)) => {
+ if m.key != self.key {
+ m.detach(parent);
+ } else if self.detached_parent != m.detached_parent {
+ // we delete everything from the outdated parent.
+ m.detached_parent.set_inner_html("");
+ }
+
+ (
+ m.suspended,
+ Some((*m.children).clone()),
+ Some((*m.fallback).clone()),
+ )
+ }
+ Some(mut m) => {
+ m.detach(parent);
+ (false, None, None)
+ }
+ None => (false, None, None),
+ };
+
+ match (self.suspended, already_suspended) {
+ (true, true) => {
+ self.children.apply(
+ parent_scope,
+ &self.detached_parent,
+ NodeRef::default(),
+ children_ancestor,
+ );
+
+ self.fallback
+ .apply(parent_scope, parent, next_sibling, fallback_ancestor)
+ }
+
+ (false, false) => {
+ self.children
+ .apply(parent_scope, parent, next_sibling, children_ancestor)
+ }
+
+ (true, false) => {
+ self.children.apply(
+ parent_scope,
+ &self.detached_parent,
+ NodeRef::default(),
+ children_ancestor,
+ );
+ self.fallback
+ .apply(parent_scope, parent, next_sibling, fallback_ancestor)
+ }
+
+ (false, true) => {
+ self.fallback.detach(parent);
+ self.children
+ .apply(parent_scope, parent, next_sibling, children_ancestor)
+ }
+ }
+ }
+}
From 7f196835e108e16992b4ea1afe467dd11c6ddd1c Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Sun, 28 Nov 2021 00:55:38 +0900
Subject: [PATCH 04/27] Schedule render when suspension is resumed.
---
packages/yew/src/html/component/lifecycle.rs | 46 ++++++++++++++++----
packages/yew/src/virtual_dom/vsuspense.rs | 2 +
2 files changed, 40 insertions(+), 8 deletions(-)
diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs
index 9a64d660678..cd843508627 100644
--- a/packages/yew/src/html/component/lifecycle.rs
+++ b/packages/yew/src/html/component/lifecycle.rs
@@ -5,6 +5,7 @@ use crate::html::RenderError;
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{Suspense, Suspension};
use crate::virtual_dom::{VDiff, VNode};
+use crate::Callback;
use crate::{Context, NodeRef};
use std::rc::Rc;
use web_sys::Element;
@@ -194,14 +195,43 @@ impl Runnable for RenderRunner {
Err(RenderError::Suspended(m)) => {
// Currently suspended, we re-use previous root node and send
// suspension to parent element.
- let comp_scope = AnyScope::from(state.context.scope.clone());
-
- let suspense_scope = comp_scope.find_parent_scope::().unwrap();
- let suspense = suspense_scope.get_component().unwrap();
-
- state.suspension = Some(m.clone());
-
- suspense.suspend(m);
+ 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(),
+ },
+ RenderedRunner {
+ state: shared_state,
+ },
+ );
+ } else {
+ // We schedule a render after current suspension is resumed.
+
+ let comp_scope = AnyScope::from(state.context.scope.clone());
+
+ let suspense_scope = comp_scope.find_parent_scope::().unwrap();
+ let suspense = suspense_scope.get_component().unwrap();
+ m.listen(Callback::once(move |_| {
+ scheduler::push_component_render(
+ shared_state.as_ptr() as usize,
+ RenderRunner {
+ state: shared_state.clone(),
+ },
+ RenderedRunner {
+ state: shared_state,
+ },
+ );
+ }));
+
+ state.suspension = Some(m.clone());
+
+ suspense.suspend(m);
+ }
state.root_node.clone()
}
diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs
index ff476ecd0dc..c0c3bb1635d 100644
--- a/packages/yew/src/virtual_dom/vsuspense.rs
+++ b/packages/yew/src/virtual_dom/vsuspense.rs
@@ -86,6 +86,8 @@ impl VDiff for VSuspense {
None => (false, None, None),
};
+ // When it's suspended, we render children into an element that is detached from the dom
+ // tree while rendering fallback UI into the original place where children resides in.
match (self.suspended, already_suspended) {
(true, true) => {
self.children.apply(
From 31332c67bf42d94ecde37a007bce7c899e13e3e4 Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Sun, 28 Nov 2021 17:57:34 +0900
Subject: [PATCH 05/27] Shift children into a detached node.
---
Cargo.toml | 1 +
examples/suspense/Cargo.toml | 21 ++++++++
examples/suspense/README.md | 10 ++++
examples/suspense/index.html | 11 ++++
examples/suspense/index.scss | 0
examples/suspense/src/main.rs | 55 ++++++++++++++++++++
examples/suspense/src/use_sleep.rs | 43 +++++++++++++++
packages/yew/Cargo.toml | 1 +
packages/yew/src/html/component/lifecycle.rs | 48 ++++++++++++-----
packages/yew/src/html/component/scope.rs | 8 +++
packages/yew/src/lib.rs | 1 +
packages/yew/src/suspense/component.rs | 19 +++++--
packages/yew/src/suspense/mod.rs | 2 +-
packages/yew/src/suspense/suspension.rs | 8 ++-
packages/yew/src/virtual_dom/mod.rs | 6 +++
packages/yew/src/virtual_dom/vcomp.rs | 5 ++
packages/yew/src/virtual_dom/vlist.rs | 10 ++++
packages/yew/src/virtual_dom/vnode.rs | 21 ++++++++
packages/yew/src/virtual_dom/vportal.rs | 4 ++
packages/yew/src/virtual_dom/vsuspense.rs | 44 +++++++++++-----
packages/yew/src/virtual_dom/vtag.rs | 12 +++++
packages/yew/src/virtual_dom/vtext.rs | 12 +++++
22 files changed, 308 insertions(+), 34 deletions(-)
create mode 100644 examples/suspense/Cargo.toml
create mode 100644 examples/suspense/README.md
create mode 100644 examples/suspense/index.html
create mode 100644 examples/suspense/index.scss
create mode 100644 examples/suspense/src/main.rs
create mode 100644 examples/suspense/src/use_sleep.rs
diff --git a/Cargo.toml b/Cargo.toml
index 6b6177b5b9c..7d9485c079f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,7 @@ members = [
"examples/two_apps",
"examples/webgl",
"examples/web_worker_fib",
+ "examples/suspense",
# Release tools
"packages/changelog",
diff --git a/examples/suspense/Cargo.toml b/examples/suspense/Cargo.toml
new file mode 100644
index 00000000000..e6ce038fdda
--- /dev/null
+++ b/examples/suspense/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "suspense"
+version = "0.1.0"
+edition = "2018"
+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" }
+gloo-timers = { version = "0.2.1", features = ["futures"] }
+wasm-bindgen-futures = "0.4"
+wasm-bindgen = "0.2"
+log = "0.4.14"
+console_log = { version = "0.2.0", features = ["color"] }
+
+[dependencies.web-sys]
+version = "0.3"
+features = [
+ "HtmlTextAreaElement",
+]
diff --git a/examples/suspense/README.md b/examples/suspense/README.md
new file mode 100644
index 00000000000..137b848a254
--- /dev/null
+++ b/examples/suspense/README.md
@@ -0,0 +1,10 @@
+# Suspense Example
+
+[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fsuspense)](https://examples.yew.rs/suspense)
+
+This is an example that demonstrates `` support.
+
+## Concepts
+
+This example shows that how `` works in Yew and how you can
+create hooks that utilises suspense.
diff --git a/examples/suspense/index.html b/examples/suspense/index.html
new file mode 100644
index 00000000000..ec8ff43c363
--- /dev/null
+++ b/examples/suspense/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Yew Suspense Demo
+
+
+
+
+
+
diff --git a/examples/suspense/index.scss b/examples/suspense/index.scss
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/examples/suspense/src/main.rs b/examples/suspense/src/main.rs
new file mode 100644
index 00000000000..77f555a6542
--- /dev/null
+++ b/examples/suspense/src/main.rs
@@ -0,0 +1,55 @@
+use web_sys::HtmlTextAreaElement;
+use yew::prelude::*;
+
+use log::Level;
+
+mod use_sleep;
+
+use use_sleep::use_sleep;
+
+#[function_component(PleaseWait)]
+fn please_wait() -> Html {
+ html! {
{"Please wait..."}
}
+}
+
+#[function_component(AppContent)]
+fn app_content() -> Html {
+ let resleep = use_sleep()?;
+
+ let value = use_state(|| "".to_string());
+
+ let on_text_input = {
+ let value = value.clone();
+
+ Callback::from(move |e: InputEvent| {
+ let input: HtmlTextAreaElement = e.target_unchecked_into();
+
+ value.set(input.value());
+ })
+ };
+
+ let on_take_a_break = Callback::from(move |_| (resleep.clone())());
+
+ html! {
+ <>
+
+
+ >
+ }
+}
+
+#[function_component(App)]
+fn app() -> Html {
+ let fallback = html! {};
+
+ html! {
+
+
+
+ }
+}
+
+fn main() {
+ console_log::init_with_level(Level::Trace).expect("Failed to initialise Log!");
+ yew::start_app::();
+}
diff --git a/examples/suspense/src/use_sleep.rs b/examples/suspense/src/use_sleep.rs
new file mode 100644
index 00000000000..5ef99755f42
--- /dev/null
+++ b/examples/suspense/src/use_sleep.rs
@@ -0,0 +1,43 @@
+use std::rc::Rc;
+
+use gloo_timers::future::TimeoutFuture;
+use wasm_bindgen_futures::spawn_local;
+use yew::prelude::*;
+use yew::suspense::{Suspension, SuspensionResult};
+
+#[derive(PartialEq)]
+pub struct SleepState {
+ s: Suspension,
+}
+
+impl SleepState {
+ fn new() -> Self {
+ let (s, handle) = Suspension::new();
+
+ spawn_local(async move {
+ TimeoutFuture::new(5_000).await;
+
+ handle.resume();
+ });
+
+ Self { s }
+ }
+}
+
+impl Reducible for SleepState {
+ type Action = ();
+
+ fn reduce(self: Rc, _action: Self::Action) -> Rc {
+ Self::new().into()
+ }
+}
+
+pub fn use_sleep() -> SuspensionResult> {
+ let sleep_state = use_reducer(SleepState::new);
+
+ if sleep_state.s.resumed() {
+ Ok(Rc::new(move || sleep_state.dispatch(())))
+ } else {
+ Err(sleep_state.s.clone())
+ }
+}
diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml
index d9de79f8f78..2ace6221ae9 100644
--- a/packages/yew/Cargo.toml
+++ b/packages/yew/Cargo.toml
@@ -26,6 +26,7 @@ wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
yew-macro = { version = "^0.18.0", path = "../yew-macro" }
thiserror = "1.0"
+log = "0.4"
scoped-tls-hkt = "0.1"
diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs
index cd843508627..4ae86a5bb90 100644
--- a/packages/yew/src/html/component/lifecycle.rs
+++ b/packages/yew/src/html/component/lifecycle.rs
@@ -96,6 +96,8 @@ pub(crate) enum UpdateEvent {
MessageBatch(Vec),
/// Wraps properties, node ref, and next sibling for a component.
Properties(Rc, NodeRef, NodeRef),
+ /// Shift Scope.
+ Shift(Element, NodeRef),
}
pub(crate) struct UpdateRunner {
@@ -126,6 +128,16 @@ impl Runnable for UpdateRunner {
false
}
}
+ UpdateEvent::Shift(parent, next_sibling) => {
+ state
+ .root_node
+ .shift(&state.parent, &parent, next_sibling.clone());
+
+ state.parent = parent;
+ state.next_sibling = next_sibling;
+
+ false
+ }
};
#[cfg(debug_assertions)]
@@ -177,19 +189,29 @@ impl Runnable for RenderRunner {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render");
- let old_root = match state.component.view(&state.context) {
+ match state.component.view(&state.context) {
Ok(m) => {
- // Currently not suspended, we cancel any previous suspension and update
+ // Currently not suspended, we remove any previous suspension and update
// normally.
let mut root = m;
std::mem::swap(&mut root, &mut state.root_node);
if let Some(ref m) = state.suspension {
- m.resume_by_ref();
- state.suspension = None;
+ let comp_scope = AnyScope::from(state.context.scope.clone());
+
+ let suspense_scope = comp_scope.find_parent_scope::().unwrap();
+ let suspense = suspense_scope.get_component().unwrap();
+
+ suspense.resume(m.clone());
}
- root
+ let ancestor = Some(root);
+ let new_root = &mut state.root_node;
+ let scope = state.context.scope.clone().into();
+ let next_sibling = state.next_sibling.clone();
+
+ let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
+ state.node_ref.link(node);
}
Err(RenderError::Suspended(m)) => {
@@ -216,6 +238,7 @@ impl Runnable for RenderRunner {
let suspense_scope = comp_scope.find_parent_scope::().unwrap();
let suspense = suspense_scope.get_component().unwrap();
+
m.listen(Callback::once(move |_| {
scheduler::push_component_render(
shared_state.as_ptr() as usize,
@@ -228,21 +251,18 @@ impl Runnable for RenderRunner {
);
}));
+ 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);
}
-
- state.root_node.clone()
}
};
-
- let ancestor = Some(old_root);
- let new_root = &mut state.root_node;
- let scope = state.context.scope.clone().into();
- let next_sibling = state.next_sibling.clone();
- let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
- state.node_ref.link(node);
}
}
}
diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs
index 93afcf80449..6d5aaeed50d 100644
--- a/packages/yew/src/html/component/scope.rs
+++ b/packages/yew/src/html/component/scope.rs
@@ -119,6 +119,7 @@ pub(crate) trait Scoped {
fn to_any(&self) -> AnyScope;
fn root_vnode(&self) -> Option>;
fn destroy(&mut self);
+ fn shift_node(&self, parent: Element, next_sibling: NodeRef);
}
impl Scoped for Scope {
@@ -145,6 +146,13 @@ impl Scoped for Scope {
// Not guaranteed to already have the scheduler started
scheduler::start();
}
+
+ fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
+ scheduler::push_component_update(UpdateRunner {
+ state: self.state.clone(),
+ event: UpdateEvent::Shift(parent, next_sibling),
+ });
+ }
}
/// A context which allows sending messages to a component.
diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs
index 843f7c90bf7..1ce2fb11eda 100644
--- a/packages/yew/src/lib.rs
+++ b/packages/yew/src/lib.rs
@@ -391,6 +391,7 @@ pub mod prelude {
NodeRef, Properties,
};
pub use crate::macros::{classes, html, html_nested};
+ pub use crate::suspense::Suspense;
pub use crate::functional::*;
}
diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs
index 30676f35595..7fbeda4ec82 100644
--- a/packages/yew/src/suspense/component.rs
+++ b/packages/yew/src/suspense/component.rs
@@ -9,13 +9,13 @@ use super::Suspension;
#[derive(Properties, PartialEq, Debug)]
pub struct SuspenseProps {
#[prop_or_default]
- children: Children,
+ pub children: Children,
#[prop_or_default]
- fallback: Children,
+ pub fallback: Children,
#[prop_or_default]
- key: Option,
+ pub key: Option,
}
#[derive(Debug)]
@@ -52,13 +52,18 @@ impl Component for Suspense {
}
m.listen(self.link.callback(Self::Message::Resume));
+
+ self.suspensions.push(m);
+
+ true
}
Self::Message::Resume(ref m) => {
+ let suspensions_len = self.suspensions.len();
self.suspensions.retain(|n| m != n);
+
+ suspensions_len != self.suspensions.len()
}
}
-
- true
}
fn view(&self, ctx: &Context) -> Html {
@@ -87,4 +92,8 @@ impl Suspense {
pub(crate) fn suspend(&self, s: Suspension) {
self.link.send_message(SuspenseMsg::Suspend(s));
}
+
+ pub(crate) fn resume(&self, s: Suspension) {
+ self.link.send_message(SuspenseMsg::Resume(s));
+ }
}
diff --git a/packages/yew/src/suspense/mod.rs b/packages/yew/src/suspense/mod.rs
index 174ec99f8b1..617c263775f 100644
--- a/packages/yew/src/suspense/mod.rs
+++ b/packages/yew/src/suspense/mod.rs
@@ -4,4 +4,4 @@ mod component;
mod suspension;
pub use component::Suspense;
-pub use suspension::{Suspension, SuspensionHandle};
+pub use suspension::{Suspension, SuspensionHandle, SuspensionResult};
diff --git a/packages/yew/src/suspense/suspension.rs b/packages/yew/src/suspense/suspension.rs
index f841cb176d1..f0d58f24a9e 100644
--- a/packages/yew/src/suspense/suspension.rs
+++ b/packages/yew/src/suspense/suspension.rs
@@ -48,7 +48,8 @@ impl Suspension {
(self_.clone(), SuspensionHandle { inner: self_ })
}
- pub(crate) fn resumed(&self) -> bool {
+ /// Returns `true` if the current suspension is already resumed.
+ pub fn resumed(&self) -> bool {
self.resumed.load(Ordering::Relaxed)
}
@@ -63,7 +64,7 @@ impl Suspension {
listeners.push(cb);
}
- pub(crate) fn resume_by_ref(&self) {
+ fn resume_by_ref(&self) {
// The component can resume rendering by returning a non-suspended result after a state is
// updated, so we always need to check here.
if !self.resumed.load(Ordering::Relaxed) {
@@ -77,6 +78,9 @@ impl Suspension {
}
}
+/// A Suspension Result.
+pub type SuspensionResult = std::result::Result;
+
/// A Suspension Handle.
///
/// This type is used to control the corresponding [`Suspension`].
diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs
index 9871deb1d04..6ada48557dc 100644
--- a/packages/yew/src/virtual_dom/mod.rs
+++ b/packages/yew/src/virtual_dom/mod.rs
@@ -499,6 +499,12 @@ pub(crate) trait VDiff {
/// Remove self from parent.
fn detach(&mut self, parent: &Element);
+ /// Move elements from one parent to another parent.
+ /// This is currently only used by `VSuspense` to preserve component state without detaching
+ /// (which destroys component state).
+ /// Prefer `detach` then apply if possible.
+ fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef);
+
/// Scoped diff apply to other tree.
///
/// Virtual rendering for the node. It uses parent node and existing
diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs
index 06237292fa0..a51305a2f29 100644
--- a/packages/yew/src/virtual_dom/vcomp.rs
+++ b/packages/yew/src/virtual_dom/vcomp.rs
@@ -225,6 +225,11 @@ impl VDiff for VComp {
self.take_scope().destroy();
}
+ fn shift(&self, _previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
+ let scope = self.scope.as_ref().unwrap();
+ scope.shift_node(next_parent.clone(), next_sibling);
+ }
+
fn apply(
&mut self,
parent_scope: &AnyScope,
diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs
index e02e9443902..585e4c98f43 100644
--- a/packages/yew/src/virtual_dom/vlist.rs
+++ b/packages/yew/src/virtual_dom/vlist.rs
@@ -313,6 +313,16 @@ impl VDiff for VList {
}
}
+ fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
+ let mut last_node_ref = next_sibling;
+
+ for node in self.children.iter().rev() {
+ node.shift(previous_parent, next_parent, last_node_ref);
+ last_node_ref = NodeRef::default();
+ last_node_ref.set(node.first_node());
+ }
+ }
+
fn apply(
&mut self,
parent_scope: &AnyScope,
diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs
index 876d01bb034..6bd7041d536 100644
--- a/packages/yew/src/virtual_dom/vnode.rs
+++ b/packages/yew/src/virtual_dom/vnode.rs
@@ -142,6 +142,27 @@ impl VDiff for VNode {
}
}
+ fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
+ match *self {
+ VNode::VTag(ref vtag) => vtag.shift(previous_parent, next_parent, next_sibling),
+ VNode::VText(ref vtext) => vtext.shift(previous_parent, next_parent, next_sibling),
+ VNode::VComp(ref vcomp) => vcomp.shift(previous_parent, next_parent, next_sibling),
+ VNode::VList(ref vlist) => vlist.shift(previous_parent, next_parent, next_sibling),
+ VNode::VRef(ref node) => {
+ previous_parent.remove_child(node).unwrap();
+ next_parent
+ .insert_before(node, next_sibling.get().as_ref())
+ .unwrap();
+ }
+ VNode::VPortal(ref vportal) => {
+ vportal.shift(previous_parent, next_parent, next_sibling)
+ }
+ VNode::VSuspense(ref vsuspense) => {
+ vsuspense.shift(previous_parent, next_parent, next_sibling)
+ }
+ }
+ }
+
fn apply(
&mut self,
parent_scope: &AnyScope,
diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs
index ba3428e32ab..f99a3b240ed 100644
--- a/packages/yew/src/virtual_dom/vportal.rs
+++ b/packages/yew/src/virtual_dom/vportal.rs
@@ -22,6 +22,10 @@ impl VDiff for VPortal {
self.sibling_ref.set(None);
}
+ fn shift(&self, _previous_parent: &Element, _next_parent: &Element, _next_sibling: NodeRef) {
+ // portals have nothing in it's original place of DOM, we also do nothing.
+ }
+
fn apply(
&mut self,
parent_scope: &AnyScope,
diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs
index c0c3bb1635d..5cf6d177510 100644
--- a/packages/yew/src/virtual_dom/vsuspense.rs
+++ b/packages/yew/src/virtual_dom/vsuspense.rs
@@ -57,6 +57,16 @@ impl VDiff for VSuspense {
}
}
+ fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
+ if self.suspended {
+ self.fallback
+ .shift(previous_parent, next_parent, next_sibling);
+ } else {
+ self.children
+ .shift(previous_parent, next_parent, next_sibling);
+ }
+ }
+
fn apply(
&mut self,
parent_scope: &AnyScope,
@@ -66,18 +76,14 @@ impl VDiff for VSuspense {
) -> NodeRef {
let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor {
Some(VNode::VSuspense(mut m)) => {
- if m.key != self.key {
+ // We only preserve the child state if they are the same suspense.
+ if m.key != self.key || self.detached_parent != m.detached_parent {
m.detach(parent);
- } else if self.detached_parent != m.detached_parent {
- // we delete everything from the outdated parent.
- m.detached_parent.set_inner_html("");
- }
- (
- m.suspended,
- Some((*m.children).clone()),
- Some((*m.fallback).clone()),
- )
+ (false, None, None)
+ } else {
+ (m.suspended, Some(*m.children), Some(*m.fallback))
+ }
}
Some(mut m) => {
m.detach(parent);
@@ -107,18 +113,32 @@ impl VDiff for VSuspense {
}
(true, false) => {
+ children_ancestor.as_ref().unwrap().shift(
+ parent,
+ &self.detached_parent,
+ NodeRef::default(),
+ );
+
self.children.apply(
parent_scope,
&self.detached_parent,
NodeRef::default(),
children_ancestor,
);
+
+ // first render of fallback, ancestor needs to be None.
self.fallback
- .apply(parent_scope, parent, next_sibling, fallback_ancestor)
+ .apply(parent_scope, parent, next_sibling, None)
}
(false, true) => {
- self.fallback.detach(parent);
+ fallback_ancestor.unwrap().detach(parent);
+
+ children_ancestor.as_ref().unwrap().shift(
+ &self.detached_parent,
+ parent,
+ next_sibling.clone(),
+ );
self.children
.apply(parent_scope, parent, next_sibling, children_ancestor)
}
diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs
index 40e5900fc4e..6094b83a200 100644
--- a/packages/yew/src/virtual_dom/vtag.rs
+++ b/packages/yew/src/virtual_dom/vtag.rs
@@ -479,6 +479,18 @@ impl VDiff for VTag {
self.node_ref.set(None);
}
+ fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
+ let node = self
+ .reference
+ .as_ref()
+ .expect("tried to shift not rendered VTag from DOM");
+
+ previous_parent.remove_child(node).unwrap();
+ next_parent
+ .insert_before(node, next_sibling.get().as_ref())
+ .unwrap();
+ }
+
/// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag]
/// to compute what to patch in the actual DOM nodes.
fn apply(
diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs
index cdfee1f80d5..c9ba9768a06 100644
--- a/packages/yew/src/virtual_dom/vtext.rs
+++ b/packages/yew/src/virtual_dom/vtext.rs
@@ -54,6 +54,18 @@ impl VDiff for VText {
}
}
+ fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
+ let node = self
+ .reference
+ .as_ref()
+ .expect("tried to shift not rendered VTag from DOM");
+
+ previous_parent.remove_child(node).unwrap();
+ next_parent
+ .insert_before(node, next_sibling.get().as_ref())
+ .unwrap();
+ }
+
/// Renders virtual node over existing `TextNode`, but only if value of text has changed.
fn apply(
&mut self,
From 9bef4de32a95acacd85bdb50a394aade4437f1ed Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Sun, 28 Nov 2021 18:36:52 +0900
Subject: [PATCH 06/27] styled example.
---
examples/suspense/index.scss | 62 +++++++++++++++++++++++++++++++++++
examples/suspense/src/main.rs | 25 +++++++++-----
packages/yew/Cargo.toml | 1 -
3 files changed, 79 insertions(+), 9 deletions(-)
diff --git a/examples/suspense/index.scss b/examples/suspense/index.scss
index e69de29bb2d..127178778be 100644
--- a/examples/suspense/index.scss
+++ b/examples/suspense/index.scss
@@ -0,0 +1,62 @@
+html, body {
+ font-family: sans-serif;
+
+ margin: 0;
+ padding: 0;
+
+ background-color: rgb(237, 244, 255);
+}
+
+.layout {
+ height: 100vh;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.content {
+ height: 600px;
+ width: 600px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ border-radius: 4px;
+ box-shadow: 0 0 5px 0 black;
+
+ background: white;
+}
+
+.content-area {
+ width: 350px;
+ height: 400px;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+textarea {
+ width: 300px;
+ height: 300px;
+ font-size: 15px;
+}
+
+.action-area {
+ padding-top: 20px;
+ padding-bottom: 20px;
+}
+
+button {
+ color: white;
+ height: 50px;
+ width: 300px;
+ font-size: 20px;
+ background-color: rgb(88, 164, 255);
+ border-radius: 5px;
+ border: none;
+}
diff --git a/examples/suspense/src/main.rs b/examples/suspense/src/main.rs
index 77f555a6542..3bdb829333e 100644
--- a/examples/suspense/src/main.rs
+++ b/examples/suspense/src/main.rs
@@ -9,14 +9,16 @@ use use_sleep::use_sleep;
#[function_component(PleaseWait)]
fn please_wait() -> Html {
- html! {
{"Please wait..."}
}
+ html! {
{"Please wait 5 Seconds..."}
}
}
#[function_component(AppContent)]
fn app_content() -> Html {
let resleep = use_sleep()?;
- let value = use_state(|| "".to_string());
+ let value = use_state(|| {
+ "I am writing a long story...\n\nYou can take a break at anytime!".to_string()
+ });
let on_text_input = {
let value = value.clone();
@@ -31,10 +33,12 @@ fn app_content() -> Html {
let on_take_a_break = Callback::from(move |_| (resleep.clone())());
html! {
- <>
+
"#
+ );
+
+ TimeoutFuture::new(50).await;
+
+ let result = obtain_result();
+ assert_eq!(
+ result.as_str(),
+ r#"
"#
+ );
+}
From 2e7f0bf875307bbd0881422bf778d97090b61991 Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Tue, 30 Nov 2021 22:10:07 +0900
Subject: [PATCH 10/27] Fix clippy.
---
packages/yew/tests/suspense.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs
index ae5ddcd97e1..be3d6b834aa 100644
--- a/packages/yew/tests/suspense.rs
+++ b/packages/yew/tests/suspense.rs
@@ -192,7 +192,7 @@ async fn suspense_not_suspended_at_start() {
if s.resumed() {
Ok(Rc::new(move || sleep_state.dispatch(())))
} else {
- Err(s.clone())
+ Err(s)
}
}
From 77ab06aa0c8a45171c5c5be30c485cc33749e840 Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Tue, 30 Nov 2021 22:55:01 +0900
Subject: [PATCH 11/27] Add docs.
---
website/docs/concepts/suspense.md | 84 +++++++++++++++++++++++++++++++
1 file changed, 84 insertions(+)
create mode 100644 website/docs/concepts/suspense.md
diff --git a/website/docs/concepts/suspense.md b/website/docs/concepts/suspense.md
new file mode 100644
index 00000000000..8a324384fa7
--- /dev/null
+++ b/website/docs/concepts/suspense.md
@@ -0,0 +1,84 @@
+---
+title: "Suspense"
+description: "Suspense for data fetching"
+---
+
+Suspense is a way to suspend component rendering whilst waiting a task
+to complete and a fallback (placeholder) UI is shown in the meanwhile.
+
+It can be used to fetch data from server, wait for tasks to be completed
+by an agent, or other background asynchronous task.
+
+Before suspense, data fetching usually happens after (Fetch-on-render) or before
+component rendering (Fetch-then-render).
+
+### Render-as-You-Fetch
+
+Suspense enables a new approach that allows components to initiate data request
+during the rendering process. When a component initiates a data request,
+the rendering process will become suspended and a fallback UI will be
+shown until the request is completed.
+
+The recommended way to use suspense is with hooks.
+
+```rust, ignore
+use yew::prelude::*;
+
+#[function_component(Content)]
+fn content() -> Html {
+ let user = use_user()?;
+
+ html! {
{"Hello, "}{&user.name}
}
+}
+
+#[function_component(App)]
+fn app() -> Html {
+ let fallback = html! {
{"Loading..."}
};
+
+ html! {
+
+
+
+ }
+}
+```
+
+In the above example, the `use_user` hook will suspend the component
+rendering while the user is loading and a `Loading...` placeholder will
+be shown until `user` is loaded.
+
+To define a hook that suspends a component rendering, it needs to return
+a `SuspensionResult`. When the component needs to be suspended, the
+hook should return a `Err(Suspension)` and users should unwrap it with
+`?` in which it will be converted into `Html`.
+
+```rust, ignore
+use yew::prelude::*;
+use yew::suspense::Suspension;
+
+struct User {
+ name: String,
+}
+
+fn use_user() -> SuspensionResult {
+ match load_user() {
+ // If a user is loaded, then we return it as Ok(user).
+ Some(m) => Ok(m),
+ None => {
+ // When user is still loading, then we create a `Suspension`
+ // and call `SuspensionHandle::resume` when data loading
+ // completes, the component will be re-rendered
+ // automatically.
+ let (s, handle) = Suspension::new();
+ on_load_user_complete(move || {handle.resume();});
+ Err(s)
+ },
+ }
+}
+```
+
+### Use Suspense in Struct Components
+
+Whilst it's possible to suspend a struct component, it's behaviour is
+not well defined and not recommended. You should consider using function
+components instead when using ``.
From 9afeef0966c5592841d5798ee1289237e0058a6f Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Tue, 30 Nov 2021 22:57:21 +0900
Subject: [PATCH 12/27] Add to sidebar.
---
website/sidebars.js | 217 ++++++++++++++++++++++----------------------
1 file changed, 109 insertions(+), 108 deletions(-)
diff --git a/website/sidebars.js b/website/sidebars.js
index 17078dd6f22..0f5f8f5df58 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -13,122 +13,123 @@ module.exports = {
// By default, Docusaurus generates a sidebar from the docs folder structure
// conceptsSidebar: [{type: 'autogenerated', dirName: '.'}],
- // But you can create a sidebar manually
- sidebar: [
+ // But you can create a sidebar manually
+ sidebar: [
+ {
+ type: "category",
+ label: "Getting Started",
+ items: [
{
- type: 'category',
- label: 'Getting Started',
- items: [
- {
- type: 'category',
- label: 'Project Setup',
- items: [
- 'getting-started/project-setup/introduction',
- 'getting-started/project-setup/using-trunk',
- 'getting-started/project-setup/using-wasm-pack',
- ]
- },
- "getting-started/build-a-sample-app",
- "getting-started/examples",
- "getting-started/starter-templates",
- ],
+ type: "category",
+ label: "Project Setup",
+ items: [
+ "getting-started/project-setup/introduction",
+ "getting-started/project-setup/using-trunk",
+ "getting-started/project-setup/using-wasm-pack",
+ ],
},
+ "getting-started/build-a-sample-app",
+ "getting-started/examples",
+ "getting-started/starter-templates",
+ ],
+ },
+ {
+ type: "category",
+ label: "Concepts",
+ items: [
{
- type: "category",
- label: "Concepts",
- items: [
- {
- type: "category",
- label: "wasm-bindgen",
- items: [
- "concepts/wasm-bindgen/introduction",
- "concepts/wasm-bindgen/web-sys",
- ]
- },
- {
- type: "category",
- label: "Components",
- items: [
- "concepts/components/introduction",
- "concepts/components/callbacks",
- "concepts/components/scope",
- "concepts/components/properties",
- "concepts/components/children",
- "concepts/components/refs"
- ],
- },
- {
- type: "category",
- label: "HTML",
- items: [
- "concepts/html/introduction",
- "concepts/html/components",
- "concepts/html/elements",
- "concepts/html/events",
- "concepts/html/classes",
- "concepts/html/fragments",
- "concepts/html/lists",
- "concepts/html/literals-and-expressions"
- ]
- },
- {
- type: "category",
- label: "Function Components",
- items: [
- "concepts/function-components/introduction",
- "concepts/function-components/attribute",
- "concepts/function-components/pre-defined-hooks",
- "concepts/function-components/custom-hooks",
- ]
- },
- "concepts/agents",
- "concepts/contexts",
- "concepts/router",
- ]
+ type: "category",
+ label: "wasm-bindgen",
+ items: [
+ "concepts/wasm-bindgen/introduction",
+ "concepts/wasm-bindgen/web-sys",
+ ],
},
{
- type: 'category',
- label: 'Advanced topics',
- items: [
- "advanced-topics/how-it-works",
- "advanced-topics/optimizations",
- "advanced-topics/portals",
- ]
+ type: "category",
+ label: "Components",
+ items: [
+ "concepts/components/introduction",
+ "concepts/components/callbacks",
+ "concepts/components/scope",
+ "concepts/components/properties",
+ "concepts/components/children",
+ "concepts/components/refs",
+ ],
},
{
- type: 'category',
- label: 'More',
- items: [
- "more/debugging",
- "more/development-tips",
- "more/external-libs",
- "more/css",
- "more/testing",
- "more/roadmap",
- "more/wasm-build-tools"
- ]
+ type: "category",
+ label: "HTML",
+ items: [
+ "concepts/html/introduction",
+ "concepts/html/components",
+ "concepts/html/elements",
+ "concepts/html/events",
+ "concepts/html/classes",
+ "concepts/html/fragments",
+ "concepts/html/lists",
+ "concepts/html/literals-and-expressions",
+ ],
},
{
- type: "category",
- label: "Migration guides",
- items: [
- {
- type: "category",
- label: "yew",
- items: ["migration-guides/yew/from-0_18_0-to-0_19_0"],
- },
- {
- type: "category",
- label: "yew-agent",
- items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"],
- },
- {
- type: "category",
- label: "yew-router",
- items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"],
- },
- ],
+ type: "category",
+ label: "Function Components",
+ items: [
+ "concepts/function-components/introduction",
+ "concepts/function-components/attribute",
+ "concepts/function-components/pre-defined-hooks",
+ "concepts/function-components/custom-hooks",
+ ],
},
- "tutorial"
- ],
+ "concepts/agents",
+ "concepts/contexts",
+ "concepts/router",
+ "concepts/suspense",
+ ],
+ },
+ {
+ type: "category",
+ label: "Advanced topics",
+ items: [
+ "advanced-topics/how-it-works",
+ "advanced-topics/optimizations",
+ "advanced-topics/portals",
+ ],
+ },
+ {
+ type: "category",
+ label: "More",
+ items: [
+ "more/debugging",
+ "more/development-tips",
+ "more/external-libs",
+ "more/css",
+ "more/testing",
+ "more/roadmap",
+ "more/wasm-build-tools",
+ ],
+ },
+ {
+ type: "category",
+ label: "Migration guides",
+ items: [
+ {
+ type: "category",
+ label: "yew",
+ items: ["migration-guides/yew/from-0_18_0-to-0_19_0"],
+ },
+ {
+ type: "category",
+ label: "yew-agent",
+ items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"],
+ },
+ {
+ type: "category",
+ label: "yew-router",
+ items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"],
+ },
+ ],
+ },
+ "tutorial",
+ ],
};
From 4d58a14258bfdc45959624bd2656a6b062577489 Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Tue, 30 Nov 2021 23:08:25 +0900
Subject: [PATCH 13/27] Fix syntax highlight.
---
website/docs/concepts/suspense.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/website/docs/concepts/suspense.md b/website/docs/concepts/suspense.md
index 8a324384fa7..410ff0271e6 100644
--- a/website/docs/concepts/suspense.md
+++ b/website/docs/concepts/suspense.md
@@ -7,7 +7,7 @@ Suspense is a way to suspend component rendering whilst waiting a task
to complete and a fallback (placeholder) UI is shown in the meanwhile.
It can be used to fetch data from server, wait for tasks to be completed
-by an agent, or other background asynchronous task.
+by an agent, or perform other background asynchronous task.
Before suspense, data fetching usually happens after (Fetch-on-render) or before
component rendering (Fetch-then-render).
@@ -21,7 +21,7 @@ shown until the request is completed.
The recommended way to use suspense is with hooks.
-```rust, ignore
+```rust ,ignore
use yew::prelude::*;
#[function_component(Content)]
@@ -44,7 +44,7 @@ fn app() -> Html {
```
In the above example, the `use_user` hook will suspend the component
-rendering while the user is loading and a `Loading...` placeholder will
+rendering while user information is loading and a `Loading...` placeholder will
be shown until `user` is loaded.
To define a hook that suspends a component rendering, it needs to return
@@ -52,9 +52,9 @@ a `SuspensionResult`. When the component needs to be suspended, the
hook should return a `Err(Suspension)` and users should unwrap it with
`?` in which it will be converted into `Html`.
-```rust, ignore
+```rust ,ignore
use yew::prelude::*;
-use yew::suspense::Suspension;
+use yew::suspense::{Suspension, SuspensionResult};
struct User {
name: String,
@@ -80,5 +80,5 @@ fn use_user() -> SuspensionResult {
### Use Suspense in Struct Components
Whilst it's possible to suspend a struct component, it's behaviour is
-not well defined and not recommended. You should consider using function
+not well-defined and not recommended. You should consider using function
components instead when using ``.
From 8bcacf6a2f9cab5d002978d61700ec47885e3fe7 Mon Sep 17 00:00:00 2001
From: Kaede Hoshikawa
Date: Mon, 20 Dec 2021 20:00:38 +0900
Subject: [PATCH 14/27] Component -> BaseComponent.
---
.../yew-macro/src/html_tree/html_component.rs | 2 +-
packages/yew/src/app_handle.rs | 8 +--
packages/yew/src/functional/mod.rs | 11 +++-
packages/yew/src/html/component/lifecycle.rs | 28 ++++----
packages/yew/src/html/component/mod.rs | 66 ++++++++++++++++++-
packages/yew/src/html/component/scope.rs | 24 +++----
packages/yew/src/lib.rs | 14 ++--
packages/yew/src/virtual_dom/vcomp.rs | 22 +++----
packages/yew/src/virtual_dom/vnode.rs | 4 +-
9 files changed, 124 insertions(+), 55 deletions(-)
diff --git a/packages/yew-macro/src/html_tree/html_component.rs b/packages/yew-macro/src/html_tree/html_component.rs
index 1714d542fb1..53a97236fdf 100644
--- a/packages/yew-macro/src/html_tree/html_component.rs
+++ b/packages/yew-macro/src/html_tree/html_component.rs
@@ -92,7 +92,7 @@ impl ToTokens for HtmlComponent {
children,
} = self;
- let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::Component>::Properties);
+ let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::BaseComponent>::Properties);
let children_renderer = if children.is_empty() {
None
} else {
diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs
index 8ac8cfcc8f8..04e3b79afd7 100644
--- a/packages/yew/src/app_handle.rs
+++ b/packages/yew/src/app_handle.rs
@@ -3,21 +3,21 @@
use std::ops::Deref;
-use crate::html::{Component, NodeRef, Scope, Scoped};
+use crate::html::{BaseComponent, NodeRef, Scope, Scoped};
use gloo_utils::document;
use std::rc::Rc;
use web_sys::Element;
/// An instance of an application.
#[derive(Debug)]
-pub struct AppHandle {
+pub struct AppHandle {
/// `Scope` holder
pub(crate) scope: Scope,
}
impl AppHandle
where
- COMP: Component,
+ COMP: BaseComponent,
{
/// The main entry point of a Yew program which also allows passing properties. It works
/// similarly to the `program` function in Elm. You should provide an initial model, `update`
@@ -56,7 +56,7 @@ where
impl Deref for AppHandle
where
- COMP: Component,
+ COMP: BaseComponent,
{
type Target = Scope;
diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs
index aa3df703725..37825d87685 100644
--- a/packages/yew/src/functional/mod.rs
+++ b/packages/yew/src/functional/mod.rs
@@ -13,8 +13,8 @@
//!
//! More details about function components and Hooks can be found on [Yew Docs](https://yew.rs/docs/next/concepts/function-components/introduction)
-use crate::html::AnyScope;
-use crate::{Component, Html, Properties};
+use crate::html::{AnyScope, BaseComponent};
+use crate::{Html, Properties};
use scoped_tls_hkt::scoped_thread_local;
use std::cell::RefCell;
use std::fmt;
@@ -100,7 +100,7 @@ where
}
}
-impl Component for FunctionComponent
+impl BaseComponent for FunctionComponent
where
T: FunctionProvider,
{
@@ -137,6 +137,10 @@ where
msg()
}
+ fn changed(&mut self, _ctx: &Context) -> bool {
+ true
+ }
+
fn view(&self, ctx: &Context) -> Html {
self.with_hook_state(|| T::run(&*ctx.props()))
}
@@ -192,6 +196,7 @@ pub struct HookUpdater {
hook: Rc>,
process_message: ProcessMessage,
}
+
impl HookUpdater {
/// Callback which runs the hook.
pub fn callback(&self, cb: F)
diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs
index 4ae86a5bb90..004480071f4 100644
--- a/packages/yew/src/html/component/lifecycle.rs
+++ b/packages/yew/src/html/component/lifecycle.rs
@@ -1,6 +1,6 @@
//! Component lifecycle module
-use super::{AnyScope, Component, Scope};
+use super::{AnyScope, BaseComponent, Scope};
use crate::html::RenderError;
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{Suspense, Suspension};
@@ -10,7 +10,7 @@ use crate::{Context, NodeRef};
use std::rc::Rc;
use web_sys::Element;
-pub(crate) struct ComponentState {
+pub(crate) struct ComponentState {
pub(crate) component: Box,
pub(crate) root_node: VNode,
@@ -27,7 +27,7 @@ pub(crate) struct ComponentState {
pub(crate) vcomp_id: u64,
}
-impl ComponentState {
+impl ComponentState {
pub(crate) fn new(
parent: Element,
next_sibling: NodeRef,
@@ -61,7 +61,7 @@ impl ComponentState {
}
}
-pub(crate) struct CreateRunner {
+pub(crate) struct CreateRunner {
pub(crate) parent: Element,
pub(crate) next_sibling: NodeRef,
pub(crate) placeholder: VNode,
@@ -70,7 +70,7 @@ pub(crate) struct CreateRunner {
pub(crate) scope: Scope,
}
-impl Runnable for CreateRunner {
+impl Runnable for CreateRunner {
fn run(self: Box) {
let mut current_state = self.scope.state.borrow_mut();
if current_state.is_none() {
@@ -89,7 +89,7 @@ impl Runnable for CreateRunner {
}
}
-pub(crate) enum UpdateEvent {
+pub(crate) enum UpdateEvent {
/// Wraps messages for a component.
Message(COMP::Message),
/// Wraps batch of messages for a component.
@@ -100,12 +100,12 @@ pub(crate) enum UpdateEvent {
Shift(Element, NodeRef),
}
-pub(crate) struct UpdateRunner {
+pub(crate) struct UpdateRunner {
pub(crate) state: Shared
>>,
pub(crate) event: UpdateEvent,
}
-impl Runnable for UpdateRunner {
+impl Runnable for UpdateRunner {
fn run(self: Box) {
if let Some(mut state) = self.state.borrow_mut().as_mut() {
let schedule_render = match self.event {
@@ -162,11 +162,11 @@ impl Runnable for UpdateRunner {
}
}
-pub(crate) struct DestroyRunner {
+pub(crate) struct DestroyRunner {
pub(crate) state: Shared
>>,
}
-impl Runnable for DestroyRunner {
+impl Runnable for DestroyRunner {
fn run(self: Box) {
if let Some(mut state) = self.state.borrow_mut().take() {
#[cfg(debug_assertions)]
@@ -179,11 +179,11 @@ impl Runnable for DestroyRunner {
}
}
-pub(crate) struct RenderRunner {
+pub(crate) struct RenderRunner {
pub(crate) state: Shared
>>,
}
-impl Runnable for RenderRunner {
+impl Runnable for RenderRunner {
fn run(self: Box) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]
@@ -267,11 +267,11 @@ impl Runnable for RenderRunner {
}
}
-pub(crate) struct RenderedRunner {
+pub(crate) struct RenderedRunner {
pub(crate) state: Shared
>>,
}
-impl Runnable for RenderedRunner {
+impl Runnable for RenderedRunner {
fn run(self: Box) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]
diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs
index 6c405d4ee3b..124a5167943 100644
--- a/packages/yew/src/html/component/mod.rs
+++ b/packages/yew/src/html/component/mod.rs
@@ -15,12 +15,12 @@ use std::rc::Rc;
/// The [`Component`]'s context. This contains component's [`Scope`] and and props and
/// is passed to every lifecycle method.
#[derive(Debug)]
-pub struct Context {
+pub struct Context {
pub(crate) scope: Scope,
pub(crate) props: Rc,
}
-impl Context {
+impl Context {
/// The component scope
#[inline]
pub fn link(&self) -> &Scope {
@@ -34,6 +34,35 @@ impl Context {
}
}
+/// The common base of both function components and struct components.
+///
+/// If you are taken here by doc links, you might be looking for: [`Component`]
+pub trait BaseComponent: Sized + 'static {
+ /// The Component's Message.
+ type Message: 'static;
+
+ /// The Component's properties.
+ type Properties: Properties;
+
+ /// Creates a component.
+ fn create(ctx: &Context) -> Self;
+
+ /// Updates component's internal state.
+ fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool;
+
+ /// React to changes of component properties.
+ fn changed(&mut self, ctx: &Context) -> bool;
+
+ /// Returns a component layout to be rendered.
+ fn view(&self, ctx: &Context) -> Html;
+
+ /// Notified after a layout is rendered.
+ fn rendered(&mut self, ctx: &Context, first_render: bool);
+
+ /// Notified before a component is destroyed.
+ fn destroy(&mut self, ctx: &Context);
+}
+
/// Components are the basic building blocks of the UI in a Yew app. Each Component
/// chooses how to display itself using received props and self-managed state.
/// Components can be dynamic and interactive by declaring messages that are
@@ -95,3 +124,36 @@ pub trait Component: Sized + 'static {
#[allow(unused_variables)]
fn destroy(&mut self, ctx: &Context) {}
}
+
+impl BaseComponent for T
+where
+ T: Sized + Component + 'static,
+{
+ type Message = ::Message;
+
+ type Properties = ::Properties;
+
+ fn create(ctx: &Context) -> Self {
+ Component::create(ctx)
+ }
+
+ fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool {
+ Component::update(self, ctx, msg)
+ }
+
+ fn changed(&mut self, ctx: &Context) -> bool {
+ Component::changed(self, ctx)
+ }
+
+ fn view(&self, ctx: &Context) -> Html {
+ Component::view(self, ctx)
+ }
+
+ fn rendered(&mut self, ctx: &Context, first_render: bool) {
+ Component::rendered(self, ctx, first_render)
+ }
+
+ fn destroy(&mut self, ctx: &Context) {
+ Component::destroy(self, ctx)
+ }
+}
diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs
index 6d5aaeed50d..c29001b8487 100644
--- a/packages/yew/src/html/component/scope.rs
+++ b/packages/yew/src/html/component/scope.rs
@@ -5,7 +5,7 @@ use super::{
ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent,
UpdateRunner,
},
- Component,
+ BaseComponent,
};
use crate::callback::Callback;
use crate::context::{ContextHandle, ContextProvider};
@@ -34,7 +34,7 @@ pub struct AnyScope {
pub(crate) vcomp_id: u64,
}
-impl From> for AnyScope {
+impl From> for AnyScope {
fn from(scope: Scope) -> Self {
AnyScope {
type_id: TypeId::of::(),
@@ -71,7 +71,7 @@ impl AnyScope {
}
/// Attempts to downcast into a typed scope
- pub fn downcast(self) -> Scope {
+ pub fn downcast(self) -> Scope {
let state = self
.state
.downcast::>>>()
@@ -93,7 +93,7 @@ impl AnyScope {
}
}
- pub(crate) fn find_parent_scope(&self) -> Option> {
+ pub(crate) fn find_parent_scope(&self) -> Option> {
let expected_type_id = TypeId::of::();
iter::successors(Some(self), |scope| scope.get_parent())
.filter(|scope| scope.get_type_id() == &expected_type_id)
@@ -122,7 +122,7 @@ pub(crate) trait Scoped {
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
}
-impl Scoped for Scope {
+impl Scoped for Scope {
fn to_any(&self) -> AnyScope {
self.clone().into()
}
@@ -156,7 +156,7 @@ impl Scoped for Scope {
}
/// A context which allows sending messages to a component.
-pub struct Scope {
+pub struct Scope {
parent: Option>,
pub(crate) state: Shared
>>,
@@ -165,13 +165,13 @@ pub struct Scope {
pub(crate) vcomp_id: u64,
}
-impl fmt::Debug for Scope {
+impl fmt::Debug for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Scope<_>")
}
}
-impl Clone for Scope {
+impl Clone for Scope {
fn clone(&self) -> Self {
Scope {
parent: self.parent.clone(),
@@ -183,7 +183,7 @@ impl Clone for Scope {
}
}
-impl Scope {
+impl Scope {
/// Returns the parent scope
pub fn get_parent(&self) -> Option<&AnyScope> {
self.parent.as_deref()
@@ -513,7 +513,7 @@ impl Scope {
/// Defines a message type that can be sent to a component.
/// Used for the return value of closure given to [Scope::batch_callback](struct.Scope.html#method.batch_callback).
-pub trait SendAsMessage {
+pub trait SendAsMessage {
/// Sends the message to the given component's scope.
/// See [Scope::batch_callback](struct.Scope.html#method.batch_callback).
fn send(self, scope: &Scope);
@@ -521,7 +521,7 @@ pub trait SendAsMessage