This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/dom_bundle/
bsuspense.rs

1//! This module contains the bundle version of a supsense [BSuspense]
2
3use gloo::utils::document;
4use web_sys::Element;
5
6#[cfg(feature = "hydration")]
7use super::Fragment;
8use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget};
9use crate::html::AnyScope;
10use crate::virtual_dom::{Key, VSuspense};
11
12#[derive(Debug)]
13enum Fallback {
14    /// Suspense Fallback with fallback being rendered as placeholder.
15    Bundle(BNode),
16    /// Suspense Fallback with Hydration Fragment being rendered as placeholder.
17    #[cfg(feature = "hydration")]
18    Fragment(Fragment),
19}
20
21/// The bundle implementation to [VSuspense]
22#[derive(Debug)]
23pub(super) struct BSuspense {
24    children_bundle: BNode,
25    /// The supsense is suspended if fallback contains [Some] bundle
26    fallback: Option<Fallback>,
27    detached_parent: Element,
28    key: Option<Key>,
29}
30
31impl BSuspense {
32    /// Get the key of the underlying suspense
33    pub fn key(&self) -> Option<&Key> {
34        self.key.as_ref()
35    }
36}
37
38impl ReconcileTarget for BSuspense {
39    fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
40        match self.fallback {
41            Some(m) => {
42                match m {
43                    Fallback::Bundle(bundle) => {
44                        bundle.detach(root, parent, parent_to_detach);
45                    }
46
47                    #[cfg(feature = "hydration")]
48                    Fallback::Fragment(fragment) => {
49                        fragment.detach(root, parent, parent_to_detach);
50                    }
51                }
52
53                self.children_bundle
54                    .detach(root, &self.detached_parent, false);
55            }
56            None => {
57                self.children_bundle.detach(root, parent, parent_to_detach);
58            }
59        }
60    }
61
62    fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot {
63        match self.fallback.as_ref() {
64            Some(Fallback::Bundle(bundle)) => bundle.shift(next_parent, slot),
65            #[cfg(feature = "hydration")]
66            Some(Fallback::Fragment(fragment)) => fragment.shift(next_parent, slot),
67            None => self.children_bundle.shift(next_parent, slot),
68        }
69    }
70}
71
72impl Reconcilable for VSuspense {
73    type Bundle = BSuspense;
74
75    fn attach(
76        self,
77        root: &BSubtree,
78        parent_scope: &AnyScope,
79        parent: &Element,
80        slot: DomSlot,
81    ) -> (DomSlot, Self::Bundle) {
82        let VSuspense {
83            children,
84            fallback,
85            suspended,
86            key,
87        } = self;
88        let detached_parent = document()
89            .create_element("div")
90            .expect("failed to create detached element");
91
92        // When it's suspended, we render children into an element that is detached from the dom
93        // tree while rendering fallback UI into the original place where children resides in.
94        if suspended {
95            let (_child_ref, children_bundle) =
96                children.attach(root, parent_scope, &detached_parent, DomSlot::at_end());
97            let (fallback_ref, fallback) = fallback.attach(root, parent_scope, parent, slot);
98            (
99                fallback_ref,
100                BSuspense {
101                    children_bundle,
102                    fallback: Some(Fallback::Bundle(fallback)),
103                    detached_parent,
104                    key,
105                },
106            )
107        } else {
108            let (child_ref, children_bundle) = children.attach(root, parent_scope, parent, slot);
109            (
110                child_ref,
111                BSuspense {
112                    children_bundle,
113                    fallback: None,
114                    detached_parent,
115                    key,
116                },
117            )
118        }
119    }
120
121    fn reconcile_node(
122        self,
123        root: &BSubtree,
124        parent_scope: &AnyScope,
125        parent: &Element,
126        slot: DomSlot,
127        bundle: &mut BNode,
128    ) -> DomSlot {
129        match bundle {
130            // We only preserve the child state if they are the same suspense.
131            BNode::Suspense(m) if m.key == self.key => {
132                self.reconcile(root, parent_scope, parent, slot, m)
133            }
134            _ => self.replace(root, parent_scope, parent, slot, bundle),
135        }
136    }
137
138    fn reconcile(
139        self,
140        root: &BSubtree,
141        parent_scope: &AnyScope,
142        parent: &Element,
143        slot: DomSlot,
144        suspense: &mut Self::Bundle,
145    ) -> DomSlot {
146        let VSuspense {
147            children,
148            fallback: vfallback,
149            suspended,
150            key: _,
151        } = self;
152
153        let children_bundle = &mut suspense.children_bundle;
154        // no need to update key & detached_parent
155
156        // When it's suspended, we render children into an element that is detached from the dom
157        // tree while rendering fallback UI into the original place where children resides in.
158        match (suspended, &mut suspense.fallback) {
159            // Both suspended, reconcile children into detached_parent, fallback into the DOM
160            (true, Some(fallback)) => {
161                children.reconcile_node(
162                    root,
163                    parent_scope,
164                    &suspense.detached_parent,
165                    DomSlot::at_end(),
166                    children_bundle,
167                );
168
169                match fallback {
170                    Fallback::Bundle(bundle) => {
171                        vfallback.reconcile_node(root, parent_scope, parent, slot, bundle)
172                    }
173                    #[cfg(feature = "hydration")]
174                    Fallback::Fragment(fragment) => match fragment.front().cloned() {
175                        Some(m) => DomSlot::at(m),
176                        None => slot,
177                    },
178                }
179            }
180            // Not suspended, just reconcile the children into the DOM
181            (false, None) => {
182                children.reconcile_node(root, parent_scope, parent, slot, children_bundle)
183            }
184            // Freshly suspended. Shift children into the detached parent, then add fallback to the
185            // DOM
186            (true, None) => {
187                children_bundle.shift(&suspense.detached_parent, DomSlot::at_end());
188
189                children.reconcile_node(
190                    root,
191                    parent_scope,
192                    &suspense.detached_parent,
193                    DomSlot::at_end(),
194                    children_bundle,
195                );
196                // first render of fallback
197
198                let (fallback_ref, fallback) = vfallback.attach(root, parent_scope, parent, slot);
199                suspense.fallback = Some(Fallback::Bundle(fallback));
200                fallback_ref
201            }
202            // Freshly unsuspended. Detach fallback from the DOM, then shift children into it.
203            (false, Some(_)) => {
204                match suspense.fallback.take() {
205                    Some(Fallback::Bundle(bundle)) => {
206                        bundle.detach(root, parent, false);
207                    }
208                    #[cfg(feature = "hydration")]
209                    Some(Fallback::Fragment(fragment)) => {
210                        fragment.detach(root, parent, false);
211                    }
212                    None => {
213                        unreachable!("None condition has been checked before.")
214                    }
215                };
216
217                children_bundle.shift(parent, slot.clone());
218                children.reconcile_node(root, parent_scope, parent, slot, children_bundle)
219            }
220        }
221    }
222}
223
224#[cfg(feature = "hydration")]
225mod feat_hydration {
226    use super::*;
227    use crate::dom_bundle::{Fragment, Hydratable};
228    use crate::virtual_dom::Collectable;
229
230    impl Hydratable for VSuspense {
231        fn hydrate(
232            self,
233            root: &BSubtree,
234            parent_scope: &AnyScope,
235            parent: &Element,
236            fragment: &mut Fragment,
237        ) -> Self::Bundle {
238            let detached_parent = document()
239                .create_element("div")
240                .expect("failed to create detached element");
241
242            let collectable = Collectable::Suspense;
243            let fallback_fragment = Fragment::collect_between(fragment, &collectable, parent);
244
245            let mut nodes = fallback_fragment.deep_clone();
246
247            for node in nodes.iter() {
248                detached_parent.append_child(node).unwrap();
249            }
250
251            // Even if initially suspended, these children correspond to the first non-suspended
252            // content Refer to VSuspense::render_to_string
253            let children_bundle =
254                self.children
255                    .hydrate(root, parent_scope, &detached_parent, &mut nodes);
256
257            // We trim all leading text nodes before checking as it's likely these are whitespaces.
258            nodes.trim_start_text_nodes();
259
260            assert!(nodes.is_empty(), "expected end of suspense, found node.");
261
262            BSuspense {
263                children_bundle,
264                detached_parent,
265                key: self.key,
266
267                // We start hydration with the BSuspense being suspended.
268                // A subsequent render will resume the BSuspense if not needed to be suspended.
269                fallback: Some(Fallback::Fragment(fallback_fragment)),
270            }
271        }
272    }
273}