This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/dom_bundle/btag/
mod.rs

1//! This module contains the bundle implementation of a tag [BTag]
2
3mod attributes;
4mod listeners;
5
6use std::cell::RefCell;
7use std::collections::HashMap;
8use std::hint::unreachable_unchecked;
9use std::ops::DerefMut;
10
11use gloo::utils::document;
12use listeners::ListenerRegistration;
13pub use listeners::Registry;
14use wasm_bindgen::JsCast;
15use web_sys::{Element, HtmlTextAreaElement as TextAreaElement};
16
17use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget};
18use crate::html::AnyScope;
19#[cfg(feature = "hydration")]
20use crate::virtual_dom::vtag::HTML_NAMESPACE;
21use crate::virtual_dom::vtag::{
22    InputFields, TextareaFields, VTagInner, Value, MATHML_NAMESPACE, SVG_NAMESPACE,
23};
24use crate::virtual_dom::{AttrValue, Attributes, Key, VTag};
25use crate::NodeRef;
26
27/// Applies contained changes to DOM [web_sys::Element]
28trait Apply {
29    /// [web_sys::Element] subtype to apply the changes to
30    type Element;
31    type Bundle;
32
33    /// Apply contained values to [Element](Self::Element) with no ancestor
34    fn apply(self, root: &BSubtree, el: &Self::Element) -> Self::Bundle;
35
36    /// Apply diff between [self] and `bundle` to [Element](Self::Element).
37    fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self::Bundle);
38}
39
40/// [BTag] fields that are specific to different [BTag] kinds.
41/// Decreases the memory footprint of [BTag] by avoiding impossible field and value combinations.
42#[derive(Debug)]
43enum BTagInner {
44    /// Fields specific to
45    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
46    Input(InputFields),
47    /// Fields specific to
48    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
49    Textarea {
50        /// Contains a value of an
51        /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
52        value: Value<TextAreaElement>,
53    },
54    /// Fields for all other kinds of [VTag]s
55    Other {
56        /// A tag of the element.
57        tag: AttrValue,
58        /// Child node.
59        child_bundle: BNode,
60    },
61}
62
63/// The bundle implementation to [VTag]
64#[derive(Debug)]
65pub(super) struct BTag {
66    /// [BTag] fields that are specific to different [BTag] kinds.
67    inner: BTagInner,
68    listeners: ListenerRegistration,
69    attributes: Attributes,
70    /// A reference to the DOM [`Element`].
71    reference: Element,
72    /// A node reference used for DOM access in Component lifecycle methods
73    node_ref: NodeRef,
74    key: Option<Key>,
75}
76
77impl ReconcileTarget for BTag {
78    fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
79        self.listeners.unregister(root);
80
81        let node = self.reference;
82        // recursively remove its children
83        if let BTagInner::Other { child_bundle, .. } = self.inner {
84            // This tag will be removed, so there's no point to remove any child.
85            child_bundle.detach(root, &node, true);
86        }
87        if !parent_to_detach {
88            let result = parent.remove_child(&node);
89
90            if result.is_err() {
91                tracing::warn!("Node not found to remove VTag");
92            }
93        }
94        // It could be that the ref was already reused when rendering another element.
95        // Only unset the ref it still belongs to our node
96        if self.node_ref.get().as_ref() == Some(&node) {
97            self.node_ref.set(None);
98        }
99    }
100
101    fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot {
102        slot.insert(next_parent, &self.reference);
103
104        DomSlot::at(self.reference.clone().into())
105    }
106}
107
108impl Reconcilable for VTag {
109    type Bundle = BTag;
110
111    fn attach(
112        self,
113        root: &BSubtree,
114        parent_scope: &AnyScope,
115        parent: &Element,
116        slot: DomSlot,
117    ) -> (DomSlot, Self::Bundle) {
118        let el = self.create_element(parent);
119        let Self {
120            listeners,
121            attributes,
122            node_ref,
123            key,
124            ..
125        } = self;
126
127        // Apply attributes BEFORE inserting the element into the DOM
128        // This is crucial for SVG animation elements where the animation
129        // starts immediately upon DOM insertion
130        let attributes = attributes.apply(root, &el);
131        let listeners = listeners.apply(root, &el);
132
133        // Now insert the element with attributes already set
134        slot.insert(parent, &el);
135
136        let inner = match self.inner {
137            VTagInner::Input(f) => {
138                let f = f.apply(root, el.unchecked_ref());
139                BTagInner::Input(f)
140            }
141            VTagInner::Textarea(f) => {
142                let value = f.apply(root, el.unchecked_ref());
143                BTagInner::Textarea { value }
144            }
145            VTagInner::Other { children, tag } => {
146                let (_, child_bundle) = children.attach(root, parent_scope, &el, DomSlot::at_end());
147                BTagInner::Other { child_bundle, tag }
148            }
149        };
150        node_ref.set(Some(el.clone().into()));
151        (
152            DomSlot::at(el.clone().into()),
153            BTag {
154                inner,
155                listeners,
156                reference: el,
157                attributes,
158                key,
159                node_ref,
160            },
161        )
162    }
163
164    fn reconcile_node(
165        self,
166        root: &BSubtree,
167        parent_scope: &AnyScope,
168        parent: &Element,
169        slot: DomSlot,
170        bundle: &mut BNode,
171    ) -> DomSlot {
172        // This kind of branching patching routine reduces branch predictor misses and the need to
173        // unpack the enums (including `Option`s) all the time, resulting in a more streamlined
174        // patching flow
175        match bundle {
176            // If the ancestor is a tag of the same type, don't recreate, keep the
177            // old tag and update its attributes and children.
178            BNode::Tag(ex) if self.key == ex.key => {
179                if match (&self.inner, &ex.inner) {
180                    (VTagInner::Input(_), BTagInner::Input(_)) => true,
181                    (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true,
182                    (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. })
183                        if l == r =>
184                    {
185                        true
186                    }
187                    _ => false,
188                } {
189                    return self.reconcile(root, parent_scope, parent, slot, ex.deref_mut());
190                }
191            }
192            _ => {}
193        };
194        self.replace(root, parent_scope, parent, slot, bundle)
195    }
196
197    fn reconcile(
198        self,
199        root: &BSubtree,
200        parent_scope: &AnyScope,
201        _parent: &Element,
202        _slot: DomSlot,
203        tag: &mut Self::Bundle,
204    ) -> DomSlot {
205        let el = &tag.reference;
206        self.attributes.apply_diff(root, el, &mut tag.attributes);
207        self.listeners.apply_diff(root, el, &mut tag.listeners);
208
209        match (self.inner, &mut tag.inner) {
210            (VTagInner::Input(new), BTagInner::Input(old)) => {
211                new.apply_diff(root, el.unchecked_ref(), old);
212            }
213            (
214                VTagInner::Textarea(TextareaFields { value: new, .. }),
215                BTagInner::Textarea { value: old },
216            ) => {
217                new.apply_diff(root, el.unchecked_ref(), old);
218            }
219            (
220                VTagInner::Other { children: new, .. },
221                BTagInner::Other {
222                    child_bundle: old, ..
223                },
224            ) => {
225                new.reconcile(root, parent_scope, el, DomSlot::at_end(), old);
226            }
227            // Can not happen, because we checked for tag equability above
228            _ => unsafe { unreachable_unchecked() },
229        }
230
231        tag.key = self.key;
232
233        if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) {
234            tag.node_ref.set(None);
235        }
236        if self.node_ref != tag.node_ref {
237            tag.node_ref = self.node_ref;
238            tag.node_ref.set(Some(el.clone().into()));
239        }
240
241        DomSlot::at(el.clone().into())
242    }
243}
244
245impl VTag {
246    fn create_element(&self, parent: &Element) -> Element {
247        let tag = self.tag();
248        // check for an xmlns attribute. If it exists, create an element with the specified
249        // namespace
250        if let Some(xmlns) = self
251            .attributes
252            .iter()
253            .find(|(k, _)| *k == "xmlns")
254            .map(|(_, v)| v)
255        {
256            document()
257                .create_element_ns(Some(xmlns), tag)
258                .expect("can't create namespaced element for vtag")
259        } else if tag == "svg" || parent.namespace_uri().is_some_and(|ns| ns == SVG_NAMESPACE) {
260            let namespace = Some(SVG_NAMESPACE);
261            document()
262                .create_element_ns(namespace, tag)
263                .expect("can't create namespaced element for vtag")
264        } else if tag == "math"
265            || parent
266                .namespace_uri()
267                .is_some_and(|ns| ns == MATHML_NAMESPACE)
268        {
269            let namespace = Some(MATHML_NAMESPACE);
270            document()
271                .create_element_ns(namespace, tag)
272                .expect("can't create namespaced element for vtag")
273        } else {
274            thread_local! {
275                static CACHED_ELEMENTS: RefCell<HashMap<String, Element>> = RefCell::new(HashMap::with_capacity(32));
276            }
277
278            CACHED_ELEMENTS.with(|cache| {
279                let mut cache = cache.borrow_mut();
280                let cached = cache.get(tag).map(|el| {
281                    el.clone_node()
282                        .expect("couldn't clone cached element")
283                        .unchecked_into::<Element>()
284                });
285                cached.unwrap_or_else(|| {
286                    let to_be_cached = document()
287                        .create_element(tag)
288                        .expect("can't create element for vtag");
289                    cache.insert(
290                        tag.to_string(),
291                        to_be_cached
292                            .clone_node()
293                            .expect("couldn't clone node to be cached")
294                            .unchecked_into(),
295                    );
296                    to_be_cached
297                })
298            })
299        }
300    }
301}
302
303impl BTag {
304    /// Get the key of the underlying tag
305    pub fn key(&self) -> Option<&Key> {
306        self.key.as_ref()
307    }
308
309    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
310    #[cfg(test)]
311    fn reference(&self) -> &Element {
312        &self.reference
313    }
314
315    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
316    #[cfg(test)]
317    fn children(&self) -> Option<&BNode> {
318        match &self.inner {
319            BTagInner::Other { child_bundle, .. } => Some(child_bundle),
320            _ => None,
321        }
322    }
323
324    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
325    #[cfg(test)]
326    fn tag(&self) -> &str {
327        match &self.inner {
328            BTagInner::Input { .. } => "input",
329            BTagInner::Textarea { .. } => "textarea",
330            BTagInner::Other { tag, .. } => tag.as_ref(),
331        }
332    }
333}
334
335#[cfg(feature = "hydration")]
336mod feat_hydration {
337    use web_sys::Node;
338
339    use super::*;
340    use crate::dom_bundle::{node_type_str, Fragment, Hydratable};
341
342    impl Hydratable for VTag {
343        fn hydrate(
344            self,
345            root: &BSubtree,
346            parent_scope: &AnyScope,
347            _parent: &Element,
348            fragment: &mut Fragment,
349        ) -> Self::Bundle {
350            let tag_name = self.tag().to_owned();
351
352            let Self {
353                inner,
354                listeners,
355                attributes,
356                node_ref,
357                key,
358            } = self;
359
360            // We trim all text nodes as it's likely these are whitespaces.
361            fragment.trim_start_text_nodes();
362
363            let node = fragment
364                .pop_front()
365                .unwrap_or_else(|| panic!("expected element of type {tag_name}, found EOF."));
366
367            assert_eq!(
368                node.node_type(),
369                Node::ELEMENT_NODE,
370                "expected element, found node type {}.",
371                node_type_str(&node),
372            );
373            let el = node.dyn_into::<Element>().expect("expected an element.");
374
375            {
376                let el_tag_name = el.tag_name();
377                let parent_namespace = _parent.namespace_uri();
378
379                // In HTML namespace (or no namespace), createElement is case-insensitive
380                // In other namespaces (SVG, MathML), createElementNS is case-sensitive
381                let should_compare_case_insensitive = parent_namespace.is_none()
382                    || parent_namespace.as_deref() == Some(HTML_NAMESPACE);
383
384                if should_compare_case_insensitive {
385                    // Case-insensitive comparison for HTML elements
386                    assert!(
387                        tag_name.eq_ignore_ascii_case(&el_tag_name),
388                        "expected element of kind {tag_name}, found {el_tag_name}.",
389                    );
390                } else {
391                    // Case-sensitive comparison for namespaced elements (SVG, MathML)
392                    assert_eq!(
393                        el_tag_name, tag_name,
394                        "expected element of kind {tag_name}, found {el_tag_name}.",
395                    );
396                }
397            }
398            // We simply register listeners and update all attributes.
399            let attributes = attributes.apply(root, &el);
400            let listeners = listeners.apply(root, &el);
401
402            // For input and textarea elements, we update their value anyways.
403            let inner = match inner {
404                VTagInner::Input(f) => {
405                    let f = f.apply(root, el.unchecked_ref());
406                    BTagInner::Input(f)
407                }
408                VTagInner::Textarea(f) => {
409                    let value = f.apply(root, el.unchecked_ref());
410
411                    BTagInner::Textarea { value }
412                }
413                VTagInner::Other { children, tag } => {
414                    let mut nodes = Fragment::collect_children(&el);
415                    let child_bundle = children.hydrate(root, parent_scope, &el, &mut nodes);
416
417                    nodes.trim_start_text_nodes();
418
419                    assert!(nodes.is_empty(), "expected EOF, found node.");
420
421                    BTagInner::Other { child_bundle, tag }
422                }
423            };
424
425            node_ref.set(Some((*el).clone()));
426
427            BTag {
428                inner,
429                listeners,
430                attributes,
431                reference: el,
432                node_ref,
433                key,
434            }
435        }
436    }
437}
438
439#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
440#[cfg(test)]
441mod tests {
442    use std::rc::Rc;
443
444    use wasm_bindgen::JsCast;
445    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
446    use web_sys::HtmlInputElement as InputElement;
447
448    use super::*;
449    use crate::dom_bundle::utils::setup_parent;
450    use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget};
451    use crate::utils::RcExt;
452    use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE};
453    use crate::virtual_dom::{AttrValue, VNode, VTag};
454    use crate::{html, Html, NodeRef};
455
456    wasm_bindgen_test_configure!(run_in_browser);
457
458    #[test]
459    fn it_compares_tags() {
460        let a = html! {
461            <div></div>
462        };
463
464        let b = html! {
465            <div></div>
466        };
467
468        let c = html! {
469            <p></p>
470        };
471
472        assert_eq!(a, b);
473        assert_ne!(a, c);
474    }
475
476    #[test]
477    fn it_compares_text() {
478        let a = html! {
479            <div>{ "correct" }</div>
480        };
481
482        let b = html! {
483            <div>{ "correct" }</div>
484        };
485
486        let c = html! {
487            <div>{ "incorrect" }</div>
488        };
489
490        assert_eq!(a, b);
491        assert_ne!(a, c);
492    }
493
494    #[test]
495    fn it_compares_attributes_static() {
496        let a = html! {
497            <div a="test"></div>
498        };
499
500        let b = html! {
501            <div a="test"></div>
502        };
503
504        let c = html! {
505            <div a="fail"></div>
506        };
507
508        assert_eq!(a, b);
509        assert_ne!(a, c);
510    }
511
512    #[test]
513    fn it_compares_attributes_dynamic() {
514        let a = html! {
515            <div a={"test".to_owned()}></div>
516        };
517
518        let b = html! {
519            <div a={"test".to_owned()}></div>
520        };
521
522        let c = html! {
523            <div a={"fail".to_owned()}></div>
524        };
525
526        assert_eq!(a, b);
527        assert_ne!(a, c);
528    }
529
530    #[test]
531    fn it_compares_children() {
532        let a = html! {
533            <div>
534                <p></p>
535            </div>
536        };
537
538        let b = html! {
539            <div>
540                <p></p>
541            </div>
542        };
543
544        let c = html! {
545            <div>
546                <span></span>
547            </div>
548        };
549
550        assert_eq!(a, b);
551        assert_ne!(a, c);
552    }
553
554    #[test]
555    fn it_compares_classes_static() {
556        let a = html! {
557            <div class="test"></div>
558        };
559
560        let b = html! {
561            <div class="test"></div>
562        };
563
564        let c = html! {
565            <div class="fail"></div>
566        };
567
568        let d = html! {
569            <div class={format!("fail{}", "")}></div>
570        };
571
572        assert_eq!(a, b);
573        assert_ne!(a, c);
574        assert_ne!(a, d);
575    }
576
577    #[test]
578    fn it_compares_classes_dynamic() {
579        let a = html! {
580            <div class={"test".to_owned()}></div>
581        };
582
583        let b = html! {
584            <div class={"test".to_owned()}></div>
585        };
586
587        let c = html! {
588            <div class={"fail".to_owned()}></div>
589        };
590
591        let d = html! {
592            <div class={format!("fail{}", "")}></div>
593        };
594
595        assert_eq!(a, b);
596        assert_ne!(a, c);
597        assert_ne!(a, d);
598    }
599
600    fn assert_vtag(node: VNode) -> VTag {
601        if let VNode::VTag(vtag) = node {
602            return RcExt::unwrap_or_clone(vtag);
603        }
604        panic!("should be vtag");
605    }
606
607    fn assert_btag_ref(node: &BNode) -> &BTag {
608        if let BNode::Tag(vtag) = node {
609            return vtag;
610        }
611        panic!("should be btag");
612    }
613
614    fn assert_vtag_ref(node: &VNode) -> &VTag {
615        if let VNode::VTag(vtag) = node {
616            return vtag;
617        }
618        panic!("should be vtag");
619    }
620
621    fn assert_btag_mut(node: &mut BNode) -> &mut BTag {
622        if let BNode::Tag(btag) = node {
623            return btag;
624        }
625        panic!("should be btag");
626    }
627
628    fn assert_namespace(vtag: &BTag, namespace: &'static str) {
629        assert_eq!(vtag.reference().namespace_uri().unwrap(), namespace);
630    }
631
632    #[test]
633    fn supports_svg() {
634        let (root, scope, parent) = setup_parent();
635        let document = web_sys::window().unwrap().document().unwrap();
636
637        let namespace = SVG_NAMESPACE;
638        let namespace = Some(namespace);
639        let svg_el = document.create_element_ns(namespace, "svg").unwrap();
640
641        let g_node = html! { <g class="segment"></g> };
642        let path_node = html! { <path></path> };
643        let svg_node = html! { <svg>{path_node}</svg> };
644
645        let svg_tag = assert_vtag(svg_node);
646        let (_, svg_tag) = svg_tag.attach(&root, &scope, &parent, DomSlot::at_end());
647        assert_namespace(&svg_tag, SVG_NAMESPACE);
648        let path_tag = assert_btag_ref(svg_tag.children().unwrap());
649        assert_namespace(path_tag, SVG_NAMESPACE);
650
651        let g_tag = assert_vtag(g_node.clone());
652        let (_, g_tag) = g_tag.attach(&root, &scope, &parent, DomSlot::at_end());
653        assert_namespace(&g_tag, HTML_NAMESPACE);
654
655        let g_tag = assert_vtag(g_node);
656        let (_, g_tag) = g_tag.attach(&root, &scope, &svg_el, DomSlot::at_end());
657        assert_namespace(&g_tag, SVG_NAMESPACE);
658    }
659
660    #[test]
661    fn supports_mathml() {
662        let (root, scope, parent) = setup_parent();
663        let mfrac_node = html! { <mfrac> </mfrac> };
664        let math_node = html! { <math>{mfrac_node}</math> };
665
666        let math_tag = assert_vtag(math_node);
667        let (_, math_tag) = math_tag.attach(&root, &scope, &parent, DomSlot::at_end());
668        assert_namespace(&math_tag, MATHML_NAMESPACE);
669        let mfrac_tag = assert_btag_ref(math_tag.children().unwrap());
670        assert_namespace(mfrac_tag, MATHML_NAMESPACE);
671    }
672
673    #[test]
674    fn it_compares_values() {
675        let a = html! {
676            <input value="test"/>
677        };
678
679        let b = html! {
680            <input value="test"/>
681        };
682
683        let c = html! {
684            <input value="fail"/>
685        };
686
687        assert_eq!(a, b);
688        assert_ne!(a, c);
689    }
690
691    #[test]
692    fn it_compares_kinds() {
693        let a = html! {
694            <input type="text"/>
695        };
696
697        let b = html! {
698            <input type="text"/>
699        };
700
701        let c = html! {
702            <input type="hidden"/>
703        };
704
705        assert_eq!(a, b);
706        assert_ne!(a, c);
707    }
708
709    #[test]
710    fn it_compares_checked() {
711        let a = html! {
712            <input type="checkbox" checked=false />
713        };
714
715        let b = html! {
716            <input type="checkbox" checked=false />
717        };
718
719        let c = html! {
720            <input type="checkbox" checked=true />
721        };
722
723        assert_eq!(a, b);
724        assert_ne!(a, c);
725    }
726
727    #[test]
728    fn it_allows_aria_attributes() {
729        let a = html! {
730            <p aria-controls="it-works">
731                <a class="btn btn-primary"
732                   data-toggle="collapse"
733                   href="#collapseExample"
734                   role="button"
735                   aria-expanded="false"
736                   aria-controls="collapseExample">
737                    { "Link with href" }
738                </a>
739                <button class="btn btn-primary"
740                        type="button"
741                        data-toggle="collapse"
742                        data-target="#collapseExample"
743                        aria-expanded="false"
744                        aria-controls="collapseExample">
745                    { "Button with data-target" }
746                </button>
747                <div own-attribute-with-multiple-parts="works" />
748            </p>
749        };
750        if let VNode::VTag(vtag) = a {
751            assert_eq!(
752                vtag.attributes
753                    .iter()
754                    .find(|(k, _)| k == &"aria-controls")
755                    .map(|(_, v)| v),
756                Some("it-works")
757            );
758        } else {
759            panic!("vtag expected");
760        }
761    }
762
763    #[test]
764    fn it_does_not_set_missing_class_name() {
765        let (root, scope, parent) = setup_parent();
766
767        let elem = html! { <div></div> };
768        let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end());
769        let vtag = assert_btag_mut(&mut elem);
770        // test if the className has not been set
771        assert!(!vtag.reference().has_attribute("class"));
772    }
773
774    fn test_set_class_name(gen_html: impl FnOnce() -> Html) {
775        let (root, scope, parent) = setup_parent();
776
777        let elem = gen_html();
778        let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end());
779        let vtag = assert_btag_mut(&mut elem);
780        // test if the className has been set
781        assert!(vtag.reference().has_attribute("class"));
782    }
783
784    #[test]
785    fn it_sets_class_name_static() {
786        test_set_class_name(|| html! { <div class="ferris the crab"></div> });
787    }
788
789    #[test]
790    fn it_sets_class_name_dynamic() {
791        test_set_class_name(|| html! { <div class={"ferris the crab".to_owned()}></div> });
792    }
793
794    #[test]
795    fn controlled_input_synced() {
796        let (root, scope, parent) = setup_parent();
797
798        let expected = "not_changed_value";
799
800        // Initial state
801        let elem = html! { <input value={expected} /> };
802        let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end());
803        let vtag = assert_btag_ref(&elem);
804
805        // User input
806        let input_ref = &vtag.reference();
807        let input = input_ref.dyn_ref::<InputElement>();
808        input.unwrap().set_value("User input");
809
810        let next_elem = html! { <input value={expected} /> };
811        let elem_vtag = assert_vtag(next_elem);
812
813        // Sync happens here
814        elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
815        let vtag = assert_btag_ref(&elem);
816
817        // Get new current value of the input element
818        let input_ref = &vtag.reference();
819        let input = input_ref.dyn_ref::<InputElement>().unwrap();
820
821        let current_value = input.value();
822
823        // check whether not changed virtual dom value has been set to the input element
824        assert_eq!(current_value, expected);
825    }
826
827    #[test]
828    fn uncontrolled_input_unsynced() {
829        let (root, scope, parent) = setup_parent();
830
831        // Initial state
832        let elem = html! { <input /> };
833        let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end());
834        let vtag = assert_btag_ref(&elem);
835
836        // User input
837        let input_ref = &vtag.reference();
838        let input = input_ref.dyn_ref::<InputElement>();
839        input.unwrap().set_value("User input");
840
841        let next_elem = html! { <input /> };
842        let elem_vtag = assert_vtag(next_elem);
843
844        // Value should not be refreshed
845        elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
846        let vtag = assert_btag_ref(&elem);
847
848        // Get user value of the input element
849        let input_ref = &vtag.reference();
850        let input = input_ref.dyn_ref::<InputElement>().unwrap();
851
852        let current_value = input.value();
853
854        // check whether not changed virtual dom value has been set to the input element
855        assert_eq!(current_value, "User input");
856
857        // Need to remove the element to clean up the dirty state of the DOM. Failing this causes
858        // event listener tests to fail.
859        parent.remove();
860    }
861
862    #[test]
863    fn dynamic_tags_work() {
864        let (root, scope, parent) = setup_parent();
865
866        let elem = html! { <@{{
867            let mut builder = String::new();
868            builder.push('a');
869            builder
870        }}/> };
871
872        let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end());
873        let vtag = assert_btag_mut(&mut elem);
874        // make sure the new tag name is used internally
875        assert_eq!(vtag.tag(), "a");
876
877        // Element.tagName is always in the canonical upper-case form.
878        assert_eq!(vtag.reference().tag_name(), "A");
879    }
880
881    #[test]
882    fn dynamic_tags_handle_value_attribute() {
883        let div_el = html! {
884            <@{"div"} value="Hello"/>
885        };
886        let div_vtag = assert_vtag_ref(&div_el);
887        assert!(div_vtag.value().is_none());
888        let v: Option<&str> = div_vtag
889            .attributes
890            .iter()
891            .find(|(k, _)| k == &"value")
892            .map(|(_, v)| AsRef::as_ref(v));
893        assert_eq!(v, Some("Hello"));
894
895        let input_el = html! {
896            <@{"input"} value="World"/>
897        };
898        let input_vtag = assert_vtag_ref(&input_el);
899        assert_eq!(input_vtag.value(), Some(&AttrValue::Static("World")));
900        assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value"));
901    }
902
903    #[test]
904    fn dynamic_tags_handle_weird_capitalization() {
905        let el = html! {
906            <@{"tExTAREa"}/>
907        };
908        let vtag = assert_vtag_ref(&el);
909        // textarea is a special element, so it gets normalized
910        assert_eq!(vtag.tag(), "textarea");
911    }
912
913    #[test]
914    fn dynamic_tags_allow_custom_capitalization() {
915        let el = html! {
916            <@{"clipPath"}/>
917        };
918        let vtag = assert_vtag_ref(&el);
919        // no special treatment for elements not recognized e.g. clipPath
920        assert_eq!(vtag.tag(), "clipPath");
921    }
922
923    #[test]
924    fn reset_node_ref() {
925        let (root, scope, parent) = setup_parent();
926
927        let node_ref = NodeRef::default();
928        let elem: VNode = html! { <div ref={node_ref.clone()}></div> };
929        assert_vtag_ref(&elem);
930        let (_, elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end());
931        assert_eq!(node_ref.get(), parent.first_child());
932        elem.detach(&root, &parent, false);
933        assert!(node_ref.get().is_none());
934    }
935
936    #[test]
937    fn vtag_reuse_should_reset_ancestors_node_ref() {
938        let (root, scope, parent) = setup_parent();
939
940        let node_ref_a = NodeRef::default();
941        let elem_a = html! { <div id="a" ref={node_ref_a.clone()} /> };
942        let (_, mut elem) = elem_a.attach(&root, &scope, &parent, DomSlot::at_end());
943
944        // save the Node to check later that it has been reused.
945        let node_a = node_ref_a.get().unwrap();
946
947        let node_ref_b = NodeRef::default();
948        let elem_b = html! { <div id="b" ref={node_ref_b.clone()} /> };
949        elem_b.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
950
951        let node_b = node_ref_b.get().unwrap();
952
953        assert_eq!(node_a, node_b, "VTag should have reused the element");
954        assert!(
955            node_ref_a.get().is_none(),
956            "node_ref_a should have been reset when the element was reused."
957        );
958    }
959
960    #[test]
961    fn vtag_should_not_touch_newly_bound_refs() {
962        let (root, scope, parent) = setup_parent();
963
964        let test_ref = NodeRef::default();
965        let before = html! {
966            <>
967                <div ref={&test_ref} id="before" />
968            </>
969        };
970        let after = html! {
971            <>
972                <h6 />
973                <div ref={&test_ref} id="after" />
974            </>
975        };
976        // The point of this diff is to first render the "after" div and then detach the "before"
977        // div, while both should be bound to the same node ref
978
979        let (_, mut elem) = before.attach(&root, &scope, &parent, DomSlot::at_end());
980        after.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
981
982        assert_eq!(
983            test_ref
984                .get()
985                .unwrap()
986                .dyn_ref::<web_sys::Element>()
987                .unwrap()
988                .outer_html(),
989            "<div id=\"after\"></div>"
990        );
991    }
992
993    // test for bug: https://github.com/yewstack/yew/pull/2653
994    #[test]
995    fn test_index_map_attribute_diff() {
996        let (root, scope, parent) = setup_parent();
997
998        let test_ref = NodeRef::default();
999
1000        // We want to test appy_diff with Attributes::IndexMap, so we
1001        // need to create the VTag manually
1002
1003        // Create <div disabled="disabled" tabindex="0">
1004        let mut vtag = VTag::new("div");
1005        vtag.node_ref = test_ref.clone();
1006        vtag.add_attribute("disabled", "disabled");
1007        vtag.add_attribute("tabindex", "0");
1008
1009        let elem = VNode::VTag(Rc::new(vtag));
1010
1011        let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end());
1012
1013        // Create <div tabindex="0"> (removed first attribute "disabled")
1014        let mut vtag = VTag::new("div");
1015        vtag.node_ref = test_ref.clone();
1016        vtag.add_attribute("tabindex", "0");
1017        let next_elem = VNode::VTag(Rc::new(vtag));
1018        let elem_vtag = assert_vtag(next_elem);
1019
1020        // Sync happens here
1021        // this should remove the "disabled" attribute
1022        elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
1023
1024        assert_eq!(
1025            test_ref
1026                .get()
1027                .unwrap()
1028                .dyn_ref::<web_sys::Element>()
1029                .unwrap()
1030                .outer_html(),
1031            "<div tabindex=\"0\"></div>"
1032        );
1033    }
1034}
1035
1036#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1037#[cfg(test)]
1038mod layout_tests {
1039    extern crate self as yew;
1040
1041    use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
1042
1043    use crate::html;
1044    use crate::tests::layout_tests::{diff_layouts, TestLayout};
1045
1046    wasm_bindgen_test_configure!(run_in_browser);
1047
1048    #[test]
1049    fn diff() {
1050        let layout1 = TestLayout {
1051            name: "1",
1052            node: html! {
1053                <ul>
1054                    <li>
1055                        {"a"}
1056                    </li>
1057                    <li>
1058                        {"b"}
1059                    </li>
1060                </ul>
1061            },
1062            expected: "<ul><li>a</li><li>b</li></ul>",
1063        };
1064
1065        let layout2 = TestLayout {
1066            name: "2",
1067            node: html! {
1068                <ul>
1069                    <li>
1070                        {"a"}
1071                    </li>
1072                    <li>
1073                        {"b"}
1074                    </li>
1075                    <li>
1076                        {"d"}
1077                    </li>
1078                </ul>
1079            },
1080            expected: "<ul><li>a</li><li>b</li><li>d</li></ul>",
1081        };
1082
1083        let layout3 = TestLayout {
1084            name: "3",
1085            node: html! {
1086                <ul>
1087                    <li>
1088                        {"a"}
1089                    </li>
1090                    <li>
1091                        {"b"}
1092                    </li>
1093                    <li>
1094                        {"c"}
1095                    </li>
1096                    <li>
1097                        {"d"}
1098                    </li>
1099                </ul>
1100            },
1101            expected: "<ul><li>a</li><li>b</li><li>c</li><li>d</li></ul>",
1102        };
1103
1104        let layout4 = TestLayout {
1105            name: "4",
1106            node: html! {
1107                <ul>
1108                    <li>
1109                        <>
1110                            {"a"}
1111                        </>
1112                    </li>
1113                    <li>
1114                        {"b"}
1115                        <li>
1116                            {"c"}
1117                        </li>
1118                        <li>
1119                            {"d"}
1120                        </li>
1121                    </li>
1122                </ul>
1123            },
1124            expected: "<ul><li>a</li><li>b<li>c</li><li>d</li></li></ul>",
1125        };
1126
1127        diff_layouts(vec![layout1, layout2, layout3, layout4]);
1128    }
1129}
1130
1131#[cfg(test)]
1132mod tests_without_browser {
1133    use crate::html;
1134    use crate::virtual_dom::VNode;
1135
1136    #[test]
1137    fn html_if_bool() {
1138        assert_eq!(
1139            html! {
1140                if true {
1141                    <div class="foo" />
1142                }
1143            },
1144            html! {
1145                <>
1146                    <div class="foo" />
1147                </>
1148            },
1149        );
1150        assert_eq!(
1151            html! {
1152                if false {
1153                    <div class="foo" />
1154                } else {
1155                    <div class="bar" />
1156                }
1157            },
1158            html! {
1159                <><div class="bar" /></>
1160            },
1161        );
1162        assert_eq!(
1163            html! {
1164                if false {
1165                    <div class="foo" />
1166                }
1167            },
1168            html! {
1169                <></>
1170            },
1171        );
1172
1173        // non-root tests
1174        assert_eq!(
1175            html! {
1176                <div>
1177                    if true {
1178                        <div class="foo" />
1179                    }
1180                </div>
1181            },
1182            html! {
1183                <div>
1184                    <><div class="foo" /></>
1185                </div>
1186            },
1187        );
1188        assert_eq!(
1189            html! {
1190                <div>
1191                    if false {
1192                        <div class="foo" />
1193                    } else {
1194                        <div class="bar" />
1195                    }
1196                </div>
1197            },
1198            html! {
1199                <div>
1200                    <><div class="bar" /></>
1201                </div>
1202            },
1203        );
1204        assert_eq!(
1205            html! {
1206                <div>
1207                    if false {
1208                        <div class="foo" />
1209                    }
1210                </div>
1211            },
1212            html! {
1213                <div>
1214                    <></>
1215                </div>
1216            },
1217        );
1218    }
1219
1220    #[test]
1221    fn html_if_option() {
1222        let option_foo = Some("foo");
1223        let none: Option<&'static str> = None;
1224        assert_eq!(
1225            html! {
1226                if let Some(class) = option_foo {
1227                    <div class={class} />
1228                }
1229            },
1230            html! {
1231                <>
1232                    <div class={Some("foo")} />
1233                </>
1234            },
1235        );
1236        assert_eq!(
1237            html! {
1238                if let Some(class) = none {
1239                    <div class={class} />
1240                } else {
1241                    <div class="bar" />
1242                }
1243            },
1244            html! {
1245                <>
1246                    <div class="bar" />
1247                </>
1248            },
1249        );
1250        assert_eq!(
1251            html! {
1252                if let Some(class) = none {
1253                    <div class={class} />
1254                }
1255            },
1256            html! {
1257                <></>
1258            },
1259        );
1260
1261        // non-root tests
1262        assert_eq!(
1263            html! {
1264                <div>
1265                    if let Some(class) = option_foo {
1266                        <div class={class} />
1267                    }
1268                </div>
1269            },
1270            html! {
1271                <div>
1272                    <>
1273                        <div class={Some("foo")} />
1274                    </>
1275                </div>
1276            },
1277        );
1278        assert_eq!(
1279            html! {
1280                <div>
1281                    if let Some(class) = none {
1282                        <div class={class} />
1283                    } else {
1284                        <div class="bar" />
1285                    }
1286                </div>
1287            },
1288            html! {
1289                <div>
1290                    <>
1291                        <div class="bar" />
1292                    </>
1293                </div>
1294            },
1295        );
1296        assert_eq!(
1297            html! {
1298                <div>
1299                    if let Some(class) = none {
1300                        <div class={class} />
1301                    }
1302                </div>
1303            },
1304            html! { <div><></></div> },
1305        );
1306    }
1307
1308    #[test]
1309    fn input_checked_stays_there() {
1310        let tag = html! {
1311            <input checked={true} />
1312        };
1313        match tag {
1314            VNode::VTag(tag) => {
1315                assert_eq!(tag.checked(), Some(true));
1316            }
1317            _ => unreachable!(),
1318        }
1319    }
1320    #[test]
1321    fn non_input_checked_stays_there() {
1322        let tag = html! {
1323            <my-el checked="true" />
1324        };
1325        match tag {
1326            VNode::VTag(tag) => {
1327                assert_eq!(
1328                    tag.attributes.iter().find(|(k, _)| *k == "checked"),
1329                    Some(("checked", "true"))
1330                );
1331            }
1332            _ => unreachable!(),
1333        }
1334    }
1335}