yew/dom_bundle/
position.rs1use std::cell::RefCell;
4use std::rc::Rc;
5
6use web_sys::{Element, Node};
7
8#[derive(Clone)]
13pub(crate) struct DomSlot {
14 variant: DomSlotVariant,
15}
16
17#[derive(Clone)]
18enum DomSlotVariant {
19 Node(Option<Node>),
20 Chained(DynamicDomSlot),
21}
22
23#[derive(Clone)]
26pub(crate) struct DynamicDomSlot {
27 target: Rc<RefCell<DomSlot>>,
28}
29
30impl std::fmt::Debug for DomSlot {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 self.with_next_sibling(|n| {
33 let formatted_node = match n {
34 None => None,
35 Some(n) if trap_impl::is_trap(n) => Some("<not yet initialized />".to_string()),
36 Some(n) => Some(crate::utils::print_node(n)),
37 };
38 write!(f, "DomSlot {{ next_sibling: {formatted_node:?} }}")
39 })
40 }
41}
42
43impl std::fmt::Debug for DynamicDomSlot {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{:#?}", *self.target.borrow())
46 }
47}
48
49mod trap_impl {
50 use super::Node;
51 #[cfg(debug_assertions)]
52 thread_local! {
53 static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into();
55 }
56 pub fn get_trap_node() -> Option<Node> {
58 #[cfg(debug_assertions)]
59 {
60 TRAP.with(|trap| Some(trap.clone()))
61 }
62 #[cfg(not(debug_assertions))]
63 {
64 None
65 }
66 }
67 #[inline]
68 pub fn is_trap(node: &Node) -> bool {
69 #[cfg(debug_assertions)]
70 {
71 TRAP.with(|trap| node == trap)
72 }
73 #[cfg(not(debug_assertions))]
74 {
75 let _ = node;
77 false
78 }
79 }
80}
81
82impl DomSlot {
83 pub fn at(next_sibling: Node) -> Self {
85 Self::create(Some(next_sibling))
86 }
87
88 pub fn at_end() -> Self {
90 Self::create(None)
91 }
92
93 pub fn create(next_sibling: Option<Node>) -> Self {
94 Self {
95 variant: DomSlotVariant::Node(next_sibling),
96 }
97 }
98
99 #[inline]
101 pub fn new_debug_trapped() -> Self {
102 Self::create(trap_impl::get_trap_node())
103 }
104
105 fn with_next_sibling_check_trap<R>(&self, f: impl FnOnce(Option<&Node>) -> R) -> R {
108 let checkedf = |node: Option<&Node>| {
109 let is_trapped = match node {
111 None => false,
112 Some(node) => trap_impl::is_trap(node),
113 };
114 assert!(
115 !is_trapped,
116 "Should not use a trapped DomSlot. Please report this as an internal bug in yew."
117 );
118 f(node)
119 };
120 self.with_next_sibling(checkedf)
121 }
122
123 fn with_next_sibling<R>(&self, f: impl FnOnce(Option<&Node>) -> R) -> R {
124 match &self.variant {
125 DomSlotVariant::Node(ref n) => f(n.as_ref()),
126 DomSlotVariant::Chained(ref chain) => chain.with_next_sibling(f),
127 }
128 }
129
130 pub(super) fn insert(&self, parent: &Element, node: &Node) {
133 self.with_next_sibling_check_trap(|next_sibling: Option<&Node>| {
134 parent
135 .insert_before(node, next_sibling)
136 .unwrap_or_else(|err| {
137 let msg = if next_sibling.is_some() {
138 "failed to insert node before next sibling"
139 } else {
140 "failed to append child"
141 };
142 gloo::console::error!(msg, err, parent, next_sibling, node);
144 tracing::error!(msg);
146 panic!("{}", msg)
148 });
149 });
150 }
151
152 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
153 #[cfg(test)]
154 fn get(&self) -> Option<Node> {
155 self.with_next_sibling(|n| n.cloned())
156 }
157}
158
159impl DynamicDomSlot {
160 pub fn new(initial_position: DomSlot) -> Self {
163 Self {
164 target: Rc::new(RefCell::new(initial_position)),
165 }
166 }
167
168 pub fn new_debug_trapped() -> Self {
169 Self::new(DomSlot::new_debug_trapped())
170 }
171
172 pub fn reassign(&self, next_position: DomSlot) {
175 *self.target.borrow_mut() = next_position;
177 }
178
179 pub fn to_position(&self) -> DomSlot {
182 DomSlot {
183 variant: DomSlotVariant::Chained(self.clone()),
184 }
185 }
186
187 fn with_next_sibling<R>(&self, f: impl FnOnce(Option<&Node>) -> R) -> R {
188 let mut this = self.target.clone();
194 loop {
195 let next_this = match &this.borrow().variant {
197 DomSlotVariant::Node(ref n) => break f(n.as_ref()),
198 DomSlotVariant::Chained(ref chain) => chain.target.clone(),
202 };
203 this = next_this;
204 }
205 }
206}
207
208#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
209#[cfg(test)]
210mod layout_tests {
211 use gloo::utils::document;
212 use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
213
214 use super::*;
215
216 wasm_bindgen_test_configure!(run_in_browser);
217
218 #[test]
219 fn new_at_and_get() {
220 let node = document().create_element("p").unwrap();
221 let position = DomSlot::at(node.clone().into());
222 assert_eq!(
223 position.get().unwrap(),
224 node.clone().into(),
225 "expected the DomSlot to be at {node:#?}"
226 );
227 }
228
229 #[test]
230 fn new_at_end_and_get() {
231 let position = DomSlot::at_end();
232 assert!(
233 position.get().is_none(),
234 "expected the DomSlot to not have a next sibling"
235 );
236 }
237
238 #[test]
239 fn get_through_dynamic() {
240 let original = DomSlot::at(document().create_element("p").unwrap().into());
241 let target = DynamicDomSlot::new(original.clone());
242 assert_eq!(
243 target.to_position().get(),
244 original.get(),
245 "expected {target:#?} to point to the same position as {original:#?}"
246 );
247 }
248
249 #[test]
250 fn get_after_reassign() {
251 let target = DynamicDomSlot::new(DomSlot::at_end());
252 let target_pos = target.to_position();
253 let replacement = DomSlot::at(document().create_element("p").unwrap().into());
255 target.reassign(replacement.clone());
256 assert_eq!(
257 target_pos.get(),
258 replacement.get(),
259 "expected {target:#?} to point to the same position as {replacement:#?}"
260 );
261 }
262
263 #[test]
264 fn get_chain_after_reassign() {
265 let middleman = DynamicDomSlot::new(DomSlot::at_end());
266 let target = DynamicDomSlot::new(middleman.to_position());
267 let target_pos = target.to_position();
268 assert!(
269 target.to_position().get().is_none(),
270 "should not yet point to a node"
271 );
272 let replacement = DomSlot::at(document().create_element("p").unwrap().into());
274 middleman.reassign(replacement.clone());
275 assert_eq!(
276 target_pos.get(),
277 replacement.get(),
278 "expected {target:#?} to point to the same position as {replacement:#?}"
279 );
280 }
281
282 #[test]
283 fn debug_printing() {
284 println!("At end: {:?}", DomSlot::at_end());
286 println!("Trapped: {:?}", DomSlot::new_debug_trapped());
287 println!(
288 "At element: {:?}",
289 DomSlot::at(document().create_element("p").unwrap().into())
290 );
291 }
292}