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;
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
25trait Apply {
27 type Element;
29 type Bundle;
30
31 fn apply(self, root: &BSubtree, el: &Self::Element) -> Self::Bundle;
33
34 fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self::Bundle);
36}
37
38#[derive(Debug)]
41enum BTagInner {
42 Input(InputFields),
45 Textarea {
48 value: Value<TextAreaElement>,
51 },
52 Other {
54 tag: AttrValue,
56 child_bundle: BNode,
58 },
59}
60
61#[derive(Debug)]
63pub(super) struct BTag {
64 inner: BTagInner,
66 listeners: ListenerRegistration,
67 attributes: Attributes,
68 reference: Element,
70 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 if let BTagInner::Other { child_bundle, .. } = self.inner {
82 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 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 match bundle {
169 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 _ => 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 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 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 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 let attributes = attributes.apply(root, &el);
378 let listeners = listeners.apply(root, &el);
379
380 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 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 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 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 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 elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
793 let vtag = assert_btag_ref(&elem);
794
795 let input_ref = &vtag.reference();
797 let input = input_ref.dyn_ref::<InputElement>().unwrap();
798
799 let current_value = input.value();
800
801 assert_eq!(current_value, expected);
803 }
804
805 #[test]
806 fn uncontrolled_input_unsynced() {
807 let (root, scope, parent) = setup_parent();
808
809 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 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 elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem);
824 let vtag = assert_btag_ref(&elem);
825
826 let input_ref = &vtag.reference();
828 let input = input_ref.dyn_ref::<InputElement>().unwrap();
829
830 let current_value = input.value();
831
832 assert_eq!(current_value, "User input");
834
835 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 assert_eq!(vtag.tag(), "a");
854
855 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 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 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 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 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]
973 fn test_index_map_attribute_diff() {
974 let (root, scope, parent) = setup_parent();
975
976 let test_ref = NodeRef::default();
977
978 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 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 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 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 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}