yew_stdweb/virtual_dom/
vtag.rs

1//! This module contains the implementation of a virtual element node `VTag`.
2
3use super::{
4    AttrValue, Attributes, Key, Listener, Listeners, Patch, PositionalAttr, VDiff, VList, VNode,
5};
6use crate::html::{AnyScope, IntoOptPropValue, IntoPropValue, NodeRef};
7use crate::utils::document;
8use cfg_if::cfg_if;
9use cfg_match::cfg_match;
10use log::warn;
11use std::borrow::Cow;
12use std::cmp::PartialEq;
13use std::rc::Rc;
14cfg_if! {
15    if #[cfg(feature = "std_web")] {
16        use crate::html::EventListener;
17        #[allow(unused_imports)]
18        use stdweb::{_js_impl, js};
19        use stdweb::unstable::TryFrom;
20        use stdweb::web::html_element::{InputElement, TextAreaElement};
21        use stdweb::web::{Element, IElement, INode};
22    } else if #[cfg(feature = "web_sys")] {
23        use gloo::events::EventListener;
24        use std::ops::Deref;
25        use wasm_bindgen::JsCast;
26        use web_sys::{
27            Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement, HtmlButtonElement
28        };
29    }
30}
31
32/// SVG namespace string used for creating svg elements
33pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
34
35/// Default namespace for html elements
36pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
37
38/// Used to improve performance of runtime element checks
39#[derive(Clone, Copy, Debug, PartialEq)]
40enum ElementType {
41    Input,
42    Textarea,
43    Button,
44    Other,
45}
46
47impl ElementType {
48    fn from_tag(tag: &str) -> Self {
49        match tag.to_ascii_lowercase().as_str() {
50            "input" => Self::Input,
51            "textarea" => Self::Textarea,
52            "button" => Self::Button,
53            _ => Self::Other,
54        }
55    }
56}
57
58/// A type for a virtual
59/// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)
60/// representation.
61#[derive(Debug)]
62pub struct VTag {
63    /// A tag of the element.
64    tag: Cow<'static, str>,
65    /// Type of element.
66    element_type: ElementType,
67    /// A reference to the DOM `Element`.
68    pub reference: Option<Element>,
69    /// List of attached listeners.
70    pub listeners: Listeners,
71    /// List of attributes.
72    pub attributes: Attributes,
73    /// List of children nodes
74    pub children: VList,
75    /// Contains a value of an
76    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
77    pub value: Option<AttrValue>,
78    /// Contains
79    /// [kind](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types)
80    /// value of an `InputElement`.
81    pub kind: Option<AttrValue>,
82    /// Represents `checked` attribute of
83    /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked).
84    /// It exists to override standard behavior of `checked` attribute, because
85    /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive
86    /// frameworks it's more useful to control `checked` value of an `InputElement`.
87    pub checked: bool,
88    /// A node reference used for DOM access in Component lifecycle methods
89    pub node_ref: NodeRef,
90    /// Keeps handler for attached listeners to have an opportunity to drop them later.
91    captured: Vec<EventListener>,
92
93    pub key: Option<Key>,
94}
95
96impl Clone for VTag {
97    fn clone(&self) -> Self {
98        VTag {
99            tag: self.tag.clone(),
100            element_type: self.element_type,
101            reference: None,
102            listeners: self.listeners.clone(),
103            attributes: self.attributes.clone(),
104            children: self.children.clone(),
105            value: self.value.clone(),
106            kind: self.kind.clone(),
107            checked: self.checked,
108            node_ref: self.node_ref.clone(),
109            key: self.key.clone(),
110            captured: Vec::new(),
111        }
112    }
113}
114
115impl VTag {
116    /// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM).
117    pub fn new(tag: impl Into<Cow<'static, str>>) -> Self {
118        let tag = tag.into();
119        let element_type = ElementType::from_tag(&tag);
120        VTag {
121            tag,
122            element_type,
123            reference: None,
124            attributes: Attributes::new(),
125            listeners: Vec::new(),
126            captured: Vec::new(),
127            children: VList::new(),
128            node_ref: NodeRef::default(),
129            key: None,
130            value: None,
131            kind: None,
132            // In HTML node `checked` attribute sets `defaultChecked` parameter,
133            // but we use own field to control real `checked` parameter
134            checked: false,
135        }
136    }
137
138    /// Returns tag of an `Element`. In HTML tags are always uppercase.
139    pub fn tag(&self) -> &str {
140        &self.tag
141    }
142
143    /// Add `VNode` child.
144    pub fn add_child(&mut self, child: VNode) {
145        self.children.add_child(child);
146    }
147
148    /// Add multiple `VNode` children.
149    pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) {
150        self.children.add_children(children);
151    }
152
153    /// Sets `value` for an
154    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
155    pub fn set_value(&mut self, value: impl IntoOptPropValue<AttrValue>) {
156        self.value = value.into_opt_prop_value();
157    }
158
159    /// Sets `kind` property of an
160    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
161    /// Same as set `type` attribute.
162    pub fn set_kind(&mut self, value: impl IntoOptPropValue<AttrValue>) {
163        self.kind = value.into_opt_prop_value();
164    }
165
166    #[doc(hidden)]
167    pub fn __macro_set_key(&mut self, value: impl Into<Key>) {
168        self.key = Some(value.into())
169    }
170
171    /// Sets `checked` property of an
172    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
173    /// (Not a value of node's attribute).
174    pub fn set_checked(&mut self, value: bool) {
175        self.checked = value;
176    }
177
178    #[doc(hidden)]
179    pub fn __macro_set_node_ref(&mut self, value: impl IntoPropValue<NodeRef>) {
180        self.node_ref = value.into_prop_value()
181    }
182
183    /// Adds a key-value pair to attributes
184    ///
185    /// Not every attribute works when it set as an attribute. We use workarounds for:
186    /// `type/kind`, `value` and `checked`.
187    pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
188        self.attributes
189            .get_mut_index_map()
190            .insert(key, value.into());
191    }
192
193    /// Sets attributes to a virtual node.
194    ///
195    /// Not every attribute works when it set as an attribute. We use workarounds for:
196    /// `type/kind`, `value` and `checked`.
197    pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
198        self.attributes = attrs.into();
199    }
200
201    #[doc(hidden)]
202    pub fn __macro_push_attr(&mut self, attr: PositionalAttr) {
203        match &mut self.attributes {
204            Attributes::Vec(attrs) => attrs.push(attr),
205            _ => unreachable!("the macro always uses positional attributes"),
206        }
207    }
208
209    /// Adds new listener to the node.
210    /// It's boxed because we want to keep it in a single list.
211    /// Later `Listener::attach` will attach an actual listener to a DOM node.
212    pub fn add_listener(&mut self, listener: Rc<dyn Listener>) {
213        self.listeners.push(listener);
214    }
215
216    /// Adds new listeners to the node.
217    /// They are boxed because we want to keep them in a single list.
218    /// Later `Listener::attach` will attach an actual listener to a DOM node.
219    pub fn add_listeners(&mut self, listeners: Listeners) {
220        self.listeners.extend(listeners);
221    }
222
223    #[doc(hidden)]
224    pub fn __macro_set_listeners(
225        &mut self,
226        listeners: impl IntoIterator<Item = Option<Rc<dyn Listener>>>,
227    ) {
228        self.listeners = listeners.into_iter().flatten().collect();
229    }
230
231    /// Every render it removes all listeners and attach it back later
232    /// TODO(#943): Compare references of handler to do listeners update better
233    fn recreate_listeners(&mut self, ancestor: &mut Option<Box<Self>>) {
234        if let Some(ancestor) = ancestor.as_mut() {
235            ancestor.captured.clear();
236        }
237
238        let element = self.reference.clone().expect("element expected");
239
240        for listener in self.listeners.drain(..) {
241            let handle = listener.attach(&element);
242            self.captured.push(handle);
243        }
244    }
245
246    fn refresh_value(&mut self) {
247        // Don't refresh value if the element is not controlled
248        if self.value.is_none() {
249            return;
250        }
251
252        if let Some(element) = self.reference.as_ref() {
253            if self.element_type == ElementType::Input {
254                let input_el = cfg_match! {
255                    feature = "std_web" => InputElement::try_from(element.clone()).ok(),
256                    feature = "web_sys" => element.dyn_ref::<InputElement>(),
257                };
258                if let Some(input) = input_el {
259                    let current_value = cfg_match! {
260                        feature = "std_web" => input.raw_value(),
261                        feature = "web_sys" => input.value(),
262                    };
263                    self.set_value(Cow::Owned(current_value));
264                }
265            } else if self.element_type == ElementType::Textarea {
266                let textarea_el = cfg_match! {
267                    feature = "std_web" => TextAreaElement::try_from(element.clone()).ok(),
268                    feature = "web_sys" => element.dyn_ref::<TextAreaElement>(),
269                };
270                if let Some(tae) = textarea_el {
271                    let value = tae.value();
272                    self.set_value(Cow::Owned(value));
273                }
274            }
275        }
276    }
277
278    /// Compares new kind with ancestor and produces a patch to apply, if any
279    fn diff_kind<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
280        match (
281            self.kind.as_ref(),
282            ancestor.as_ref().and_then(|anc| anc.kind.as_ref()),
283        ) {
284            (Some(ref left), Some(ref right)) => {
285                if left != right {
286                    Some(Patch::Replace(&**left, ()))
287                } else {
288                    None
289                }
290            }
291            (Some(ref left), None) => Some(Patch::Add(&**left, ())),
292            (None, Some(right)) => Some(Patch::Remove(&**right)),
293            (None, None) => None,
294        }
295    }
296
297    /// Compares new value with ancestor and produces a patch to apply, if any
298    fn diff_value<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
299        match (
300            self.value.as_ref(),
301            ancestor.as_ref().and_then(|anc| anc.value.as_ref()),
302        ) {
303            (Some(ref left), Some(ref right)) => {
304                if left != right {
305                    Some(Patch::Replace(&**left, ()))
306                } else {
307                    None
308                }
309            }
310            (Some(ref left), None) => Some(Patch::Add(&**left, ())),
311            (None, Some(right)) => Some(Patch::Remove(&**right)),
312            (None, None) => None,
313        }
314    }
315
316    fn apply_diffs(&mut self, ancestor: &mut Option<Box<Self>>) {
317        let changes = if let Some(old_attributes) = ancestor.as_mut().map(|a| &mut a.attributes) {
318            Attributes::diff(&self.attributes, old_attributes)
319        } else {
320            self.attributes
321                .iter()
322                .map(|(k, v)| Patch::Add(k, v))
323                .collect()
324        };
325
326        let element = self.reference.as_ref().expect("element expected");
327
328        for change in changes {
329            match change {
330                Patch::Add(key, value) | Patch::Replace(key, value) => {
331                    element
332                        .set_attribute(&key, &value)
333                        .expect("invalid attribute key");
334                }
335                Patch::Remove(key) => {
336                    cfg_match! {
337                        feature = "std_web" => element.remove_attribute(&key),
338                        feature = "web_sys" => element.remove_attribute(&key)
339                                                      .expect("could not remove attribute"),
340                    };
341                }
342            }
343        }
344
345        // TODO: add std_web after https://github.com/koute/stdweb/issues/395 will be approved
346        // Check this out: https://github.com/yewstack/yew/pull/1033/commits/4b4e958bb1ccac0524eb20f63f06ae394c20553d
347        #[cfg(feature = "web_sys")]
348        {
349            if self.element_type == ElementType::Button {
350                if let Some(button) = element.dyn_ref::<HtmlButtonElement>() {
351                    if let Some(change) = self.diff_kind(ancestor) {
352                        let kind = match change {
353                            Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
354                            Patch::Remove(_) => "",
355                        };
356                        button.set_type(kind);
357                    }
358                }
359            }
360        }
361
362        // `input` element has extra parameters to control
363        // I override behavior of attributes to make it more clear
364        // and useful in templates. For example I interpret `checked`
365        // attribute as `checked` parameter, not `defaultChecked` as browsers do
366        if self.element_type == ElementType::Input {
367            if let Some(input) = {
368                cfg_match! {
369                    feature = "std_web" => InputElement::try_from(element.clone()).ok(),
370                    feature = "web_sys" => element.dyn_ref::<InputElement>(),
371                }
372            } {
373                if let Some(change) = self.diff_kind(ancestor) {
374                    let kind = match change {
375                        Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
376                        Patch::Remove(_) => "",
377                    };
378                    cfg_match! {
379                        feature = "std_web" => ({
380                            //https://github.com/koute/stdweb/commit/3b85c941db00b8e3c942624afd50c5929085fb08
381                            //input.set_kind(&kind);
382                            let input = &input;
383                            js! { @(no_return)
384                                @{input}.type = @{kind};
385                            }
386                        }),
387                        feature = "web_sys" => input.set_type(kind),
388                    }
389                }
390
391                if let Some(change) = self.diff_value(ancestor) {
392                    let raw_value = match change {
393                        Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
394                        Patch::Remove(_) => "",
395                    };
396                    cfg_match! {
397                        feature = "std_web" => input.set_raw_value(raw_value),
398                        feature = "web_sys" => input.set_value(raw_value),
399                    };
400                }
401
402                // IMPORTANT! This parameter has to be set every time
403                // to prevent strange behaviour in the browser when the DOM changes
404                set_checked(&input, self.checked);
405            }
406        } else if self.element_type == ElementType::Textarea {
407            if let Some(tae) = {
408                cfg_match! {
409                    feature = "std_web" => TextAreaElement::try_from(element.clone()).ok(),
410                    feature = "web_sys" => element.dyn_ref::<TextAreaElement>(),
411                }
412            } {
413                if let Some(change) = self.diff_value(ancestor) {
414                    let value = match change {
415                        Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
416                        Patch::Remove(_) => "",
417                    };
418                    tae.set_value(value);
419                }
420            }
421        }
422    }
423
424    fn create_element(&self, parent: &Element) -> Element {
425        let tag = self.tag();
426        if tag == "svg"
427            || parent
428                .namespace_uri()
429                .map_or(false, |ns| ns == SVG_NAMESPACE)
430        {
431            let namespace = cfg_match! {
432                feature = "std_web" => SVG_NAMESPACE,
433                feature = "web_sys" => Some(SVG_NAMESPACE),
434            };
435            document()
436                .create_element_ns(namespace, tag)
437                .expect("can't create namespaced element for vtag")
438        } else {
439            document()
440                .create_element(tag)
441                .expect("can't create element for vtag")
442        }
443    }
444}
445
446impl VDiff for VTag {
447    /// Remove VTag from parent.
448    fn detach(&mut self, parent: &Element) {
449        let node = self
450            .reference
451            .take()
452            .expect("tried to remove not rendered VTag from DOM");
453
454        // recursively remove its children
455        self.children.detach(&node);
456        if parent.remove_child(&node).is_err() {
457            warn!("Node not found to remove VTag");
458        }
459        self.node_ref.set(None);
460    }
461
462    /// Renders virtual tag over DOM `Element`, but it also compares this with an ancestor `VTag`
463    /// to compute what to patch in the actual DOM nodes.
464    fn apply(
465        &mut self,
466        parent_scope: &AnyScope,
467        parent: &Element,
468        next_sibling: NodeRef,
469        ancestor: Option<VNode>,
470    ) -> NodeRef {
471        let mut ancestor_tag = ancestor.and_then(|mut ancestor| {
472            match ancestor {
473                // If the ancestor is a tag of the same type, don't recreate, keep the
474                // old tag and update its attributes and children.
475                VNode::VTag(vtag) if self.tag() == vtag.tag() && self.key == vtag.key => Some(vtag),
476                _ => {
477                    let element = self.create_element(parent);
478                    super::insert_node(&element, parent, Some(ancestor.first_node()));
479                    self.reference = Some(element);
480                    ancestor.detach(parent);
481                    None
482                }
483            }
484        });
485
486        if let Some(ref mut ancestor_tag) = &mut ancestor_tag {
487            // Refresh the current value to later compare it against the desired value
488            // since it may have been changed since we last set it.
489            ancestor_tag.refresh_value();
490            // Preserve the reference that already exists.
491            self.reference = ancestor_tag.reference.take();
492        } else if self.reference.is_none() {
493            let element = self.create_element(parent);
494            super::insert_node(&element, parent, next_sibling.get());
495            self.reference = Some(element);
496        }
497
498        self.apply_diffs(&mut ancestor_tag);
499        self.recreate_listeners(&mut ancestor_tag);
500
501        // Process children
502        let element = self.reference.as_ref().expect("Reference should be set");
503        if !self.children.is_empty() {
504            self.children.apply(
505                parent_scope,
506                element,
507                NodeRef::default(),
508                ancestor_tag.map(|a| a.children.into()),
509            );
510        } else if let Some(mut ancestor_tag) = ancestor_tag {
511            ancestor_tag.children.detach(element);
512        }
513
514        let node = cfg_match! {
515            feature = "std_web" => element.as_node(),
516            feature = "web_sys" => element.deref(),
517        };
518        self.node_ref.set(Some(node.clone()));
519        self.node_ref.clone()
520    }
521}
522
523/// Set `checked` value for the `InputElement`.
524fn set_checked(input: &InputElement, value: bool) {
525    cfg_match! {
526        feature = "std_web" => js!( @(no_return) @{input}.checked = @{value}; ),
527        feature = "web_sys" => input.set_checked(value),
528    };
529}
530
531impl PartialEq for VTag {
532    fn eq(&self, other: &VTag) -> bool {
533        self.tag == other.tag
534            && self.value == other.value
535            && self.kind == other.kind
536            && self.checked == other.checked
537            && self.listeners.len() == other.listeners.len()
538            && self
539                .listeners
540                .iter()
541                .map(|l| l.kind())
542                .eq(other.listeners.iter().map(|l| l.kind()))
543            && self.attributes == other.attributes
544            && self.children == other.children
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use crate::html;
552    use std::any::TypeId;
553    #[cfg(feature = "std_web")]
554    use stdweb::web::{document, IElement};
555    #[cfg(feature = "wasm_test")]
556    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
557
558    #[cfg(feature = "wasm_test")]
559    wasm_bindgen_test_configure!(run_in_browser);
560
561    fn test_scope() -> AnyScope {
562        AnyScope {
563            type_id: TypeId::of::<()>(),
564            parent: None,
565            state: Rc::new(()),
566        }
567    }
568
569    #[test]
570    fn it_compares_tags() {
571        let a = html! {
572            <div></div>
573        };
574
575        let b = html! {
576            <div></div>
577        };
578
579        let c = html! {
580            <p></p>
581        };
582
583        assert_eq!(a, b);
584        assert_ne!(a, c);
585    }
586
587    #[test]
588    fn it_compares_text() {
589        let a = html! {
590            <div>{ "correct" }</div>
591        };
592
593        let b = html! {
594            <div>{ "correct" }</div>
595        };
596
597        let c = html! {
598            <div>{ "incorrect" }</div>
599        };
600
601        assert_eq!(a, b);
602        assert_ne!(a, c);
603    }
604
605    #[test]
606    fn it_compares_attributes() {
607        let a = html! {
608            <div a="test"></div>
609        };
610
611        let b = html! {
612            <div a="test"></div>
613        };
614
615        let c = html! {
616            <div a="fail"></div>
617        };
618
619        assert_eq!(a, b);
620        assert_ne!(a, c);
621    }
622
623    #[test]
624    fn it_compares_children() {
625        let a = html! {
626            <div>
627                <p></p>
628            </div>
629        };
630
631        let b = html! {
632            <div>
633                <p></p>
634            </div>
635        };
636
637        let c = html! {
638            <div>
639                <span></span>
640            </div>
641        };
642
643        assert_eq!(a, b);
644        assert_ne!(a, c);
645    }
646
647    #[test]
648    fn it_compares_classes() {
649        let a = html! {
650            <div class="test"></div>
651        };
652
653        let b = html! {
654            <div class="test"></div>
655        };
656
657        let c = html! {
658            <div class="fail"></div>
659        };
660
661        let d = html! {
662            <div class=format!("fail{}", "")></div>
663        };
664
665        assert_eq!(a, b);
666        assert_ne!(a, c);
667        assert_eq!(c, d);
668    }
669
670    fn assert_vtag(node: &mut VNode) -> &mut VTag {
671        if let VNode::VTag(vtag) = node {
672            return vtag;
673        }
674        panic!("should be vtag");
675    }
676
677    fn assert_namespace(vtag: &VTag, namespace: &'static str) {
678        assert_eq!(
679            vtag.reference.as_ref().unwrap().namespace_uri().unwrap(),
680            namespace
681        );
682    }
683
684    #[test]
685    fn supports_svg() {
686        #[cfg(feature = "std_web")]
687        let document = document();
688        #[cfg(feature = "web_sys")]
689        let document = web_sys::window().unwrap().document().unwrap();
690
691        let scope = test_scope();
692        let div_el = document.create_element("div").unwrap();
693        let namespace = SVG_NAMESPACE;
694        #[cfg(feature = "web_sys")]
695        let namespace = Some(namespace);
696        let svg_el = document.create_element_ns(namespace, "svg").unwrap();
697
698        let mut g_node = html! { <g class="segment"></g> };
699        let path_node = html! { <path></path> };
700        let mut svg_node = html! { <svg>{path_node}</svg> };
701
702        let svg_tag = assert_vtag(&mut svg_node);
703        svg_tag.apply(&scope, &div_el, NodeRef::default(), None);
704        assert_namespace(svg_tag, SVG_NAMESPACE);
705        let path_tag = assert_vtag(svg_tag.children.get_mut(0).unwrap());
706        assert_namespace(path_tag, SVG_NAMESPACE);
707
708        let g_tag = assert_vtag(&mut g_node);
709        g_tag.apply(&scope, &div_el, NodeRef::default(), None);
710        assert_namespace(g_tag, HTML_NAMESPACE);
711        g_tag.reference = None;
712
713        g_tag.apply(&scope, &svg_el, NodeRef::default(), None);
714        assert_namespace(g_tag, SVG_NAMESPACE);
715    }
716
717    #[test]
718    fn it_compares_values() {
719        let a = html! {
720            <input value="test"/>
721        };
722
723        let b = html! {
724            <input value="test"/>
725        };
726
727        let c = html! {
728            <input value="fail"/>
729        };
730
731        assert_eq!(a, b);
732        assert_ne!(a, c);
733    }
734
735    #[test]
736    fn it_compares_kinds() {
737        let a = html! {
738            <input type="text"/>
739        };
740
741        let b = html! {
742            <input type="text"/>
743        };
744
745        let c = html! {
746            <input type="hidden"/>
747        };
748
749        assert_eq!(a, b);
750        assert_ne!(a, c);
751    }
752
753    #[test]
754    fn it_compares_checked() {
755        let a = html! {
756            <input type="checkbox" checked=false />
757        };
758
759        let b = html! {
760            <input type="checkbox" checked=false />
761        };
762
763        let c = html! {
764            <input type="checkbox" checked=true />
765        };
766
767        assert_eq!(a, b);
768        assert_ne!(a, c);
769    }
770
771    #[test]
772    fn it_allows_aria_attributes() {
773        let a = html! {
774            <p aria-controls="it-works">
775                <a class="btn btn-primary"
776                   data-toggle="collapse"
777                   href="#collapseExample"
778                   role="button"
779                   aria-expanded="false"
780                   aria-controls="collapseExample">
781                    { "Link with href" }
782                </a>
783                <button class="btn btn-primary"
784                        type="button"
785                        data-toggle="collapse"
786                        data-target="#collapseExample"
787                        aria-expanded="false"
788                        aria-controls="collapseExample">
789                    { "Button with data-target" }
790                </button>
791                <div own-attribute-with-multiple-parts="works" />
792            </p>
793        };
794        if let VNode::VTag(vtag) = a {
795            assert_eq!(
796                vtag.attributes
797                    .iter()
798                    .find(|(k, _)| k == &"aria-controls")
799                    .map(|(_, v)| v),
800                Some("it-works")
801            );
802        } else {
803            panic!("vtag expected");
804        }
805    }
806
807    #[test]
808    fn it_does_not_set_missing_class_name() {
809        let scope = test_scope();
810        let parent = document().create_element("div").unwrap();
811
812        #[cfg(feature = "std_web")]
813        document().body().unwrap().append_child(&parent);
814        #[cfg(feature = "web_sys")]
815        document().body().unwrap().append_child(&parent).unwrap();
816
817        let mut elem = html! { <div></div> };
818        elem.apply(&scope, &parent, NodeRef::default(), None);
819        let vtag = assert_vtag(&mut elem);
820        // test if the className has not been set
821        assert!(!vtag.reference.as_ref().unwrap().has_attribute("class"));
822    }
823
824    #[test]
825    fn it_sets_class_name() {
826        let scope = test_scope();
827        let parent = document().create_element("div").unwrap();
828
829        #[cfg(feature = "std_web")]
830        document().body().unwrap().append_child(&parent);
831        #[cfg(feature = "web_sys")]
832        document().body().unwrap().append_child(&parent).unwrap();
833
834        let mut elem = html! { <div class="ferris the crab"></div> };
835        elem.apply(&scope, &parent, NodeRef::default(), None);
836        let vtag = assert_vtag(&mut elem);
837        // test if the className has been set
838        assert!(vtag.reference.as_ref().unwrap().has_attribute("class"));
839    }
840
841    #[test]
842    fn controlled_input_synced() {
843        let scope = test_scope();
844        let parent = document().create_element("div").unwrap();
845
846        #[cfg(feature = "std_web")]
847        document().body().unwrap().append_child(&parent);
848        #[cfg(feature = "web_sys")]
849        document().body().unwrap().append_child(&parent).unwrap();
850
851        let expected = "not_changed_value";
852
853        // Initial state
854        let mut elem = html! { <input value=expected /> };
855        elem.apply(&scope, &parent, NodeRef::default(), None);
856        let vtag = if let VNode::VTag(vtag) = elem {
857            vtag
858        } else {
859            panic!("should be vtag")
860        };
861
862        // User input
863        let input_ref = vtag.reference.as_ref().unwrap();
864        let input = cfg_match! {
865            feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
866            feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
867        };
868        cfg_match! {
869            feature = "std_web" => input.unwrap().set_raw_value("User input"),
870            feature = "web_sys" => input.unwrap().set_value("User input"),
871        };
872
873        let ancestor = vtag;
874        let mut elem = html! { <input value=expected /> };
875        let vtag = assert_vtag(&mut elem);
876
877        // Sync happens here
878        vtag.apply(
879            &scope,
880            &parent,
881            NodeRef::default(),
882            Some(VNode::VTag(ancestor)),
883        );
884
885        // Get new current value of the input element
886        let input_ref = vtag.reference.as_ref().unwrap();
887        let input = cfg_match! {
888            feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
889            feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
890        }
891        .unwrap();
892
893        let current_value = cfg_match! {
894            feature = "std_web" => input.raw_value(),
895            feature = "web_sys" => input.value(),
896        };
897
898        // check whether not changed virtual dom value has been set to the input element
899        assert_eq!(current_value, expected);
900    }
901
902    #[test]
903    fn uncontrolled_input_unsynced() {
904        let scope = test_scope();
905        let parent = document().create_element("div").unwrap();
906
907        #[cfg(feature = "std_web")]
908        document().body().unwrap().append_child(&parent);
909        #[cfg(feature = "web_sys")]
910        document().body().unwrap().append_child(&parent).unwrap();
911
912        // Initial state
913        let mut elem = html! { <input /> };
914        elem.apply(&scope, &parent, NodeRef::default(), None);
915        let vtag = if let VNode::VTag(vtag) = elem {
916            vtag
917        } else {
918            panic!("should be vtag")
919        };
920
921        // User input
922        let input_ref = vtag.reference.as_ref().unwrap();
923        let input = cfg_match! {
924            feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
925            feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
926        };
927        cfg_match! {
928            feature = "std_web" => input.unwrap().set_raw_value("User input"),
929            feature = "web_sys" => input.unwrap().set_value("User input"),
930        };
931
932        let ancestor = vtag;
933        let mut elem = html! { <input /> };
934        let vtag = assert_vtag(&mut elem);
935
936        // Value should not be refreshed
937        vtag.apply(
938            &scope,
939            &parent,
940            NodeRef::default(),
941            Some(VNode::VTag(ancestor)),
942        );
943
944        // Get user value of the input element
945        let input_ref = vtag.reference.as_ref().unwrap();
946        let input = cfg_match! {
947            feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
948            feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
949        }
950        .unwrap();
951
952        let current_value = cfg_match! {
953            feature = "std_web" => input.raw_value(),
954            feature = "web_sys" => input.value(),
955        };
956
957        // check whether not changed virtual dom value has been set to the input element
958        assert_eq!(current_value, "User input");
959    }
960
961    #[test]
962    fn dynamic_tags_work() {
963        let scope = test_scope();
964        let parent = document().create_element("div").unwrap();
965
966        #[cfg(feature = "std_web")]
967        document().body().unwrap().append_child(&parent);
968        #[cfg(feature = "web_sys")]
969        document().body().unwrap().append_child(&parent).unwrap();
970
971        let mut elem = html! { <@{
972            let mut builder = String::new();
973            builder.push('a');
974            builder
975        }/> };
976
977        elem.apply(&scope, &parent, NodeRef::default(), None);
978        let vtag = assert_vtag(&mut elem);
979        // make sure the new tag name is used internally
980        assert_eq!(vtag.tag(), "a");
981
982        #[cfg(feature = "web_sys")]
983        // Element.tagName is always in the canonical upper-case form.
984        assert_eq!(vtag.reference.as_ref().unwrap().tag_name(), "A");
985    }
986
987    #[test]
988    fn dynamic_tags_handle_value_attribute() {
989        let mut div_el = html! {
990            <@{"div"} value="Hello"/>
991        };
992        let div_vtag = assert_vtag(&mut div_el);
993        assert!(div_vtag.value.is_none());
994        let v: Option<&str> = div_vtag
995            .attributes
996            .iter()
997            .find(|(k, _)| k == &"value")
998            .map(|(_, v)| AsRef::as_ref(v));
999        assert_eq!(v, Some("Hello"));
1000
1001        let mut input_el = html! {
1002            <@{"input"} value="World"/>
1003        };
1004        let input_vtag = assert_vtag(&mut input_el);
1005        assert_eq!(input_vtag.value, Some(Cow::Borrowed("World")));
1006        assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value"));
1007    }
1008
1009    #[test]
1010    fn dynamic_tags_handle_weird_capitalization() {
1011        let mut el = html! {
1012            <@{"tExTAREa"}/>
1013        };
1014        let vtag = assert_vtag(&mut el);
1015        assert_eq!(vtag.tag(), "textarea");
1016    }
1017
1018    #[test]
1019    fn reset_node_ref() {
1020        let scope = test_scope();
1021        let parent = document().create_element("div").unwrap();
1022
1023        #[cfg(feature = "std_web")]
1024        document().body().unwrap().append_child(&parent);
1025        #[cfg(feature = "web_sys")]
1026        document().body().unwrap().append_child(&parent).unwrap();
1027
1028        let node_ref = NodeRef::default();
1029        let mut elem: VNode = html! { <div ref=node_ref.clone()></div> };
1030        assert_vtag(&mut elem);
1031        elem.apply(&scope, &parent, NodeRef::default(), None);
1032        let parent_node = cfg_match! {
1033            feature = "std_web" => parent.as_node(),
1034            feature = "web_sys" => parent.deref(),
1035        };
1036        assert_eq!(node_ref.get(), parent_node.first_child());
1037        elem.detach(&parent);
1038        assert!(node_ref.get().is_none());
1039    }
1040
1041    /// Returns the class attribute as str reference, or "" if the attribute is not set.
1042    fn get_class_str(vtag: &VTag) -> &str {
1043        vtag.attributes
1044            .iter()
1045            .find(|(k, _)| k == &"class")
1046            .map(|(_, v)| AsRef::as_ref(v))
1047            .unwrap_or("")
1048    }
1049
1050    #[test]
1051    fn old_class_syntax_is_still_supported() {
1052        let a_classes = "class-1 class-2".to_string();
1053        #[allow(deprecated)]
1054        let a = html! {
1055            <div class=("class-1", a_classes)></div>
1056        };
1057
1058        if let VNode::VTag(vtag) = a {
1059            assert!(get_class_str(&vtag).contains("class-1"));
1060            assert!(get_class_str(&vtag).contains("class-2"));
1061            assert!(!get_class_str(&vtag).contains("class-3"));
1062        } else {
1063            panic!("vtag expected");
1064        }
1065    }
1066}
1067
1068#[cfg(all(test, feature = "web_sys"))]
1069mod layout_tests {
1070    extern crate self as yew;
1071
1072    use crate::html;
1073    use crate::virtual_dom::layout_tests::{diff_layouts, TestLayout};
1074
1075    #[cfg(feature = "wasm_test")]
1076    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
1077
1078    #[cfg(feature = "wasm_test")]
1079    wasm_bindgen_test_configure!(run_in_browser);
1080
1081    #[test]
1082    fn diff() {
1083        let layout1 = TestLayout {
1084            name: "1",
1085            node: html! {
1086                <ul>
1087                    <li>
1088                        {"a"}
1089                    </li>
1090                    <li>
1091                        {"b"}
1092                    </li>
1093                </ul>
1094            },
1095            expected: "<ul><li>a</li><li>b</li></ul>",
1096        };
1097
1098        let layout2 = TestLayout {
1099            name: "2",
1100            node: html! {
1101                <ul>
1102                    <li>
1103                        {"a"}
1104                    </li>
1105                    <li>
1106                        {"b"}
1107                    </li>
1108                    <li>
1109                        {"d"}
1110                    </li>
1111                </ul>
1112            },
1113            expected: "<ul><li>a</li><li>b</li><li>d</li></ul>",
1114        };
1115
1116        let layout3 = TestLayout {
1117            name: "3",
1118            node: html! {
1119                <ul>
1120                    <li>
1121                        {"a"}
1122                    </li>
1123                    <li>
1124                        {"b"}
1125                    </li>
1126                    <li>
1127                        {"c"}
1128                    </li>
1129                    <li>
1130                        {"d"}
1131                    </li>
1132                </ul>
1133            },
1134            expected: "<ul><li>a</li><li>b</li><li>c</li><li>d</li></ul>",
1135        };
1136
1137        let layout4 = TestLayout {
1138            name: "4",
1139            node: html! {
1140                <ul>
1141                    <li>
1142                        <>
1143                            {"a"}
1144                        </>
1145                    </li>
1146                    <li>
1147                        {"b"}
1148                        <li>
1149                            {"c"}
1150                        </li>
1151                        <li>
1152                            {"d"}
1153                        </li>
1154                    </li>
1155                </ul>
1156            },
1157            expected: "<ul><li>a</li><li>b<li>c</li><li>d</li></li></ul>",
1158        };
1159
1160        diff_layouts(vec![layout1, layout2, layout3, layout4]);
1161    }
1162}