Skip to main content

wal_core/virtual_dom/
velement.rs

1use itertools::{EitherOrBoth, Itertools};
2use std::collections::{HashMap, HashSet};
3use web_sys::{Element, Node};
4
5use crate::{events::EventHandler, virtual_dom::dom};
6
7use super::VNode;
8
9/// Represents [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) in DOM.
10#[derive(Debug)]
11pub struct VElement {
12    pub(crate) tag_name: String,
13    pub(crate) attr: HashMap<String, String>,
14    pub(crate) event_handlers: Vec<EventHandler>,
15    pub(crate) key: Option<String>,
16    pub(crate) children: Vec<VNode>,
17
18    pub(crate) dom: Option<Element>,
19}
20
21impl VElement {
22    /// Creates [VElement] out of provided arguments. Optional key is a way of easy comparison, if key is some and keys match, old element is used.
23    ///
24    /// # Example
25    ///
26    /// ```
27    /// let element = VElement::new(
28    ///     String::from("div")                                     // tag_name,
29    ///     [(String::from("id"),String::from("example"))].into(),  // attr,
30    ///     vec![],                                                 // event_handlers,
31    ///     None,                                                   // key,
32    ///     vec![VNode::Text(VText::new("child"))]                  // children
33    /// );
34    /// ```
35    pub fn new(
36        tag_name: String,
37        attr: HashMap<String, String>,
38        event_handlers: Vec<EventHandler>,
39        key: Option<String>,
40        children: Vec<VNode>,
41    ) -> VElement {
42        VElement {
43            tag_name,
44            attr,
45            event_handlers,
46            key,
47            children,
48            dom: None,
49        }
50    }
51
52    pub(crate) fn patch(&mut self, last: Option<VNode>, ancestor: &Node) {
53        let mut old_virt: Option<VElement> = None;
54        self.dom = None;
55
56        match last {
57            None => {}
58            Some(VNode::Element(mut velement)) => {
59                if velement
60                    .dom
61                    .as_ref()
62                    .is_some_and(|x| x.parent_node().is_some_and(|y| y.eq(ancestor)))
63                {
64                    self.dom = velement.dom.take();
65                    old_virt = Some(velement);
66                }
67            }
68            Some(VNode::Text(v)) => {
69                v.erase();
70            }
71            Some(VNode::Component(v)) => {
72                v.erase();
73            }
74            Some(VNode::List(v)) => {
75                v.erase();
76            }
77        }
78
79        self.render(old_virt.as_ref(), ancestor);
80        self.handle_children(old_virt);
81        self.check_if_parents_match(ancestor);
82    }
83
84    pub(crate) fn erase(&self) {
85        if let Some(el) = &self.dom {
86            dom::remove_node(el);
87        }
88    }
89
90    pub(crate) fn set_depth(&mut self, depth: u32) {
91        for child in self.children.iter_mut() {
92            child.set_depth(depth);
93        }
94    }
95}
96
97impl VElement {
98    fn render(&mut self, last: Option<&VElement>, ancestor: &Node) {
99        match last {
100            // comparison over user-defined key, if match dont do anything
101            Some(last) if last.key.is_some() && last.key == self.key => {
102                dom::append_child(ancestor, self.dom.as_ref().unwrap());
103            }
104
105            Some(last) if last.tag_name == self.tag_name => {
106                let target = self
107                    .dom
108                    .as_mut()
109                    .expect("Target dom object not created before rendering element");
110                // Compare attributes
111                for (key, val) in self.attr.iter() {
112                    dom::set_attribute(target, key, val);
113                }
114                for (key, _val) in last.attr.iter() {
115                    if !self.attr.contains_key(key) {
116                        dom::remove_attribute(target, key);
117                    }
118                }
119
120                for event_handler in &mut self.event_handlers {
121                    event_handler.attach(target);
122                }
123            }
124            _ => {
125                // inverted check, if last == None || last = Some(x) that x.tag_name !=
126                // self.tag_name => Swap whole element
127                let el = dom::create_element(&self.tag_name);
128
129                // add attributes
130                for (name, value) in self.attr.iter() {
131                    dom::set_attribute(&el, name, value);
132                }
133
134                for event_handler in &mut self.event_handlers {
135                    event_handler.attach(&el);
136                }
137
138                match &self.dom {
139                    Some(old_child) => dom::replace_child(ancestor, old_child, &el),
140                    None => dom::append_child(ancestor, &el),
141                };
142                self.dom = Some(el);
143            }
144        }
145    }
146
147    fn handle_children(&mut self, old_element: Option<VElement>) {
148        let target = self.dom.as_mut().unwrap();
149        let old_children = old_element.map_or(Vec::new(), |e| e.children.into_iter().collect());
150
151        for either_child_or_both in self.children.iter_mut().zip_longest(old_children) {
152            match either_child_or_both {
153                EitherOrBoth::Both(child, old_child) => {
154                    child.patch(Some(old_child), target);
155                }
156                EitherOrBoth::Left(child) => {
157                    child.patch(None, target);
158                }
159                EitherOrBoth::Right(old_child) => {
160                    // child doesnt exist anymore
161                    old_child.erase();
162                }
163            }
164        }
165    }
166
167    fn check_if_parents_match(&mut self, ancestor: &Node) {
168        // Corner case when parent is changed but child cannot be reassigned earlier
169        let parent_node = self.dom.as_ref().unwrap().parent_node().unwrap();
170        if !parent_node.eq(ancestor) {
171            let dom_ref = self.dom.as_ref().unwrap();
172            dom::remove_child(&parent_node, dom_ref);
173            dom::append_child(ancestor, dom_ref);
174        }
175    }
176}
177
178impl PartialEq for VElement {
179    fn eq(&self, other: &Self) -> bool {
180        let self_event_handlers: HashSet<_> = self.event_handlers.iter().collect();
181        let other_event_handlers: HashSet<_> = other.event_handlers.iter().collect();
182
183        self.tag_name == other.tag_name
184            && self.attr == other.attr
185            && self.children == other.children
186            && self.dom == other.dom
187            && self_event_handlers == other_event_handlers
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use wasm_bindgen_test::wasm_bindgen_test;
194
195    use crate::{
196        component::{behavior::Behavior, Component},
197        virtual_dom::{dom, VComponent, VList, VNode, VText},
198    };
199
200    use super::VElement;
201    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
202
203    macro_rules! function_name {
204        () => {{
205            fn f() {}
206            fn type_name_of<T>(_: T) -> &'static str {
207                std::any::type_name::<T>()
208            }
209            let name = type_name_of(f);
210            name.strip_suffix("::f").unwrap()
211        }};
212    }
213
214    const VALID_TEXT: &str = "";
215
216    #[wasm_bindgen_test]
217    fn patch_last_none() {
218        let ancestor = dom::create_element("div");
219        dom::set_attribute(&ancestor, "id", function_name!());
220        dom::append_child(&dom::get_root_element(), &ancestor);
221
222        let mut target = VElement::new(
223            "div".into(),
224            [("id".into(), VALID_TEXT.into())].into(),
225            vec![],
226            None,
227            vec![],
228        );
229        target.patch(None, &ancestor);
230    }
231
232    #[wasm_bindgen_test]
233    fn patch_last_text() {
234        let ancestor = dom::create_element("div");
235        dom::set_attribute(&ancestor, "id", function_name!());
236
237        let current = dom::create_text_node("I dont love Rust");
238        dom::append_child(&ancestor, &current);
239
240        dom::append_child(&dom::get_root_element(), &ancestor);
241
242        let text = VNode::Text(VText {
243            text: "I dont love Rust".into(),
244            dom: Some(current),
245        });
246
247        let mut target = VElement::new(
248            "div".into(),
249            [("id".into(), VALID_TEXT.into())].into(),
250            vec![],
251            None,
252            vec![],
253        );
254        target.patch(Some(text), &ancestor);
255    }
256
257    #[wasm_bindgen_test]
258    fn patch_last_elem_different_key() {
259        let ancestor = dom::create_element("div");
260        dom::set_attribute(&ancestor, "id", function_name!());
261
262        let current = dom::create_element("div");
263        dom::set_attribute(&current, "id", "I dont love Rust");
264        dom::append_child(&ancestor, &current);
265
266        dom::append_child(&dom::get_root_element(), &ancestor);
267
268        let elem = VNode::Element(VElement {
269            tag_name: "div".into(),
270            attr: [("id".into(), "I dont love Rust".into())].into(),
271            event_handlers: vec![],
272            key: None,
273            children: vec![],
274            dom: Some(current),
275        });
276
277        let mut target = VElement::new(
278            "div".into(),
279            [("id".into(), VALID_TEXT.into())].into(),
280            vec![],
281            Some("Different_key".to_string()),
282            vec![],
283        );
284        target.patch(Some(elem), &ancestor);
285    }
286
287    #[wasm_bindgen_test]
288    fn patch_last_elem_same_key() {
289        let ancestor = dom::create_element("div");
290        dom::set_attribute(&ancestor, "id", function_name!());
291
292        let current = dom::create_element("div");
293        dom::set_attribute(&current, "id", "I dont love Rust");
294        dom::append_child(&ancestor, &current);
295
296        dom::append_child(&dom::get_root_element(), &ancestor);
297
298        let key = Some("Nice".to_string());
299
300        let elem = VNode::Element(VElement {
301            tag_name: "div".into(),
302            attr: [("id".into(), "I dont love Rust".into())].into(),
303            event_handlers: vec![],
304            key: key.clone(),
305            children: vec![],
306            dom: Some(current),
307        });
308
309        let mut target = VElement::new(
310            "div".into(),
311            [("id".into(), VALID_TEXT.into())].into(),
312            vec![],
313            key,
314            vec![],
315        );
316        target.patch(Some(elem), &ancestor);
317    }
318
319    struct Comp;
320    impl Component for Comp {
321        type Message = ();
322        type Properties = ();
323
324        fn new(_props: Self::Properties) -> Self {
325            Comp
326        }
327        fn view(&self, _behavior: &mut impl Behavior<Self>) -> VNode {
328            VText::new("I dont love Rust").into()
329        }
330        fn update(&mut self, _message: Self::Message) -> bool {
331            false
332        }
333    }
334
335    #[wasm_bindgen_test]
336    fn patch_last_comp() {
337        let ancestor = dom::create_element("div");
338        dom::set_attribute(&ancestor, "id", function_name!());
339        dom::append_child(&dom::get_root_element(), &ancestor);
340
341        let mut comp = VNode::Component(VComponent::new::<Comp>((), None));
342        comp.set_depth(0);
343        comp.patch(None, &ancestor);
344
345        let mut target = VElement::new(
346            "div".into(),
347            [("id".into(), VALID_TEXT.into())].into(),
348            vec![],
349            None,
350            vec![],
351        );
352        target.patch(Some(comp), &ancestor);
353    }
354
355    #[wasm_bindgen_test]
356    fn patch_last_list() {
357        let ancestor = dom::create_element("div");
358        dom::set_attribute(&ancestor, "id", function_name!());
359        dom::append_child(&dom::get_root_element(), &ancestor);
360
361        let mut list = VNode::List(VList::new(
362            vec![VText::new("I dont love Rust").into()],
363            None,
364        ));
365        list.set_depth(0);
366        list.patch(None, &ancestor);
367
368        let mut target = VElement::new(
369            "div".into(),
370            [("id".into(), VALID_TEXT.into())].into(),
371            vec![],
372            None,
373            vec![],
374        );
375        target.patch(Some(list), &ancestor);
376    }
377}