wal_core/virtual_dom/
vcomponent.rs

1use web_sys::Node;
2
3use crate::component::{node::AnyComponentNode, Component};
4
5use std::{
6    any::{Any, TypeId},
7    cell::RefCell,
8    collections::hash_map::DefaultHasher,
9    fmt,
10    hash::{Hash, Hasher},
11    rc::Rc,
12};
13
14use super::VNode;
15
16pub(crate) type PropertiesHash = u64;
17pub(crate) type AnyProps = Option<Box<dyn Any>>;
18pub(crate) type ComponentNodeGenerator =
19    Box<dyn Fn(AnyProps, &Node) -> Rc<RefCell<AnyComponentNode>> + 'static>;
20
21/// Special VNode type, which represents custom component node.
22/// There is no direct translation of [VComponent] to a single DOM node, but it translates to a subtree of DOM nodes.
23pub struct VComponent {
24    props: AnyProps,
25    hash: PropertiesHash,
26    generator: ComponentNodeGenerator,
27    key: Option<String>,
28    depth: Option<u32>,
29
30    pub(crate) comp: Option<Rc<RefCell<AnyComponentNode>>>,
31}
32
33impl VComponent {
34    /// Creates [VComponent] out of provided properties. Function is generic, therefore type of [Component] ***C*** has to be specified.
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// struct ExampleComponent;
40    /// impl Component for ExampleComponent {
41    ///     type Properties = ();
42    ///     ...
43    /// }
44    /// let props = ();
45    /// let vcomp = VComponent::new::<ExampleComponent>(props, None);
46    /// ```
47    pub fn new<C>(props: C::Properties, key: Option<String>) -> VComponent
48    where
49        C: Component + 'static,
50    {
51        let hash = Self::calculate_hash::<C>(&props);
52        let generator = Box::new(Self::generator::<C>);
53        VComponent {
54            props: Some(Box::new(props)),
55            generator,
56            hash,
57            key,
58            depth: None,
59            comp: None,
60        }
61    }
62
63    fn calculate_hash<C>(props: &C::Properties) -> PropertiesHash
64    where
65        C: Component + 'static,
66    {
67        let mut hasher = DefaultHasher::new();
68        props.hash(&mut hasher);
69        TypeId::of::<C>().hash(&mut hasher);
70        hasher.finish()
71    }
72
73    fn generator<C: Component + 'static>(
74        props: AnyProps,
75        ancestor: &Node,
76    ) -> Rc<RefCell<AnyComponentNode>> {
77        let props = props
78            .unwrap()
79            .downcast::<C::Properties>()
80            .expect("Trying to unpack others component properties");
81
82        AnyComponentNode::new(C::new(*props), ancestor.clone())
83    }
84
85    pub(crate) fn patch(&mut self, last: Option<VNode>, ancestor: &Node) {
86        let mut old_virt: Option<VComponent> = None;
87
88        match last {
89            Some(VNode::Component(vcomp)) => {
90                old_virt = Some(vcomp);
91            }
92            Some(VNode::Element(v)) => {
93                v.erase();
94            }
95            Some(VNode::Text(v)) => {
96                v.erase();
97            }
98            None => {}
99            Some(VNode::List(v)) => {
100                v.erase();
101            }
102        }
103
104        self.render(old_virt, ancestor);
105    }
106
107    pub(crate) fn erase(&self) {
108        if let Some(node) = self.comp.as_ref() {
109            node.borrow_mut().vdom.as_ref().unwrap().erase();
110        }
111    }
112
113    pub(crate) fn set_depth(&mut self, depth: u32) {
114        self.depth = Some(depth);
115    }
116
117    fn render(&mut self, last: Option<VComponent>, ancestor: &Node) {
118        match last {
119            Some(mut old_vcomp) if self.key.is_some() && old_vcomp.key == self.key => {
120                self.comp = old_vcomp.comp.take();
121            }
122            Some(mut old_vcomp) if old_vcomp.hash == self.hash => {
123                self.comp = old_vcomp.comp.take();
124            }
125            Some(old_vcomp) => {
126                let any_component_node_rc = (self.generator)(self.props.take(), ancestor);
127                {
128                    let mut any_component_node = any_component_node_rc.borrow_mut();
129                    any_component_node.depth = self.depth;
130                    any_component_node.view();
131                    any_component_node.patch(old_vcomp.comp.clone(), ancestor);
132                }
133                self.comp = Some(any_component_node_rc);
134            }
135            None => {
136                let any_component_node_rc = (self.generator)(self.props.take(), ancestor);
137                {
138                    let mut any_component_node = any_component_node_rc.borrow_mut();
139                    any_component_node.depth = self.depth;
140                    any_component_node.view();
141                    any_component_node.patch(None, ancestor);
142                }
143                self.comp = Some(any_component_node_rc);
144            }
145        }
146    }
147}
148
149impl fmt::Debug for VComponent {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.debug_struct("VComponent")
152            .field("props", &self.props)
153            .field("hash", &self.hash)
154            .field("comp", &self.comp)
155            .finish()
156    }
157}
158
159impl PartialEq for VComponent {
160    fn eq(&self, other: &Self) -> bool {
161        self.hash == other.hash
162            && match (&self.props, &other.props) {
163                (Some(self_props), Some(other_props)) => {
164                    (*(*self_props)).type_id() == (*(*other_props)).type_id()
165                }
166                (None, None) => true,
167                _ => false,
168            }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use wasm_bindgen_test::wasm_bindgen_test;
175
176    use crate::{
177        component::{behavior::Behavior, Component},
178        virtual_dom::{dom, VElement, VList, VNode, VText},
179    };
180
181    use super::VComponent;
182    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
183
184    macro_rules! function_name {
185        () => {{
186            fn f() {}
187            fn type_name_of<T>(_: T) -> &'static str {
188                std::any::type_name::<T>()
189            }
190            let name = type_name_of(f);
191            name.strip_suffix("::f").unwrap()
192        }};
193    }
194
195    const VALID_TEXT: &str = "";
196
197    struct Tmp;
198    impl Component for Tmp {
199        type Message = ();
200        type Properties = ();
201
202        fn new(_props: Self::Properties) -> Self {
203            Tmp
204        }
205        fn view(&self, _behavior: &mut impl Behavior<Self>) -> VNode {
206            VElement::new(
207                "div".into(),
208                [(String::from("result"), String::from(VALID_TEXT))].into(),
209                vec![],
210                None,
211                vec![],
212            )
213            .into()
214        }
215        fn update(&mut self, _message: Self::Message) -> bool {
216            false
217        }
218    }
219
220    #[wasm_bindgen_test]
221    fn patch_last_none() {
222        let ancestor = dom::create_element("div");
223        dom::set_attribute(&ancestor, "id", function_name!());
224        dom::append_child(&dom::get_root_element(), &ancestor);
225
226        let mut target = VComponent::new::<Tmp>((), None);
227        target.set_depth(0);
228        target.patch(None, &ancestor);
229    }
230
231    #[wasm_bindgen_test]
232    fn patch_last_text() {
233        let ancestor = dom::create_element("div");
234        dom::set_attribute(&ancestor, "id", function_name!());
235
236        let current = dom::create_text_node("I dont love Rust");
237        dom::append_child(&ancestor, &current);
238
239        dom::append_child(&dom::get_root_element(), &ancestor);
240
241        let text = VNode::Text(VText {
242            text: "I dont love Rust".into(),
243            dom: Some(current),
244        });
245
246        let mut target = VComponent::new::<Tmp>((), None);
247        target.set_depth(0);
248        target.patch(Some(text), &ancestor);
249    }
250
251    #[wasm_bindgen_test]
252    fn patch_last_elem() {
253        let ancestor = dom::create_element("div");
254        dom::set_attribute(&ancestor, "id", function_name!());
255
256        let current = dom::create_element("div");
257        dom::set_attribute(&current, "id", "I dont love Rust");
258        dom::append_child(&ancestor, &current);
259
260        dom::append_child(&dom::get_root_element(), &ancestor);
261
262        let elem = VNode::Element(VElement {
263            tag_name: "div".into(),
264            attr: [("id".into(), "I dont love Rust".into())].into(),
265            event_handlers: vec![],
266            key: None,
267            children: vec![],
268            dom: Some(current),
269        });
270
271        let mut target = VComponent::new::<Tmp>((), None);
272        target.set_depth(0);
273        target.patch(Some(elem), &ancestor);
274    }
275
276    struct Comp;
277    impl Component for Comp {
278        type Message = ();
279        type Properties = ();
280
281        fn new(_props: Self::Properties) -> Self {
282            Comp
283        }
284        fn view(&self, _behavior: &mut impl Behavior<Self>) -> VNode {
285            VElement::new(
286                "div".into(),
287                [(String::from("result"), String::from("I dont love Rust"))].into(),
288                vec![],
289                None,
290                vec![],
291            )
292            .into()
293        }
294        fn update(&mut self, _message: Self::Message) -> bool {
295            false
296        }
297    }
298
299    #[wasm_bindgen_test]
300    fn patch_last_comp_diff_keys() {
301        let ancestor = dom::create_element("div");
302        dom::set_attribute(&ancestor, "id", function_name!());
303        dom::append_child(&dom::get_root_element(), &ancestor);
304
305        let mut comp = VNode::Component(VComponent::new::<Comp>((), None));
306        comp.set_depth(0);
307        comp.patch(None, &ancestor);
308
309        let mut target = VComponent::new::<Tmp>((), None);
310        target.set_depth(0);
311        target.patch(Some(comp), &ancestor);
312    }
313
314    #[wasm_bindgen_test]
315    fn patch_last_comp_same_keys() {
316        let ancestor = dom::create_element("div");
317        dom::set_attribute(&ancestor, "id", function_name!());
318        dom::append_child(&dom::get_root_element(), &ancestor);
319
320        let key = Some(String::from("Same_key"));
321        let mut comp = VNode::Component(VComponent::new::<Comp>((), key.clone()));
322        comp.set_depth(0);
323        comp.patch(None, &ancestor);
324
325        let mut target = VComponent::new::<Tmp>((), key);
326        target.set_depth(0);
327        target.patch(Some(comp), &ancestor);
328    }
329
330    #[wasm_bindgen_test]
331    fn patch_last_list() {
332        let ancestor = dom::create_element("div");
333        dom::set_attribute(&ancestor, "id", function_name!());
334        dom::append_child(&dom::get_root_element(), &ancestor);
335
336        let mut list = VNode::List(VList::new(
337            vec![VText::new("I dont love Rust").into()],
338            None,
339        ));
340        list.set_depth(0);
341        list.patch(None, &ancestor);
342
343        let mut target = VComponent::new::<Tmp>((), None);
344        target.set_depth(0);
345        target.patch(Some(list), &ancestor);
346    }
347}