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