diff --git a/examples/crm/src/markdown.rs b/examples/crm/src/markdown.rs index 2a239806f4b..b809a4a4d9c 100644 --- a/examples/crm/src/markdown.rs +++ b/examples/crm/src/markdown.rs @@ -1,9 +1,26 @@ /// Original author of this code is [Nathan Ringo](https://github.com/remexre) /// Source: https://github.com/acmumn/mentoring/blob/master/web-client/src/view/markdown.rs use pulldown_cmark::{Alignment, CodeBlockKind, Event, Options, Parser, Tag}; -use yew::virtual_dom::{VNode, VTag, VText}; +use yew::virtual_dom::{Classes, VNode, VTag, VText}; use yew::{html, Html}; +/// Adds a class to the VTag. +/// You can also provide multiple classes separated by ascii whitespaces. +/// +/// Note that this has a complexity of O(n), +/// where n is the number of classes already in VTag plus +/// the number of classes to be added. +fn add_class(vtag: &mut VTag, class: &str) { + let mut classes: Classes = vtag + .attributes + .get("class") + .map(AsRef::as_ref) + .unwrap_or("") + .into(); + classes.push(class); + vtag.add_attribute("class", &classes); +} + /// Renders a string of Markdown to HTML with the default options (footnotes /// disabled, tables enabled). pub fn render_markdown(src: &str) -> Html { @@ -42,9 +59,9 @@ pub fn render_markdown(src: &str) -> Html { if let VNode::VTag(ref mut vtag) = c { match aligns[i] { Alignment::None => {} - Alignment::Left => vtag.add_class("text-left"), - Alignment::Center => vtag.add_class("text-center"), - Alignment::Right => vtag.add_class("text-right"), + Alignment::Left => add_class(vtag, "text-left"), + Alignment::Center => add_class(vtag, "text-center"), + Alignment::Right => add_class(vtag, "text-right"), } } } @@ -92,7 +109,7 @@ fn make_tag(t: Tag) -> VTag { } Tag::BlockQuote => { let mut el = VTag::new("blockquote"); - el.add_class("blockquote"); + el.add_attribute("class", &"blockquote"); el } Tag::CodeBlock(code_block_kind) => { @@ -104,10 +121,10 @@ fn make_tag(t: Tag) -> VTag { // highlighting support by locating the language classes and applying dom transforms // on their contents. match lang.as_ref() { - "html" => el.add_class("html-language"), - "rust" => el.add_class("rust-language"), - "java" => el.add_class("java-language"), - "c" => el.add_class("c-language"), + "html" => el.add_attribute("class", &"html-language"), + "rust" => el.add_attribute("class", &"rust-language"), + "java" => el.add_attribute("class", &"java-language"), + "c" => el.add_attribute("class", &"c-language"), _ => {} // Add your own language highlighting support }; } @@ -124,7 +141,7 @@ fn make_tag(t: Tag) -> VTag { Tag::Item => VTag::new("li"), Tag::Table(_) => { let mut el = VTag::new("table"); - el.add_class("table"); + el.add_attribute("class", &"table"); el } Tag::TableHead => VTag::new("th"), @@ -132,12 +149,12 @@ fn make_tag(t: Tag) -> VTag { Tag::TableCell => VTag::new("td"), Tag::Emphasis => { let mut el = VTag::new("span"); - el.add_class("font-italic"); + el.add_attribute("class", &"font-italic"); el } Tag::Strong => { let mut el = VTag::new("span"); - el.add_class("font-weight-bold"); + el.add_attribute("class", &"font-weight-bold"); el } Tag::Link(_link_type, ref href, ref title) => { @@ -161,7 +178,7 @@ fn make_tag(t: Tag) -> VTag { Tag::FootnoteDefinition(ref _footnote_id) => VTag::new("span"), // Footnotes are not rendered as anything special Tag::Strikethrough => { let mut el = VTag::new("span"); - el.add_class("text-decoration-strikethrough"); + el.add_attribute("class", &"text-decoration-strikethrough"); el } } diff --git a/yew-macro/src/html_tree/html_tag/mod.rs b/yew-macro/src/html_tree/html_tag/mod.rs index 1f45fc83f9e..834d56ce542 100644 --- a/yew-macro/src/html_tree/html_tag/mod.rs +++ b/yew-macro/src/html_tree/html_tag/mod.rs @@ -132,10 +132,16 @@ impl ToTokens for HtmlTag { }); let set_classes = classes.iter().map(|classes_form| match classes_form { ClassesForm::Tuple(classes) => quote! { - #vtag.add_classes(vec![#(&(#classes)),*]); + let __yew_classes = ::yew::virtual_dom::Classes::default()#(.extend(#classes))*; + if !__yew_classes.is_empty() { + #vtag.add_attribute("class", &__yew_classes); + } }, ClassesForm::Single(classes) => quote! { - #vtag.set_classes(#classes); + let __yew_classes = ::std::convert::Into::<::yew::virtual_dom::Classes>::into(#classes); + if !__yew_classes.is_empty() { + #vtag.add_attribute("class", &__yew_classes); + } }, }); let set_node_ref = node_ref.iter().map(|node_ref| { diff --git a/yew/src/virtual_dom/mod.rs b/yew/src/virtual_dom/mod.rs index 7093067911a..e60c2df4677 100644 --- a/yew/src/virtual_dom/mod.rs +++ b/yew/src/virtual_dom/mod.rs @@ -123,23 +123,29 @@ impl From<&str> for Classes { impl From for Classes { fn from(t: String) -> Self { - let set = t - .split_whitespace() - .map(String::from) - .filter(|c| !c.is_empty()) - .collect(); - Self { set } + Classes::from(t.as_str()) } } impl From<&String> for Classes { fn from(t: &String) -> Self { - let set = t - .split_whitespace() - .map(String::from) - .filter(|c| !c.is_empty()) - .collect(); - Self { set } + Classes::from(t.as_str()) + } +} + +impl> From> for Classes { + fn from(t: Option) -> Self { + t.as_ref() + .map(|s| >::from(s.as_ref())) + .unwrap_or_default() + } +} + +impl> From<&Option> for Classes { + fn from(t: &Option) -> Self { + t.as_ref() + .map(|s| >::from(s.as_ref())) + .unwrap_or_default() } } @@ -163,6 +169,7 @@ impl PartialEq for Classes { } /// Patch for DOM node modification. +#[derive(Debug, PartialEq)] enum Patch { Add(ID, T), Replace(ID, T), diff --git a/yew/src/virtual_dom/vtag.rs b/yew/src/virtual_dom/vtag.rs index 7a1e7d5b920..bcd7c91ec55 100644 --- a/yew/src/virtual_dom/vtag.rs +++ b/yew/src/virtual_dom/vtag.rs @@ -1,8 +1,6 @@ //! This module contains the implementation of a virtual element node `VTag`. -use super::{ - Attributes, Classes, Listener, Listeners, Patch, Reform, Transformer, VDiff, VList, VNode, -}; +use super::{Attributes, Listener, Listeners, Patch, Reform, Transformer, VDiff, VList, VNode}; use crate::html::{AnyScope, NodeRef}; use crate::utils::document; use cfg_if::cfg_if; @@ -50,8 +48,6 @@ pub struct VTag { pub attributes: Attributes, /// List of children nodes pub children: VList, - /// List of attached classes. - pub classes: Classes, /// Contains a value of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). pub value: Option, @@ -81,7 +77,6 @@ impl Clone for VTag { listeners: self.listeners.clone(), attributes: self.attributes.clone(), children: self.children.clone(), - classes: self.classes.clone(), value: self.value.clone(), kind: self.kind.clone(), checked: self.checked, @@ -98,7 +93,6 @@ impl VTag { VTag { tag: tag.into(), reference: None, - classes: Classes::new(), attributes: Attributes::new(), listeners: Vec::new(), captured: Vec::new(), @@ -130,29 +124,6 @@ impl VTag { } } - /// Adds a single class to this virtual node. Actually it will set by - /// [Element.setAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) - /// call later. - pub fn add_class(&mut self, class: &str) { - self.classes.push(class); - } - - /// Adds multiple classes to this virtual node. Actually it will set by - /// [Element.setAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) - /// call later. - pub fn add_classes(&mut self, classes: Vec<&str>) { - for class in classes { - self.classes.push(class); - } - } - - /// Add classes to this virtual node. Actually it will set by - /// [Element.setAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) - /// call later. - pub fn set_classes(&mut self, classes: impl Into) { - self.classes = classes.into(); - } - /// Sets `value` for an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). pub fn set_value(&mut self, value: &T) { @@ -175,14 +146,16 @@ impl VTag { /// Adds attribute to a virtual node. Not every attribute works when /// it set as attribute. We use workarounds for: - /// `class`, `type/kind`, `value` and `checked`. + /// `type/kind`, `value` and `checked`. + /// + /// If this virtual node has this attribute present, the value is replaced. pub fn add_attribute(&mut self, name: &str, value: &T) { self.attributes.insert(name.to_owned(), value.to_string()); } /// Adds attributes to a virtual node. Not every attribute works when /// it set as attribute. We use workarounds for: - /// `class`, `type/kind`, `value` and `checked`. + /// `type/kind`, `value` and `checked`. pub fn add_attributes(&mut self, attrs: Vec<(String, String)>) { for (name, value) in attrs { self.attributes.insert(name, value); @@ -205,25 +178,7 @@ impl VTag { } } - /// If there is no ancestor or the classes, or the order, differs from the ancestor: - /// - Returns the classes of self separated by spaces. - /// - /// Otherwise None is returned. - fn diff_classes<'a>(&'a self, ancestor: &'a Option>) -> Option { - if ancestor - .as_ref() - .map(|ancestor| self.classes.ne(&ancestor.classes)) - .unwrap_or(true) - { - Some(self.classes.to_string()) - } else { - None - } - } - - /// Similar to diff_classes except for attributes. - /// - /// This also handles patching of attributes when the keys are equal but + /// This handles patching of attributes when the keys are equal but /// the values are different. fn diff_attributes<'a>( &'a self, @@ -294,14 +249,9 @@ impl VTag { let element = self.reference.as_ref().expect("element expected"); // Update parameters - let class_str = self.diff_classes(ancestor); - if let Some(class_str) = class_str { - element - .set_attribute("class", &class_str) - .expect("could not set class"); - } - let changes = self.diff_attributes(ancestor); + + // apply attribute patches including an optional "class"-attribute patch for change in changes { match change { Patch::Add(key, value) | Patch::Replace(key, value) => { @@ -312,7 +262,7 @@ impl VTag { Patch::Remove(key) => { cfg_match! { feature = "std_web" => element.remove_attribute(&key), - feature = "web_sys" => element.remove_attribute(&key).expect("could not remove class"), + feature = "web_sys" => element.remove_attribute(&key).expect("could not remove attribute"), }; } } @@ -534,7 +484,6 @@ impl PartialEq for VTag { .map(|l| l.kind()) .eq(other.listeners.iter().map(|l| l.kind())) && self.attributes == other.attributes - && self.classes.eq(&other.classes) && self.children == other.children } } @@ -756,6 +705,23 @@ mod tests { assert_eq!(a, c); } + /// Returns the class attribute as str reference, or "" if the attribute is not set. + fn get_class_str(vtag: &VTag) -> &str { + vtag.attributes + .get("class") + .map(AsRef::as_ref) + .unwrap_or("") + } + + /// Note: Compares to "" if the class attribute is not set. + fn assert_class(vnode: VNode, class: &str) { + if let VNode::VTag(ref vtag) = vnode { + assert_eq!(get_class_str(vtag), class); + } else { + panic!("expected VTag"); + } + } + #[test] fn supports_multiple_non_unique_classes_tuple() { let a = html! { @@ -763,10 +729,9 @@ mod tests { }; if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert!(vtag.classes.contains("class-1")); - assert!(vtag.classes.contains("class-2")); - assert!(!vtag.classes.contains("class-3")); + assert!(get_class_str(&vtag).contains("class-1")); + assert!(get_class_str(&vtag).contains("class-2")); + assert!(!get_class_str(&vtag).contains("class-3")); } else { panic!("vtag expected"); } @@ -785,10 +750,9 @@ mod tests { assert_ne!(a, b); if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert!(vtag.classes.contains("class-1")); - assert!(vtag.classes.contains("class-2")); - assert!(vtag.classes.contains("class-3")); + assert!(get_class_str(&vtag).contains("class-1")); + assert!(get_class_str(&vtag).contains("class-2")); + assert!(get_class_str(&vtag).contains("class-3")); } else { panic!("vtag expected"); } @@ -803,10 +767,9 @@ mod tests { }; if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert!(vtag.classes.contains("class-1")); - assert!(vtag.classes.contains("class-2")); - assert!(!vtag.classes.contains("class-3")); + assert!(get_class_str(&vtag).contains("class-1")); + assert!(get_class_str(&vtag).contains("class-2")); + assert!(!get_class_str(&vtag).contains("class-3")); } else { panic!("vtag expected"); } @@ -820,10 +783,9 @@ mod tests { }; if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert!(vtag.classes.contains("class-1")); - assert!(vtag.classes.contains("class-2")); - assert!(!vtag.classes.contains("class-3")); + assert!(get_class_str(&vtag).contains("class-1")); + assert!(get_class_str(&vtag).contains("class-2")); + assert!(!get_class_str(&vtag).contains("class-3")); } else { panic!("vtag expected"); } @@ -838,19 +800,19 @@ mod tests { let c = html! {
}; if let VNode::VTag(vtag) = a { - assert!(vtag.classes.is_empty()); + assert!(!vtag.attributes.contains_key("class")); } else { panic!("vtag expected"); } if let VNode::VTag(vtag) = b { - assert!(vtag.classes.is_empty()); + assert!(!vtag.attributes.contains_key("class")); } else { panic!("vtag expected"); } if let VNode::VTag(vtag) = c { - assert!(vtag.classes.is_empty()); + assert!(!vtag.attributes.contains_key("class")); } else { panic!("vtag expected"); } @@ -884,7 +846,7 @@ mod tests { let namespace = Some(namespace); let svg_el = document.create_element_ns(namespace, "svg").unwrap(); - let mut g_node = html! { }; + let mut g_node = html! { }; let path_node = html! { }; let mut svg_node = html! { {path_node} }; @@ -910,8 +872,7 @@ mod tests { }; if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert_eq!(vtag.classes.to_string(), "class-1 class-2 class-3"); + assert_eq!(get_class_str(&vtag), "class-1 class-2 class-3"); } } @@ -1019,6 +980,96 @@ mod tests { html! { }; } + #[test] + fn it_does_not_set_empty_class_name() { + let scope = AnyScope::default(); + let parent = document().create_element("div").unwrap(); + + #[cfg(feature = "std_web")] + document().body().unwrap().append_child(&parent); + #[cfg(feature = "web_sys")] + document().body().unwrap().append_child(&parent).unwrap(); + + let mut elem = html! {
}; + elem.apply(&scope, &parent, None, None); + let vtag = assert_vtag(&mut elem); + // test if the className has not been set + assert!(!vtag.reference.as_ref().unwrap().has_attribute("class")); + } + + #[test] + fn it_does_not_set_missing_class_name() { + let scope = AnyScope::default(); + let parent = document().create_element("div").unwrap(); + + #[cfg(feature = "std_web")] + document().body().unwrap().append_child(&parent); + #[cfg(feature = "web_sys")] + document().body().unwrap().append_child(&parent).unwrap(); + + let mut elem = html! {
}; + elem.apply(&scope, &parent, None, None); + let vtag = assert_vtag(&mut elem); + // test if the className has not been set + assert!(!vtag.reference.as_ref().unwrap().has_attribute("class")); + } + + #[test] + fn it_sets_class_name() { + let scope = AnyScope::default(); + let parent = document().create_element("div").unwrap(); + + #[cfg(feature = "std_web")] + document().body().unwrap().append_child(&parent); + #[cfg(feature = "web_sys")] + document().body().unwrap().append_child(&parent).unwrap(); + + let mut elem = html! {
}; + elem.apply(&scope, &parent, None, None); + let vtag = assert_vtag(&mut elem); + // test if the className has been set + assert!(vtag.reference.as_ref().unwrap().has_attribute("class")); + } + + #[test] + fn tuple_different_types() { + // check if tuples containing different types are compiling + assert_class( + html! {
}, + "class-1 class-2 class-3 class-4", + ); + assert_class( + html! {
}, + "class-1 class-2 class-3 class-4", + ); + // check different string references + let str = "some-class"; + let string = str.to_string(); + let string_ref = &string; + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + assert_class(html! {

}, "some-class"); + // check with None + assert_class(html! {

::None /> }, ""); + assert_class(html! {

::None /> }, ""); + // check with variables + let some: Option<&'static str> = Some("some"); + let none: Option<&'static str> = None; + assert_class(html! {

}, "some"); + assert_class(html! {

}, ""); + // check with variables of different type + let some: Option = Some(false); + let none: Option = None; + assert_class(html! {

}, "false"); + assert_class(html! {

}, ""); + } + #[test] fn swap_order_of_classes() { let scope = AnyScope::default(); @@ -1039,7 +1090,7 @@ mod tests { }; let expected = "class-1 class-2 class-3"; - assert_eq!(vtag.classes.to_string(), expected); + assert_eq!(get_class_str(&vtag), expected); assert_eq!( vtag.reference .as_ref() @@ -1059,7 +1110,7 @@ mod tests { vtag.apply(&scope, &parent, None, Some(VNode::VTag(ancestor))); let expected = "class-3 class-2 class-1"; - assert_eq!(vtag.classes.to_string(), expected); + assert_eq!(get_class_str(&vtag), expected); assert_eq!( vtag.reference .as_ref() @@ -1090,7 +1141,7 @@ mod tests { }; let expected = "class-1 class-3"; - assert_eq!(vtag.classes.to_string(), expected); + assert_eq!(get_class_str(&vtag), expected); assert_eq!( vtag.reference .as_ref() @@ -1110,7 +1161,7 @@ mod tests { vtag.apply(&scope, &parent, None, Some(VNode::VTag(ancestor))); let expected = "class-1 class-2 class-3"; - assert_eq!(vtag.classes.to_string(), expected); + assert_eq!(get_class_str(&vtag), expected); assert_eq!( vtag.reference .as_ref()