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

yew/dom_bundle/
position.rs

1//! Structs for keeping track where in the DOM a node belongs
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use web_sys::{Element, Node};
7
8/// A position in the list of children of an implicit parent [`Element`].
9///
10/// This can either be in front of a `DomSlot::at(next_sibling)`, at the end of the list with
11/// `DomSlot::at_end()`, or a dynamic position in the list with [`DynamicDomSlot::to_position`].
12#[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/// A dynamic dom slot can be reassigned. This change is also seen by the [`DomSlot`] from
24/// [`Self::to_position`] before the reassignment took place.
25#[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        // A special marker element that should not be referenced
54        static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into();
55    }
56    /// Get a "trap" node, or None if compiled without debug_assertions
57    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            // When not running with debug_assertions, there is no trap node
76            let _ = node;
77            false
78        }
79    }
80}
81
82impl DomSlot {
83    /// Denotes the position just before the given node in its parent's list of children.
84    pub fn at(next_sibling: Node) -> Self {
85        Self::create(Some(next_sibling))
86    }
87
88    /// Denotes the position at the end of a list of children. The parent is implicit.
89    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    /// A new "placeholder" [DomSlot] that should not be used to insert nodes
100    #[inline]
101    pub fn new_debug_trapped() -> Self {
102        Self::create(trap_impl::get_trap_node())
103    }
104
105    /// Get the [Node] that comes just after the position, or `None` if this denotes the position at
106    /// the end
107    fn with_next_sibling_check_trap<R>(&self, f: impl FnOnce(Option<&Node>) -> R) -> R {
108        let checkedf = |node: Option<&Node>| {
109            // MSRV 1.82 could rewrite this with `is_none_or`
110            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    /// Insert a [Node] at the position denoted by this slot. `parent` must be the actual parent
131    /// element of the children that this slot is implicitly a part of.
132    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                    // Log normally, so we can inspect the nodes in console
143                    gloo::console::error!(msg, err, parent, next_sibling, node);
144                    // Log via tracing for consistency
145                    tracing::error!(msg);
146                    // Panic to short-curcuit and fail
147                    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    /// Create a dynamic dom slot that initially represents ("targets") the same slot as the
161    /// argument.
162    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    /// Change the [`DomSlot`] that is targeted. Subsequently, this will behave as if `self` was
173    /// created from the passed DomSlot in the first place.
174    pub fn reassign(&self, next_position: DomSlot) {
175        // TODO: is not defensive against accidental reference loops
176        *self.target.borrow_mut() = next_position;
177    }
178
179    /// Get a [`DomSlot`] that gets automatically updated when `self` gets reassigned. All such
180    /// slots are equivalent to each other and point to the same position.
181    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        // we use an iterative approach to traverse a possible long chain for references
189        // see for example issue #3043 why a recursive call is impossible for large lists in vdom
190
191        // TODO: there could be some data structure that performs better here. E.g. a balanced tree
192        // with parent pointers come to mind, but they are a bit fiddly to implement in rust
193        let mut this = self.target.clone();
194        loop {
195            //                          v------- borrow lives for this match expression
196            let next_this = match &this.borrow().variant {
197                DomSlotVariant::Node(ref n) => break f(n.as_ref()),
198                // We clone an Rc here temporarily, so that we don't have to consume stack
199                // space. The alternative would be to keep the
200                // `Ref<'_, DomSlot>` above in some temporary buffer
201                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        // We reassign *after* we called `to_position` here to be strict in the test
254        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        // Now reassign the middle man, but get the node from `target`
273        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        // basic tests that these don't panic. We don't enforce any specific format.
285        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}