Skip to main content

wal_core/virtual_dom/
vtext.rs

1use web_sys::{Node, Text};
2
3use super::{dom, VNode};
4
5/// Node in virtual DOM tree representing [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text). It undergoes virtual DOM manipulations and patching algorithm optimizations.
6#[derive(PartialEq, Debug)]
7pub struct VText {
8    pub(crate) text: String,
9    pub(crate) dom: Option<Text>,
10}
11
12impl VText {
13    /// Constructor for [VText] out of any object implementing trait [ToString].
14    ///
15    /// # Example
16    ///
17    /// ```
18    /// let vtext1 = VText::new(321);
19    /// let vtext2 = VText::new("321");
20    /// assert_eq!(vtext1, vtext2);
21    /// ```
22    pub fn new<T: ToString>(text: T) -> VText {
23        VText {
24            text: text.to_string(),
25            dom: None,
26        }
27    }
28
29    pub(crate) fn patch(&mut self, last: Option<VNode>, ancestor: &Node) {
30        let mut old_virt: Option<VText> = None;
31        self.dom = None;
32
33        match last {
34            None => {}
35            Some(VNode::Text(vtext)) => {
36                if vtext
37                    .dom
38                    .as_ref()
39                    .is_some_and(|x| x.parent_node().is_some_and(|y| y.eq(ancestor)))
40                {
41                    self.dom = vtext.dom.clone();
42                    old_virt = Some(vtext);
43                }
44            }
45            Some(VNode::Element(v)) => {
46                v.erase();
47            }
48            Some(VNode::Component(v)) => {
49                v.erase();
50            }
51            Some(VNode::List(v)) => {
52                v.erase();
53            }
54        }
55
56        self.render(old_virt, ancestor);
57        self.check_if_parents_match(ancestor);
58    }
59
60    pub(crate) fn erase(&self) {
61        if let Some(text) = &self.dom {
62            dom::remove_node(text);
63        }
64    }
65}
66
67impl VText {
68    fn render(&mut self, last: Option<VText>, ancestor: &Node) {
69        match last {
70            // Different value => just change node value
71            Some(last) if self.text != last.text => {
72                self.dom
73                    .as_ref()
74                    .expect("Dom is not created even though it should have been")
75                    .set_node_value(Some(self.text.as_str()));
76            }
77            // Same thing => do nothing
78            Some(_) => (),
79            None => {
80                let el = dom::create_text_node(&self.text);
81
82                match &self.dom {
83                    Some(old_child) => dom::replace_child(ancestor, old_child, &el),
84                    None => dom::append_child(ancestor, &el),
85                };
86                self.dom = Some(el);
87            }
88        }
89    }
90
91    fn check_if_parents_match(&mut self, ancestor: &Node) {
92        // Corner case when parent is changed but child cannot be reassigned earlier
93        let parent_node = self.dom.as_ref().unwrap().parent_node().unwrap();
94        if !parent_node.eq(ancestor) {
95            let dom_ref = self.dom.as_ref().unwrap();
96            dom::remove_child(&parent_node, dom_ref);
97            dom::append_child(ancestor, dom_ref);
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use wasm_bindgen_test::wasm_bindgen_test;
105
106    use crate::{
107        component::{behavior::Behavior, Component},
108        virtual_dom::{dom, VComponent, VElement, VList, VNode},
109    };
110
111    use super::VText;
112    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
113
114    #[wasm_bindgen_test]
115    fn new_from_str() {
116        let target = VText::new("I love Rust");
117        assert_eq!(target.text, String::from("I love Rust"));
118    }
119
120    macro_rules! function_name {
121        () => {{
122            fn f() {}
123            fn type_name_of<T>(_: T) -> &'static str {
124                std::any::type_name::<T>()
125            }
126            let name = type_name_of(f);
127            name.strip_suffix("::f").unwrap()
128        }};
129    }
130
131    const VALID_TEXT: &str = "";
132
133    #[wasm_bindgen_test]
134    fn patch_last_none() {
135        let ancestor = dom::create_element("div");
136        dom::set_attribute(&ancestor, "id", function_name!());
137        dom::append_child(&dom::get_root_element(), &ancestor);
138
139        let mut target = VText::new(VALID_TEXT);
140        target.patch(None, &ancestor);
141    }
142
143    #[wasm_bindgen_test]
144    fn patch_last_text() {
145        let ancestor = dom::create_element("div");
146        dom::set_attribute(&ancestor, "id", function_name!());
147
148        let current = dom::create_text_node("I dont love Rust");
149        dom::append_child(&ancestor, &current);
150
151        dom::append_child(&dom::get_root_element(), &ancestor);
152
153        let text = VNode::Text(VText {
154            text: "I dont love Rust".into(),
155            dom: Some(current),
156        });
157
158        let mut target = VText::new(VALID_TEXT);
159        target.patch(Some(text), &ancestor);
160    }
161
162    #[wasm_bindgen_test]
163    fn patch_last_elem() {
164        let ancestor = dom::create_element("div");
165        dom::set_attribute(&ancestor, "id", function_name!());
166
167        let current = dom::create_element("div");
168        dom::set_attribute(&current, "id", "I dont love Rust");
169        dom::append_child(&ancestor, &current);
170
171        dom::append_child(&dom::get_root_element(), &ancestor);
172
173        let elem = VNode::Element(VElement {
174            tag_name: "div".into(),
175            attr: [("id".into(), "I dont love Rust".into())].into(),
176            event_handlers: vec![],
177            key: None,
178            children: vec![],
179            dom: Some(current),
180        });
181
182        let mut target = VText::new(VALID_TEXT);
183        target.patch(Some(elem), &ancestor);
184    }
185
186    struct Comp;
187    impl Component for Comp {
188        type Message = ();
189        type Properties = ();
190
191        fn new(_props: Self::Properties) -> Self {
192            Comp
193        }
194        fn view(&self, _behavior: &mut impl Behavior<Self>) -> VNode {
195            VText::new("I dont love Rust").into()
196        }
197        fn update(&mut self, _message: Self::Message) -> bool {
198            false
199        }
200    }
201
202    #[wasm_bindgen_test]
203    fn patch_last_comp() {
204        let ancestor = dom::create_element("div");
205        dom::set_attribute(&ancestor, "id", function_name!());
206        dom::append_child(&dom::get_root_element(), &ancestor);
207
208        let mut comp = VNode::Component(VComponent::new::<Comp>((), None));
209        comp.set_depth(0);
210        comp.patch(None, &ancestor);
211
212        let mut target = VText::new(VALID_TEXT);
213        target.patch(Some(comp), &ancestor);
214    }
215
216    #[wasm_bindgen_test]
217    fn patch_last_list() {
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 list = VNode::List(VList::new(
223            vec![VText::new("I dont love Rust").into()],
224            None,
225        ));
226        list.patch(None, &ancestor);
227
228        let mut target = VText::new(VALID_TEXT);
229        target.patch(Some(list), &ancestor);
230    }
231}