Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions packages/yew/src/dom_bundle/bsuspense.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::{BNode, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
use crate::virtual_dom::{Key, VSuspense};
use crate::NodeRef;
use gloo::utils::document;
use web_sys::Element;

/// The bundle implementation to [VSuspense]
Expand Down Expand Up @@ -56,11 +57,12 @@ impl Reconcilable for VSuspense {
let VSuspense {
children,
fallback,
detached_parent,
suspended,
key,
} = self;
let detached_parent = detached_parent.expect("no detached parent?");
let detached_parent = document()
.create_element("div")
.expect("failed to create detached element");

// 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.
Expand Down Expand Up @@ -100,10 +102,7 @@ impl Reconcilable for VSuspense {
) -> NodeRef {
match bundle {
// We only preserve the child state if they are the same suspense.
BNode::Suspense(m)
if m.key == self.key
&& self.detached_parent.as_ref() == Some(&m.detached_parent) =>
{
BNode::Suspense(m) if m.key == self.key => {
self.reconcile(parent_scope, parent, next_sibling, m)
}
_ => self.replace(parent_scope, parent, next_sibling, bundle),
Expand All @@ -120,11 +119,9 @@ impl Reconcilable for VSuspense {
let VSuspense {
children,
fallback,
detached_parent,
suspended,
key: _,
} = self;
let detached_parent = detached_parent.expect("no detached parent?");

let children_bundle = &mut suspense.children_bundle;
// no need to update key & detached_parent
Expand All @@ -136,7 +133,7 @@ impl Reconcilable for VSuspense {
(true, Some(fallback_bundle)) => {
children.reconcile_node(
parent_scope,
&detached_parent,
&suspense.detached_parent,
NodeRef::default(),
children_bundle,
);
Expand All @@ -149,11 +146,11 @@ impl Reconcilable for VSuspense {
}
// Freshly suspended. Shift children into the detached parent, then add fallback to the DOM
(true, None) => {
children_bundle.shift(&detached_parent, NodeRef::default());
children_bundle.shift(&suspense.detached_parent, NodeRef::default());

children.reconcile_node(
parent_scope,
&detached_parent,
&suspense.detached_parent,
NodeRef::default(),
children_bundle,
);
Expand Down
6 changes: 3 additions & 3 deletions packages/yew/src/html/component/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use super::scope::{AnyScope, Scope};
use super::BaseComponent;
use crate::html::{Html, RenderError};
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{Suspense, Suspension};
use crate::suspense::{BaseSuspense, Suspension};
use crate::{Callback, Context, HtmlResult};
use std::any::Any;
use std::rc::Rc;
Expand Down Expand Up @@ -387,7 +387,7 @@ impl RenderRunner {
let comp_scope = state.inner.any_scope();

let suspense_scope = comp_scope
.find_parent_scope::<Suspense>()
.find_parent_scope::<BaseSuspense>()
.expect("To suspend rendering, a <Suspense /> component is required.");
let suspense = suspense_scope.get_component().unwrap();

Expand Down Expand Up @@ -419,7 +419,7 @@ impl RenderRunner {
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_scope = comp_scope.find_parent_scope::<BaseSuspense>().unwrap();
let suspense = suspense_scope.get_component().unwrap();

suspense.resume(m);
Expand Down
2 changes: 1 addition & 1 deletion packages/yew/src/html/component/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ mod feat_ssr {
}

#[cfg(not(any(feature = "ssr", feature = "csr")))]
mod feat_no_render_ssr {
mod feat_no_csr_ssr {
use super::*;

// Skeleton code to provide public methods when no renderer are enabled.
Expand Down
185 changes: 112 additions & 73 deletions packages/yew/src/suspense/component.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
use crate::html::{Children, Component, Context, Html, Properties, Scope};
use crate::virtual_dom::{Key, VList, VNode, VSuspense};

use web_sys::Element;

use super::Suspension;
use crate::html::{Children, Html, Properties};

#[derive(Properties, PartialEq, Debug, Clone)]
pub struct SuspenseProps {
Expand All @@ -12,95 +7,139 @@ pub struct SuspenseProps {

#[prop_or_default]
pub fallback: Html,

#[prop_or_default]
pub key: Option<Key>,
}

#[derive(Debug)]
pub enum SuspenseMsg {
Suspend(Suspension),
Resume(Suspension),
}
#[cfg(any(feature = "csr", feature = "ssr"))]
mod feat_csr_ssr {
use super::*;

/// Suspend rendering and show a fallback UI until the underlying task completes.
#[derive(Debug)]
pub struct Suspense {
link: Scope<Self>,
suspensions: Vec<Suspension>,
detached_parent: Option<Element>,
}
use crate::html::{Children, Component, Context, Html, Scope};
use crate::suspense::Suspension;
use crate::virtual_dom::{VNode, VSuspense};
use crate::{function_component, html};

#[derive(Properties, PartialEq, Debug, Clone)]
pub(crate) struct BaseSuspenseProps {
pub children: Children,

impl Component for Suspense {
type Properties = SuspenseProps;
type Message = SuspenseMsg;
pub fallback: Option<Html>,
}

#[derive(Debug)]
pub(crate) enum BaseSuspenseMsg {
Suspend(Suspension),
Resume(Suspension),
}

fn create(ctx: &Context<Self>) -> Self {
Self {
link: ctx.link().clone(),
suspensions: Vec::new(),
#[derive(Debug)]
pub(crate) struct BaseSuspense {
link: Scope<Self>,
suspensions: Vec<Suspension>,
}

#[cfg(target_arch = "wasm32")]
detached_parent: web_sys::window()
.and_then(|m| m.document())
.and_then(|m| m.create_element("div").ok()),
impl Component for BaseSuspense {
type Properties = BaseSuspenseProps;
type Message = BaseSuspenseMsg;

#[cfg(not(target_arch = "wasm32"))]
detached_parent: None,
fn create(ctx: &Context<Self>) -> Self {
Self {
link: ctx.link().clone(),
suspensions: Vec::new(),
}
}
}

fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Self::Message::Suspend(m) => {
if m.resumed() {
return false;
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Self::Message::Suspend(m) => {
assert!(
ctx.props().fallback.is_some(),
"You cannot suspend from a component rendered as a fallback."
);

if m.resumed() {
return false;
}

m.listen(self.link.callback(Self::Message::Resume));
m.listen(self.link.callback(Self::Message::Resume));

self.suspensions.push(m);
self.suspensions.push(m);

true
true
}
Self::Message::Resume(ref m) => {
let suspensions_len = self.suspensions.len();
self.suspensions.retain(|n| m != n);

suspensions_len != self.suspensions.len()
}
}
Self::Message::Resume(ref m) => {
let suspensions_len = self.suspensions.len();
self.suspensions.retain(|n| m != n);
}

suspensions_len != self.suspensions.len()
fn view(&self, ctx: &Context<Self>) -> Html {
let BaseSuspenseProps { children, fallback } = (*ctx.props()).clone();
let children = html! {<>{children}</>};

match fallback {
Some(fallback) => {
let vsuspense = VSuspense::new(
children,
fallback,
!self.suspensions.is_empty(),
// We don't need to key this as the key will be applied to the component.
None,
);

VNode::from(vsuspense)
}
None => children,
}
}
}

fn view(&self, ctx: &Context<Self>) -> Html {
let SuspenseProps {
children,
fallback: fallback_vnode,
key,
} = (*ctx.props()).clone();

let children_vnode =
VNode::from(VList::with_children(children.into_iter().collect(), None));

let vsuspense = VSuspense::new(
children_vnode,
fallback_vnode,
self.detached_parent.clone(),
!self.suspensions.is_empty(),
key,
);

VNode::from(vsuspense)
impl BaseSuspense {
pub(crate) fn suspend(&self, s: Suspension) {
self.link.send_message(BaseSuspenseMsg::Suspend(s));
}

pub(crate) fn resume(&self, s: Suspension) {
self.link.send_message(BaseSuspenseMsg::Resume(s));
}
}

/// Suspend rendering and show a fallback UI until the underlying task completes.
#[function_component]
pub fn Suspense(props: &SuspenseProps) -> Html {
let SuspenseProps { children, fallback } = props.clone();

let fallback = html! {
<BaseSuspense fallback={None}>
{fallback}
</BaseSuspense>
};

html! {
<BaseSuspense {fallback}>
{children}
</BaseSuspense>
}
}
}

#[cfg(any(feature = "csr", feature = "ssr"))]
impl Suspense {
pub(crate) fn suspend(&self, s: Suspension) {
self.link.send_message(SuspenseMsg::Suspend(s));
}
pub use feat_csr_ssr::*;

pub(crate) fn resume(&self, s: Suspension) {
self.link.send_message(SuspenseMsg::Resume(s));
#[cfg(not(any(feature = "ssr", feature = "csr")))]
mod feat_no_csr_ssr {
use super::*;

use crate::function_component;

/// Suspend rendering and show a fallback UI until the underlying task completes.
#[function_component]
pub fn Suspense(_props: &SuspenseProps) -> Html {
Html::default()
Comment on lines +138 to +140
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is here to be replaced when SSR hydration is a thing, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a skeleton code for libraries when they don't have any renderer enabled (which means that they never gets rendered).

If we apply feature flags on the actual Suspense component multiple feature flags will be needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in the long term, it may be beneficial to separate rendering logic into a separate crate so we don't keep adding feature flags.

}
}

#[cfg(not(any(feature = "ssr", feature = "csr")))]
pub use feat_no_csr_ssr::*;
2 changes: 2 additions & 0 deletions packages/yew/src/suspense/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
mod component;
mod suspension;

#[cfg(any(feature = "csr", feature = "ssr"))]
pub(crate) use component::BaseSuspense;
pub use component::Suspense;
pub use suspension::{Suspension, SuspensionHandle, SuspensionResult};
12 changes: 1 addition & 11 deletions packages/yew/src/virtual_dom/vsuspense.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use super::{Key, VNode};
use web_sys::Element;

/// This struct represents a suspendable DOM fragment.
#[derive(Clone, Debug, PartialEq)]
Expand All @@ -8,26 +7,17 @@ pub struct VSuspense {
pub(crate) children: Box<VNode>,
/// Fallback nodes when suspended.
pub(crate) fallback: Box<VNode>,
/// The element to attach to when children is not attached to DOM
pub(crate) detached_parent: Option<Element>,
/// Whether the current status is suspended.
pub(crate) suspended: bool,
/// The Key.
pub(crate) key: Option<Key>,
}

impl VSuspense {
pub(crate) fn new(
children: VNode,
fallback: VNode,
detached_parent: Option<Element>,
suspended: bool,
key: Option<Key>,
) -> Self {
pub fn new(children: VNode, fallback: VNode, suspended: bool, key: Option<Key>) -> Self {
Self {
children: children.into(),
fallback: fallback.into(),
detached_parent,
suspended,
key,
}
Expand Down