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