Skip to main content

rue_core/node/
patch.rs

1use std::rc::Rc;
2use wasm_bindgen::prelude::*;
3use wasm_bindgen::JsCast;
4use web_sys::*;
5
6use super::mount::mount_to_dom;
7use super::children::patch_children;
8use super::{EventHandler, VElement, VNode};
9
10/// SVG namespace URI
11const SVG_NS: &str = "http://www.w3.org/2000/svg";
12
13// ---------------------------------------------------------------------------
14// Top-level patch dispatcher
15// ---------------------------------------------------------------------------
16
17/// Patch `old` VNode tree into `new` VNode tree in-place within `parent`.
18///
19/// Returns the (possibly new) root DOM node for the patched subtree.
20/// When `old` and `new` are the same type, the existing DOM node is reused
21/// and only the differences are applied (attributes, events, children).
22/// When they differ, the old DOM node is replaced entirely.
23pub fn patch_node(
24    old: &VNode,
25    new: &VNode,
26    parent: &web_sys::Node,
27    anchor: Option<&web_sys::Node>,
28) -> Option<web_sys::Node> {
29    // If both are empty, nothing to do
30    if matches!(old, VNode::Empty) && matches!(new, VNode::Empty) {
31        return None;
32    }
33
34    // If old is empty but new is not → mount new
35    if matches!(old, VNode::Empty) {
36        return mount_to_dom(new, parent, anchor);
37    }
38
39    // If new is empty but old is not → remove old
40    if matches!(new, VNode::Empty) {
41        if let Some(old_node) = old.dom_node() {
42            let _ = parent.remove_child(&old_node);
43        }
44        return None;
45    }
46
47    // If types differ → replace
48    if !old.same_type(new) {
49        return replace_node(old, new, parent, anchor);
50    }
51
52    // Same type — dispatch to specialized patch
53    match (old, new) {
54        (VNode::Element(old_el), VNode::Element(new_el)) => {
55            Some(patch_element(old_el, new_el, parent, anchor))
56        }
57        (VNode::Text(old_text), VNode::Text(new_text)) => {
58            patch_text_node(old_text, new_text, old)
59        }
60        (VNode::Fragment(old_children), VNode::Fragment(new_children)) => {
61            // A fragment is transparent — delegate to children patching
62            // The "old DOM node" for a fragment is tricky; we patch children directly.
63            // We pass old.children and new.children.
64            patch_children(old_children, new_children, parent);
65            // Return the first child of the new fragment as the "root"
66            new_children.first().and_then(|c| c.dom_node())
67        }
68        (VNode::Dynamic(_old_fn, _old_dom), VNode::Dynamic(new_fn, new_dom)) => {
69            // Dynamic nodes: re-run the render function and patch the result.
70            // We use the new render function.
71            let f = new_fn.borrow();
72            let new_inner = f();
73            drop(f);
74
75            // Remove old dynamic content if it exists
76            if let Some(old_node) = old.dom_node() {
77                let _ = parent.remove_child(&old_node);
78            }
79
80            // Mount the new inner VNode
81            if let Some(node) = mount_to_dom(&new_inner, parent, anchor) {
82                // Update the new Dynamic node's DOM reference
83                *new_dom.borrow_mut() = Some(node.clone());
84                Some(node)
85            } else {
86                None
87            }
88        }
89        _ => {
90            // Fallback: replace
91            replace_node(old, new, parent, anchor)
92        }
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Replace: old and new have different types
98// ---------------------------------------------------------------------------
99
100/// Remove the old DOM node and mount the new one in its place.
101fn replace_node(
102    old: &VNode,
103    new: &VNode,
104    parent: &web_sys::Node,
105    anchor: Option<&web_sys::Node>,
106) -> Option<web_sys::Node> {
107    // Remove old DOM node
108    if let Some(old_node) = old.dom_node() {
109        let _ = parent.remove_child(&old_node);
110    }
111
112    // Mount new node (insert at the old node's position if we know it)
113    mount_to_dom(new, parent, anchor)
114}
115
116// ---------------------------------------------------------------------------
117// Patch text node
118// ---------------------------------------------------------------------------
119
120/// Update a text node's content if it changed.
121fn patch_text_node(old_text: &str, new_text: &str, old_vnode: &VNode) -> Option<web_sys::Node> {
122    if let Some(node) = old_vnode.dom_node() {
123        if old_text != new_text {
124            let _ = node.set_text_content(Some(new_text));
125        }
126        Some(node)
127    } else {
128        // No existing DOM node — should not happen, but handle gracefully
129        None
130    }
131}
132
133// ---------------------------------------------------------------------------
134// Patch element
135// ---------------------------------------------------------------------------
136
137/// Patch an old `VElement` into a new one, reusing the existing DOM element.
138fn patch_element(
139    old: &VElement,
140    new: &VElement,
141    _parent: &web_sys::Node,
142    _anchor: Option<&web_sys::Node>,
143) -> web_sys::Node {
144    // Get the existing DOM element from old
145    let dom_elem: web_sys::Element = match *old.dom_ref.borrow() {
146        Some(ref node) => {
147            // Safety: we know it's an Element because we created it that way
148            node.clone().dyn_into::<web_sys::Element>().unwrap_or_else(|_| {
149                // Fallback: create a new element with correct namespace
150                create_element_ns(&new.tag)
151            })
152        }
153        None => {
154            // No existing element — create a new one with correct namespace
155            create_element_ns(&new.tag)
156        }
157    };
158
159    // Patch attributes
160    patch_attributes(old, new, &dom_elem);
161
162    // Patch event listeners
163    patch_event_listeners(old, new, &dom_elem);
164
165    // Update the new VElement's dom_ref to reuse the existing DOM element
166    let node: web_sys::Node = dom_elem.clone().into();
167    *new.dom_ref.borrow_mut() = Some(node.clone());
168
169    // Patch children
170    patch_children(&old.children, &new.children, &node);
171
172    node
173}
174
175/// Create a DOM element with the correct namespace.
176/// Uses SVG namespace for SVG elements, HTML namespace otherwise.
177fn create_element_ns(tag: &str) -> web_sys::Element {
178    let doc = web_sys::window().unwrap().document().unwrap();
179    if tag == "svg" {
180        doc.create_element_ns(Some(SVG_NS), tag)
181    } else {
182        doc.create_element(tag)
183    }
184    .unwrap()
185}
186
187// ---------------------------------------------------------------------------
188// Patch attributes
189// ---------------------------------------------------------------------------
190
191/// Compare old and new attributes, applying only the changes.
192fn patch_attributes(old: &VElement, new: &VElement, el: &web_sys::Element) {
193    // Build lookup maps: name → value
194    let old_map: std::collections::HashMap<&str, &str> =
195        old.attrs.iter().map(|(k, v)| (*k, v.as_str())).collect();
196    let new_map: std::collections::HashMap<&str, &str> =
197        new.attrs.iter().map(|(k, v)| (*k, v.as_str())).collect();
198
199    // Attributes in new but not in old, or with different values → set
200    for (name, new_val) in &new.attrs {
201        match old_map.get(name) {
202            Some(old_val) if *old_val == new_val.as_str() => {
203                // Same value — skip
204            }
205            _ => {
206                let _ = el.set_attribute(name, new_val);
207            }
208        }
209    }
210
211    // Attributes in old but not in new → remove
212    for (name, _) in &old.attrs {
213        if !new_map.contains_key(name) {
214            let _ = el.remove_attribute(name);
215        }
216    }
217}
218
219// ---------------------------------------------------------------------------
220// Patch event listeners
221// ---------------------------------------------------------------------------
222
223/// Compare old and new event listeners, adding/removing as needed.
224fn patch_event_listeners(old: &VElement, new: &VElement, el: &web_sys::Element) {
225    // Build lookup: event_name → handler reference (from old)
226    let old_events: std::collections::HashMap<&str, &EventHandler> =
227        old.events.iter().map(|(k, v)| (*k, v)).collect();
228    let new_events: std::collections::HashMap<&str, &EventHandler> =
229        new.events.iter().map(|(k, v)| (*k, v)).collect();
230
231    // Remove event listeners that are no longer present or have changed
232    let mut closures_to_keep: Vec<(&'static str, Closure<dyn FnMut(web_sys::Event)>)> = Vec::new();
233
234    for (event_name, handler) in &old.events {
235        match new_events.get(event_name) {
236            Some(new_handler) => {
237                // Event still exists — check if handler changed
238                // Since EventHandler stores Rc, we compare ptr equality
239                let changed = !Rc::ptr_eq(&handler.0, &new_handler.0);
240                if changed {
241                    // Remove old listener
242                    if let Some(old_closure) = find_and_remove_closure(&old, event_name) {
243                        let js_func: &js_sys::Function = old_closure.as_ref().unchecked_ref();
244                        let _ = el.remove_event_listener_with_callback(event_name, js_func);
245                        // old_closure dropped here → JS callback released
246                    }
247                    // Add new listener
248                    let new_closure = create_and_add_listener(el, event_name, new_handler);
249                    closures_to_keep.push((event_name, new_closure));
250                } else {
251                    // Same handler — keep the old closure
252                    if let Some(old_closure) = take_closure(&old, event_name) {
253                        closures_to_keep.push((event_name, old_closure));
254                    } else {
255                        // No old closure found — create new one
256                        let new_closure = create_and_add_listener(el, event_name, new_handler);
257                        closures_to_keep.push((event_name, new_closure));
258                    }
259                }
260            }
261            None => {
262                // Event removed — remove old listener
263                if let Some(old_closure) = find_and_remove_closure(&old, event_name) {
264                    let js_func: &js_sys::Function = old_closure.as_ref().unchecked_ref();
265                    let _ = el.remove_event_listener_with_callback(event_name, js_func);
266                    // old_closure dropped here
267                }
268            }
269        }
270    }
271
272    // Add brand-new events (in new but not in old)
273    for (event_name, handler) in &new.events {
274        if !old_events.contains_key(event_name) {
275            let new_closure = create_and_add_listener(el, event_name, handler);
276            closures_to_keep.push((event_name, new_closure));
277        }
278    }
279
280    // Update the new VElement's listener_closures
281    *new.listener_closures.borrow_mut() = closures_to_keep;
282}
283
284/// Find and remove (by taking) a closure from the old VElement's stored list.
285fn take_closure(
286    el: &VElement,
287    event_name: &str,
288) -> Option<Closure<dyn FnMut(web_sys::Event)>> {
289    let mut closures = el.listener_closures.borrow_mut();
290    if let Some(pos) = closures.iter().position(|(name, _)| *name == event_name) {
291        let (_, closure) = closures.remove(pos);
292        Some(closure)
293    } else {
294        None
295    }
296}
297
298/// Find and remove a closure from the old VElement's stored list (dropping it).
299fn find_and_remove_closure(el: &VElement, event_name: &str) -> Option<Closure<dyn FnMut(web_sys::Event)>> {
300    take_closure(el, event_name)
301}
302
303/// Create a Closure for an event handler, add it to the element, and return it.
304fn create_and_add_listener(
305    el: &web_sys::Element,
306    event_name: &str,
307    handler: &EventHandler,
308) -> Closure<dyn FnMut(web_sys::Event)> {
309    let handler = handler.clone();
310    let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
311        handler.call(event);
312    }) as Box<dyn FnMut(web_sys::Event)>);
313
314    let js_func: &js_sys::Function = closure.as_ref().unchecked_ref();
315    let _ = el.add_event_listener_with_callback(event_name, js_func);
316
317    closure
318}