1mod 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
27trait Apply {
29 type Element;
31 type Bundle;
32
33 fn apply(self, root: &BSubtree, el: &Self::Element) -> Self::Bundle;
35
36 fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self::Bundle);
38}
39
40#[derive(Debug)]
43enum BTagInner {
44 Input(InputFields),
47 Textarea {
50 value: Value<TextAreaElement>,
53 },
54 Other {
56 tag: AttrValue,
58 child_bundle: BNode,
60 },
61}
62
63#[derive(Debug)]
65pub(super) struct BTag {
66 inner: BTagInner,
68 listeners: ListenerRegistration,
69 attributes: Attributes,
70 reference: Element,
72 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 if let BTagInner::Other { child_bundle, .. } = self.inner {
84 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 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 let attributes = attributes.apply(root, &el);
131 let listeners = listeners.apply(root, &el);
132
133 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 match bundle {
176 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 _ => 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 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 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 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 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 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 assert_eq!(
393 el_tag_name, tag_name,
394 "expected element of kind {tag_name}, found {el_tag_name}.",
395 );
396 }
397 }
398 let attributes = attributes.apply(root, &el);
400 let listeners = listeners.apply(root, &el);
401
402 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 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 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 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 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 elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
815 let vtag = assert_btag_ref(&elem);
816
817 let input_ref = &vtag.reference();
819 let input = input_ref.dyn_ref::<InputElement>().unwrap();
820
821 let current_value = input.value();
822
823 assert_eq!(current_value, expected);
825 }
826
827 #[test]
828 fn uncontrolled_input_unsynced() {
829 let (root, scope, parent) = setup_parent();
830
831 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 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 elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
846 let vtag = assert_btag_ref(&elem);
847
848 let input_ref = &vtag.reference();
850 let input = input_ref.dyn_ref::<InputElement>().unwrap();
851
852 let current_value = input.value();
853
854 assert_eq!(current_value, "User input");
856
857 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 assert_eq!(vtag.tag(), "a");
876
877 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 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 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 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 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]
995 fn test_index_map_attribute_diff() {
996 let (root, scope, parent) = setup_parent();
997
998 let test_ref = NodeRef::default();
999
1000 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 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 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 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 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}