sauron_core/dom/
dom_patch.rs

1use std::{cell::RefCell, rc::Rc};
2
3use indexmap::IndexMap;
4use wasm_bindgen::closure::Closure;
5use wasm_bindgen::JsValue;
6
7use crate::{
8    dom::{
9        self, dom_node, dom_node::DomInner, Application, DomAttr, DomAttrValue, DomNode, Program,
10    },
11    vdom::{
12        Attribute, AttributeValue, ComponentEventCallback, EventCallback, Patch, PatchType,
13        TreePath,
14    },
15};
16
17/// a Patch where the virtual nodes are all created in the document.
18/// This is necessary since the created Node  doesn't contain references
19/// as opposed to Patch which contains reference to the vdom, which makes it hard
20/// to be included in a struct
21#[derive(Debug)]
22pub struct DomPatch {
23    /// The path to traverse to get to the target_element
24    pub patch_path: TreePath,
25    /// the target node
26    pub target_element: DomNode,
27    /// the parent element of the target node
28    pub target_parent: DomNode,
29    /// the patch variant
30    pub patch_variant: PatchVariant,
31}
32
33/// patch variant
34#[derive(Debug)]
35pub enum PatchVariant {
36    /// Insert nodes before the target node
37    InsertBeforeNode {
38        /// nodes to be inserted before the target node
39        nodes: Vec<DomNode>,
40    },
41    /// Insert nodes after the target node
42    InsertAfterNode {
43        /// the nodes to be inserted after the target node
44        nodes: Vec<DomNode>,
45    },
46    /// Append nodes into the target node
47    AppendChildren {
48        /// the children nodes to be appended into the target node
49        children: Vec<DomNode>,
50    },
51    /// Add attributes to the target node
52    AddAttributes {
53        /// the attributes to be added to the target node
54        attrs: Vec<DomAttr>,
55    },
56    /// Remove attributes from the target node
57    RemoveAttributes {
58        /// the attributes names to be removed
59        attrs: Vec<DomAttr>,
60    },
61    /// Replace the target node with the replacement node
62    ReplaceNode {
63        /// the replacement node
64        replacement: Vec<DomNode>,
65    },
66    /// Remove the target node
67    RemoveNode,
68    /// Clear the children of the target node
69    ClearChildren,
70    /// Move the target node before the node specified in the path location
71    MoveBeforeNode {
72        /// before the node at this location
73        for_moving: Vec<DomNode>,
74    },
75    /// Move the target node after the node specified in the path location
76    MoveAfterNode {
77        /// after the node at this location
78        for_moving: Vec<DomNode>,
79    },
80}
81
82impl DomNode {
83    pub(crate) fn find_node(&self, path: &mut TreePath) -> Option<DomNode> {
84        match &self.inner {
85            DomInner::StatefulComponent { .. } => {
86                log::info!(
87                    "This is a stateful component, should return the element
88                inside relative to the child container at this path: {:?}",
89                    path
90                );
91                // just return self and handle its own patches
92                Some(self.clone())
93            }
94            _ => {
95                if path.is_empty() {
96                    Some(self.clone())
97                } else {
98                    let idx = path.remove_first();
99                    if let Some(children) = self.children() {
100                        if let Some(child) = children.get(idx) {
101                            child.find_node(path)
102                        } else {
103                            log::warn!("There is no child at index: {idx}");
104                            None
105                        }
106                    } else {
107                        log::warn!("Traversing to a childless node..");
108                        None
109                    }
110                }
111            }
112        }
113    }
114
115    pub(crate) fn find_all_nodes(
116        &self,
117        nodes_to_find: &[(&TreePath, Option<&&'static str>)],
118    ) -> IndexMap<TreePath, (DomNode, DomNode)> {
119        let mut nodes_to_patch = IndexMap::with_capacity(nodes_to_find.len());
120        for (path, tag) in nodes_to_find {
121            let mut traverse_path: TreePath = (*path).clone();
122            if let Some(found) = self.find_node(&mut traverse_path) {
123                let mut parent_path = path.backtrack();
124                let target_parent = self
125                    .find_node(&mut parent_path)
126                    .expect("must find the parent");
127                nodes_to_patch.insert((*path).clone(), (found, target_parent));
128            } else {
129                log::warn!(
130                    "can not find: {:?} {:?} target_node: {:?}",
131                    path,
132                    tag,
133                    &self
134                );
135                log::info!(
136                    "real entire dom: {:#?}",
137                    dom_node::render_real_dom_to_string(&self.as_node())
138                );
139                log::warn!("entire dom: {}", self.render_to_string());
140            }
141        }
142        nodes_to_patch
143    }
144}
145
146impl<APP> Program<APP>
147where
148    APP: Application + 'static,
149{
150    /// get the real DOM target node and make a DomPatch object for each of the Patch
151    pub(crate) fn convert_patches(
152        &self,
153        target_node: &DomNode,
154        patches: &[Patch<APP::MSG>],
155    ) -> Result<Vec<DomPatch>, JsValue> {
156        convert_patches(target_node, patches, self.create_ev_callback())
157    }
158
159    /// convert a virtual DOM Patch into a created DOM node Patch
160    pub fn convert_patch(
161        &self,
162        nodes_lookup: &IndexMap<TreePath, (DomNode, DomNode)>,
163        target_element: &DomNode,
164        target_parent: &DomNode,
165        patch: &Patch<APP::MSG>,
166    ) -> DomPatch {
167        convert_patch(
168            nodes_lookup,
169            target_element,
170            target_parent,
171            patch,
172            self.create_ev_callback(),
173        )
174    }
175}
176
177/// get the real DOM target node and make a DomPatch object for each of the Patch
178pub fn convert_patches<Msg, F>(
179    target_node: &DomNode,
180    patches: &[Patch<Msg>],
181    ev_callback: F,
182) -> Result<Vec<DomPatch>, JsValue>
183where
184    Msg: 'static,
185    F: Fn(Msg) + 'static + Clone,
186{
187    let nodes_to_find: Vec<(&TreePath, Option<&&'static str>)> = patches
188        .iter()
189        .map(|patch| (patch.path(), patch.tag()))
190        .chain(
191            patches
192                .iter()
193                .flat_map(|patch| patch.node_paths())
194                .map(|path| (path, None)),
195        )
196        .collect();
197
198    let nodes_lookup = target_node.find_all_nodes(&nodes_to_find);
199
200    let dom_patches:Vec<DomPatch> = patches.iter().map(|patch|{
201        let patch_path = patch.path();
202        let patch_tag = patch.tag();
203        if let Some((target_node, target_parent)) = nodes_lookup.get(patch_path) {
204            let target_tag = target_node.tag();
205            if let (Some(patch_tag), Some(target_tag)) = (patch_tag, target_tag) {
206                if **patch_tag != target_tag{
207                    panic!(
208                        "expecting a tag: {patch_tag:?}, but found: {target_tag:?}"
209                    );
210                }
211            }
212            convert_patch(&nodes_lookup, target_node, target_parent, patch, ev_callback.clone())
213        } else {
214            unreachable!("Getting here means we didn't find the element of next node that we are supposed to patch, patch_path: {:?}, with tag: {:?}", patch_path, patch_tag);
215        }
216    }).collect();
217
218    Ok(dom_patches)
219}
220
221/// convert a virtual DOM Patch into a created DOM node Patch
222pub fn convert_patch<Msg, F>(
223    nodes_lookup: &IndexMap<TreePath, (DomNode, DomNode)>,
224    target_element: &DomNode,
225    target_parent: &DomNode,
226    patch: &Patch<Msg>,
227    ev_callback: F,
228) -> DomPatch
229where
230    Msg: 'static,
231    F: Fn(Msg) + 'static + Clone,
232{
233    let target_element = target_element.clone();
234    let target_parent = target_parent.clone();
235    let Patch {
236        patch_path,
237        patch_type,
238        ..
239    } = patch;
240
241    let patch_path = patch_path.clone();
242
243    match patch_type {
244        PatchType::InsertBeforeNode { nodes } => {
245            let nodes = nodes
246                .iter()
247                .map(|for_insert| dom::create_dom_node(for_insert, ev_callback.clone()))
248                .collect();
249            DomPatch {
250                patch_path,
251                target_element,
252                target_parent,
253                patch_variant: PatchVariant::InsertBeforeNode { nodes },
254            }
255        }
256        PatchType::InsertAfterNode { nodes } => {
257            let nodes = nodes
258                .iter()
259                .map(|for_insert| dom::create_dom_node(for_insert, ev_callback.clone()))
260                .collect();
261            DomPatch {
262                patch_path,
263                target_element,
264                target_parent,
265                patch_variant: PatchVariant::InsertAfterNode { nodes },
266            }
267        }
268
269        PatchType::AddAttributes { attrs } => {
270            // we merge the attributes here prior to conversion
271            let attrs = Attribute::merge_attributes_of_same_name(attrs.iter().copied());
272            DomPatch {
273                patch_path,
274                target_element,
275                target_parent,
276                patch_variant: PatchVariant::AddAttributes {
277                    attrs: attrs
278                        .iter()
279                        .map(|a| convert_attr(a, ev_callback.clone()))
280                        .collect(),
281                },
282            }
283        }
284        PatchType::RemoveAttributes { attrs } => DomPatch {
285            patch_path,
286            target_element,
287            target_parent,
288            patch_variant: PatchVariant::RemoveAttributes {
289                attrs: attrs
290                    .iter()
291                    .map(|a| convert_attr(a, ev_callback.clone()))
292                    .collect(),
293            },
294        },
295
296        PatchType::ReplaceNode { replacement } => {
297            let replacement = replacement
298                .iter()
299                .map(|node| dom::create_dom_node(node, ev_callback.clone()))
300                .collect();
301            DomPatch {
302                patch_path,
303                target_element,
304                target_parent,
305                patch_variant: PatchVariant::ReplaceNode { replacement },
306            }
307        }
308        PatchType::RemoveNode => DomPatch {
309            patch_path,
310            target_element,
311            target_parent,
312            patch_variant: PatchVariant::RemoveNode,
313        },
314        PatchType::ClearChildren => DomPatch {
315            patch_path,
316            target_element,
317            target_parent,
318            patch_variant: PatchVariant::ClearChildren,
319        },
320        PatchType::MoveBeforeNode { nodes_path } => {
321            let for_moving = nodes_path
322                .iter()
323                .map(|path| {
324                    let (node, _) = nodes_lookup.get(path).expect("must have found the node");
325                    node.clone()
326                })
327                .collect();
328            DomPatch {
329                patch_path,
330                target_element,
331                target_parent,
332                patch_variant: PatchVariant::MoveBeforeNode { for_moving },
333            }
334        }
335        PatchType::MoveAfterNode { nodes_path } => {
336            let for_moving = nodes_path
337                .iter()
338                .map(|path| {
339                    let (node, _) = nodes_lookup.get(path).expect("must have found the node");
340                    node.clone()
341                })
342                .collect();
343            DomPatch {
344                patch_path,
345                target_element,
346                target_parent,
347                patch_variant: PatchVariant::MoveAfterNode { for_moving },
348            }
349        }
350        PatchType::AppendChildren { children } => {
351            let children = children
352                .iter()
353                .map(|for_insert| dom::create_dom_node(for_insert, ev_callback.clone()))
354                .collect();
355
356            DomPatch {
357                patch_path,
358                target_element,
359                target_parent,
360                patch_variant: PatchVariant::AppendChildren { children },
361            }
362        }
363    }
364}
365
366pub(crate) fn convert_attr<Msg, F>(attr: &Attribute<Msg>, ev_callback: F) -> DomAttr
367where
368    Msg: 'static,
369    F: Fn(Msg) + 'static + Clone,
370{
371    DomAttr {
372        namespace: attr.namespace,
373        name: attr.name,
374        value: attr
375            .value
376            .iter()
377            .filter_map(|v| convert_attr_value(v, ev_callback.clone()))
378            .collect(),
379    }
380}
381
382fn convert_attr_value<Msg, F>(
383    attr_value: &AttributeValue<Msg>,
384    ev_callback: F,
385) -> Option<DomAttrValue>
386where
387    Msg: 'static,
388    F: Fn(Msg) + 'static,
389{
390    match attr_value {
391        AttributeValue::Simple(v) => Some(DomAttrValue::Simple(v.clone())),
392        AttributeValue::Style(v) => Some(DomAttrValue::Style(v.clone())),
393        AttributeValue::EventListener(v) => Some(DomAttrValue::EventListener(
394            convert_event_listener(v, ev_callback),
395        )),
396        AttributeValue::ComponentEventListener(v) => Some(DomAttrValue::EventListener(
397            convert_component_event_listener(v),
398        )),
399        AttributeValue::Empty => None,
400    }
401}
402
403fn convert_event_listener<F, Msg>(
404    event_listener: &EventCallback<Msg>,
405    callback: F,
406) -> Closure<dyn FnMut(web_sys::Event)>
407where
408    Msg: 'static,
409    F: Fn(Msg) + 'static,
410{
411    let event_listener = event_listener.clone();
412    let closure: Closure<dyn FnMut(web_sys::Event)> = Closure::new(move |event: web_sys::Event| {
413        let msg = event_listener.emit(dom::Event::from(event));
414        callback(msg);
415    });
416    closure
417}
418
419/// TODO: this should not have access to root_node, so it can generically
420/// apply patch to any dom node
421pub fn apply_dom_patches(
422    root_node: Rc<RefCell<Option<DomNode>>>,
423    mount_node: Rc<RefCell<Option<DomNode>>>,
424    dom_patches: impl IntoIterator<Item = DomPatch>,
425) -> Result<(), JsValue> {
426    for dom_patch in dom_patches {
427        apply_dom_patch(Rc::clone(&root_node), Rc::clone(&mount_node), dom_patch)?;
428    }
429    Ok(())
430}
431
432/// apply a dom patch to this root node,
433/// return a new root_node if it would replace the original root_node
434/// TODO: this should have no access to root_node, so it can be used in general sense
435pub(crate) fn apply_dom_patch(
436    root_node: Rc<RefCell<Option<DomNode>>>,
437    mount_node: Rc<RefCell<Option<DomNode>>>,
438    dom_patch: DomPatch,
439) -> Result<(), JsValue> {
440    let DomPatch {
441        patch_path,
442        target_element,
443        target_parent,
444        patch_variant,
445    } = dom_patch;
446
447    match patch_variant {
448        PatchVariant::InsertBeforeNode { nodes } => {
449            target_parent.insert_before(&target_element, nodes);
450        }
451
452        PatchVariant::InsertAfterNode { nodes } => {
453            target_parent.insert_after(&target_element, nodes);
454        }
455        PatchVariant::AppendChildren { children } => {
456            target_element.append_children(children);
457        }
458
459        PatchVariant::AddAttributes { attrs } => {
460            target_element.set_dom_attrs(attrs).unwrap();
461        }
462        PatchVariant::RemoveAttributes { attrs } => {
463            for attr in attrs.iter() {
464                for att_value in attr.value.iter() {
465                    match att_value {
466                        DomAttrValue::Simple(_) => {
467                            target_element.remove_dom_attr(attr)?;
468                        }
469                        // it is an event listener
470                        DomAttrValue::EventListener(_) => {
471                            let DomInner::Element { listeners, .. } = &target_element.inner else {
472                                unreachable!("must be an element");
473                            };
474                            if let Some(listener) = listeners.borrow_mut().as_mut() {
475                                listener.retain(|event, _| *event != attr.name)
476                            }
477                        }
478                        DomAttrValue::Style(_) => {
479                            target_element.remove_dom_attr(attr)?;
480                        }
481                        DomAttrValue::Empty => (),
482                    }
483                }
484            }
485        }
486
487        // This also removes the associated closures and event listeners to the node being replaced
488        // including the associated closures of the descendant of replaced node
489        // before it is actully replaced in the DOM
490        // TODO: make root node a Vec
491        PatchVariant::ReplaceNode { mut replacement } => {
492            let first_node = replacement.remove(0);
493
494            if target_element.is_fragment() {
495                assert!(
496                    patch_path.is_empty(),
497                    "this should only happen to root node"
498                );
499                let mut mount_node = mount_node.borrow_mut();
500                let mount_node = mount_node.as_mut().expect("must have a mount node");
501                mount_node.append_children(vec![first_node.clone()]);
502                mount_node.append_children(replacement);
503            } else {
504                if patch_path.path.is_empty() {
505                    let mut mount_node = mount_node.borrow_mut();
506                    let mount_node = mount_node.as_mut().expect("must have a mount node");
507                    mount_node.replace_child(&target_element, first_node.clone());
508                } else {
509                    target_parent.replace_child(&target_element, first_node.clone());
510                }
511                //insert the rest
512                target_parent.insert_after(&first_node, replacement);
513            }
514            if patch_path.path.is_empty() {
515                *root_node.borrow_mut() = Some(first_node);
516            }
517        }
518        PatchVariant::RemoveNode => {
519            target_parent.remove_children(&[&target_element]);
520        }
521        PatchVariant::ClearChildren => {
522            target_element.clear_children();
523        }
524        PatchVariant::MoveBeforeNode { for_moving } => {
525            target_parent.remove_children(&for_moving.iter().collect::<Vec<_>>());
526            target_parent.insert_before(&target_element, for_moving);
527        }
528
529        PatchVariant::MoveAfterNode { for_moving } => {
530            target_parent.remove_children(&for_moving.iter().collect::<Vec<_>>());
531            target_parent.insert_after(&target_element, for_moving);
532        }
533    }
534    Ok(())
535}
536
537fn convert_component_event_listener(
538    component_callback: &ComponentEventCallback,
539) -> Closure<dyn FnMut(web_sys::Event)> {
540    let component_callback = component_callback.clone();
541    let closure: Closure<dyn FnMut(web_sys::Event)> = Closure::new(move |event: web_sys::Event| {
542        component_callback.emit(dom::Event::from(event));
543    });
544    closure
545}