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

yew/dom_bundle/
bportal.rs

1//! This module contains the bundle implementation of a portal [BPortal].
2
3use web_sys::{Element, Node};
4
5use super::{test_log, BNode, BSubtree, DomSlot};
6use crate::dom_bundle::{Reconcilable, ReconcileTarget};
7use crate::html::AnyScope;
8use crate::virtual_dom::{Key, VPortal};
9
10/// The bundle implementation to [VPortal].
11#[derive(Debug)]
12pub struct BPortal {
13    // The inner root
14    inner_root: BSubtree,
15    /// The element under which the content is inserted.
16    host: Element,
17    /// The next sibling after the inserted content
18    inner_sibling: Option<Node>,
19    /// The inserted node
20    node: Box<BNode>,
21}
22
23impl ReconcileTarget for BPortal {
24    fn detach(self, _root: &BSubtree, _parent: &Element, _parent_to_detach: bool) {
25        test_log!("Detaching portal from host",);
26        self.node.detach(&self.inner_root, &self.host, false);
27    }
28
29    fn shift(&self, _next_parent: &Element, slot: DomSlot) -> DomSlot {
30        // portals have nothing in its original place of DOM, we also do nothing.
31        slot
32    }
33}
34
35impl Reconcilable for VPortal {
36    type Bundle = BPortal;
37
38    fn attach(
39        self,
40        root: &BSubtree,
41        parent_scope: &AnyScope,
42        parent: &Element,
43        host_slot: DomSlot,
44    ) -> (DomSlot, Self::Bundle) {
45        let Self {
46            host,
47            inner_sibling,
48            node,
49        } = self;
50        let inner_slot = DomSlot::create(inner_sibling.clone());
51        let inner_root = root.create_subroot(parent.clone(), &host);
52        let (_, inner) = node.attach(&inner_root, parent_scope, &host, inner_slot);
53        (
54            host_slot,
55            BPortal {
56                inner_root,
57                host,
58                node: Box::new(inner),
59                inner_sibling,
60            },
61        )
62    }
63
64    fn reconcile_node(
65        self,
66        root: &BSubtree,
67        parent_scope: &AnyScope,
68        parent: &Element,
69        slot: DomSlot,
70        bundle: &mut BNode,
71    ) -> DomSlot {
72        match bundle {
73            BNode::Portal(portal) => self.reconcile(root, parent_scope, parent, slot, portal),
74            _ => self.replace(root, parent_scope, parent, slot, bundle),
75        }
76    }
77
78    fn reconcile(
79        self,
80        _root: &BSubtree,
81        parent_scope: &AnyScope,
82        _parent: &Element,
83        host_slot: DomSlot,
84        portal: &mut Self::Bundle,
85    ) -> DomSlot {
86        let Self {
87            host,
88            inner_sibling,
89            node,
90        } = self;
91
92        let old_host = std::mem::replace(&mut portal.host, host);
93
94        let should_shift = old_host != portal.host || portal.inner_sibling != inner_sibling;
95        portal.inner_sibling = inner_sibling;
96        let inner_slot = DomSlot::create(portal.inner_sibling.clone());
97
98        if should_shift {
99            // Remount the inner node somewhere else instead of diffing
100            // Move the node, but keep the state
101            portal.node.shift(&portal.host, inner_slot.clone());
102        }
103        node.reconcile_node(
104            &portal.inner_root,
105            parent_scope,
106            &portal.host,
107            inner_slot,
108            &mut portal.node,
109        );
110        host_slot
111    }
112}
113
114impl BPortal {
115    /// Get the key of the underlying portal
116    pub fn key(&self) -> Option<&Key> {
117        self.node.key()
118    }
119}
120
121#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
122#[cfg(test)]
123mod layout_tests {
124    extern crate self as yew;
125
126    use std::rc::Rc;
127
128    use gloo::utils::document;
129    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
130    use web_sys::HtmlInputElement;
131    use yew::virtual_dom::VPortal;
132
133    use super::*;
134    use crate::html::NodeRef;
135    use crate::tests::layout_tests::{diff_layouts, TestLayout};
136    use crate::virtual_dom::VNode;
137    use crate::{create_portal, html};
138
139    wasm_bindgen_test_configure!(run_in_browser);
140
141    #[test]
142    fn diff() {
143        let mut layouts = vec![];
144        let first_target = gloo::utils::document().create_element("i").unwrap();
145        let second_target = gloo::utils::document().create_element("o").unwrap();
146        let target_with_child = gloo::utils::document().create_element("i").unwrap();
147        let target_child = gloo::utils::document().create_element("s").unwrap();
148        target_with_child.append_child(&target_child).unwrap();
149
150        layouts.push(TestLayout {
151            name: "Portal - first target",
152            node: html! {
153                <div>
154                    {VNode::VRef(first_target.clone().into())}
155                    {VNode::VRef(second_target.clone().into())}
156                    {VNode::VPortal(Rc::new(VPortal::new(
157                        html! { {"PORTAL"} },
158                        first_target.clone(),
159                    )))}
160                    {"AFTER"}
161                </div>
162            },
163            expected: "<div><i>PORTAL</i><o></o>AFTER</div>",
164        });
165        layouts.push(TestLayout {
166            name: "Portal - second target",
167            node: html! {
168                <div>
169                    {VNode::VRef(first_target.clone().into())}
170                    {VNode::VRef(second_target.clone().into())}
171                    {VNode::VPortal(Rc::new(VPortal::new(
172                        html! { {"PORTAL"} },
173                        second_target.clone(),
174                    )))}
175                    {"AFTER"}
176                </div>
177            },
178            expected: "<div><i></i><o>PORTAL</o>AFTER</div>",
179        });
180        layouts.push(TestLayout {
181            name: "Portal - update inner content",
182            node: html! {
183                <div>
184                    {VNode::VRef(first_target.clone().into())}
185                    {VNode::VRef(second_target.clone().into())}
186                    {VNode::VPortal(Rc::new(VPortal::new(
187                        html! { <> {"PORTAL"} <b /> </> },
188                        second_target.clone(),
189                    )))}
190                    {"AFTER"}
191                </div>
192            },
193            expected: "<div><i></i><o>PORTAL<b></b></o>AFTER</div>",
194        });
195        layouts.push(TestLayout {
196            name: "Portal - replaced by text",
197            node: html! {
198                <div>
199                    {VNode::VRef(first_target.clone().into())}
200                    {VNode::VRef(second_target.clone().into())}
201                    {"FOO"}
202                    {"AFTER"}
203                </div>
204            },
205            expected: "<div><i></i><o></o>FOOAFTER</div>",
206        });
207        layouts.push(TestLayout {
208            name: "Portal - next sibling",
209            node: html! {
210                <div>
211                    {VNode::VRef(target_with_child.clone().into())}
212                    {VNode::VPortal(Rc::new(VPortal::new_before(
213                        html! { {"PORTAL"} },
214                        target_with_child.clone(),
215                        Some(target_child.clone().into()),
216                    )))}
217                </div>
218            },
219            expected: "<div><i>PORTAL<s></s></i></div>",
220        });
221
222        diff_layouts(layouts)
223    }
224
225    fn setup_parent_with_portal() -> (BSubtree, AnyScope, Element, Element) {
226        let scope = AnyScope::test();
227        let parent = document().create_element("div").unwrap();
228        let portal_host = document().create_element("div").unwrap();
229        let root = BSubtree::create_root(&parent);
230
231        let body = document().body().unwrap();
232        body.append_child(&parent).unwrap();
233        body.append_child(&portal_host).unwrap();
234
235        (root, scope, parent, portal_host)
236    }
237
238    #[test]
239    fn test_no_shift() {
240        // Portals shouldn't shift (which e.g. causes internal inputs to unfocus) when sibling
241        // doesn't change.
242        let (root, scope, parent, portal_host) = setup_parent_with_portal();
243        let input_ref = NodeRef::default();
244
245        let portal = create_portal(
246            html! { <input type="text" ref={&input_ref} /> },
247            portal_host,
248        );
249        let (_, mut bundle) = portal
250            .clone()
251            .attach(&root, &scope, &parent, DomSlot::at_end());
252
253        // Focus the input, then reconcile again
254        let input_el = input_ref.cast::<HtmlInputElement>().unwrap();
255        input_el.focus().unwrap();
256
257        let _ = portal.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut bundle);
258
259        let new_input_el = input_ref.cast::<HtmlInputElement>().unwrap();
260        assert_eq!(input_el, new_input_el);
261        assert_eq!(document().active_element(), Some(new_input_el.into()));
262    }
263}