1use 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#[derive(Debug)]
12pub struct BPortal {
13 inner_root: BSubtree,
15 host: Element,
17 inner_sibling: Option<Node>,
19 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 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 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 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 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 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}