use super::{
AttrValue, Attributes, Key, Listener, Listeners, Patch, PositionalAttr, VDiff, VList, VNode,
};
use crate::html::{AnyScope, IntoOptPropValue, IntoPropValue, NodeRef};
use crate::utils::document;
use cfg_if::cfg_if;
use cfg_match::cfg_match;
use log::warn;
use std::borrow::Cow;
use std::cmp::PartialEq;
use std::rc::Rc;
cfg_if! {
if #[cfg(feature = "std_web")] {
use crate::html::EventListener;
#[allow(unused_imports)]
use stdweb::{_js_impl, js};
use stdweb::unstable::TryFrom;
use stdweb::web::html_element::{InputElement, TextAreaElement};
use stdweb::web::{Element, IElement, INode};
} else if #[cfg(feature = "web_sys")] {
use gloo::events::EventListener;
use std::ops::Deref;
use wasm_bindgen::JsCast;
use web_sys::{
Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement, HtmlButtonElement
};
}
}
pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
#[derive(Clone, Copy, Debug, PartialEq)]
enum ElementType {
Input,
Textarea,
Button,
Other,
}
impl ElementType {
fn from_tag(tag: &str) -> Self {
match tag.to_ascii_lowercase().as_str() {
"input" => Self::Input,
"textarea" => Self::Textarea,
"button" => Self::Button,
_ => Self::Other,
}
}
}
#[derive(Debug)]
pub struct VTag {
tag: Cow<'static, str>,
element_type: ElementType,
pub reference: Option<Element>,
pub listeners: Listeners,
pub attributes: Attributes,
pub children: VList,
pub value: Option<AttrValue>,
pub kind: Option<AttrValue>,
pub checked: bool,
pub node_ref: NodeRef,
captured: Vec<EventListener>,
pub key: Option<Key>,
}
impl Clone for VTag {
fn clone(&self) -> Self {
VTag {
tag: self.tag.clone(),
element_type: self.element_type,
reference: None,
listeners: self.listeners.clone(),
attributes: self.attributes.clone(),
children: self.children.clone(),
value: self.value.clone(),
kind: self.kind.clone(),
checked: self.checked,
node_ref: self.node_ref.clone(),
key: self.key.clone(),
captured: Vec::new(),
}
}
}
impl VTag {
pub fn new(tag: impl Into<Cow<'static, str>>) -> Self {
let tag = tag.into();
let element_type = ElementType::from_tag(&tag);
VTag {
tag,
element_type,
reference: None,
attributes: Attributes::new(),
listeners: Vec::new(),
captured: Vec::new(),
children: VList::new(),
node_ref: NodeRef::default(),
key: None,
value: None,
kind: None,
checked: false,
}
}
pub fn tag(&self) -> &str {
&self.tag
}
pub fn add_child(&mut self, child: VNode) {
self.children.add_child(child);
}
pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) {
self.children.add_children(children);
}
pub fn set_value(&mut self, value: impl IntoOptPropValue<AttrValue>) {
self.value = value.into_opt_prop_value();
}
pub fn set_kind(&mut self, value: impl IntoOptPropValue<AttrValue>) {
self.kind = value.into_opt_prop_value();
}
#[doc(hidden)]
pub fn __macro_set_key(&mut self, value: impl Into<Key>) {
self.key = Some(value.into())
}
pub fn set_checked(&mut self, value: bool) {
self.checked = value;
}
#[doc(hidden)]
pub fn __macro_set_node_ref(&mut self, value: impl IntoPropValue<NodeRef>) {
self.node_ref = value.into_prop_value()
}
pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
self.attributes
.get_mut_index_map()
.insert(key, value.into());
}
pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
self.attributes = attrs.into();
}
#[doc(hidden)]
pub fn __macro_push_attr(&mut self, attr: PositionalAttr) {
match &mut self.attributes {
Attributes::Vec(attrs) => attrs.push(attr),
_ => unreachable!("the macro always uses positional attributes"),
}
}
pub fn add_listener(&mut self, listener: Rc<dyn Listener>) {
self.listeners.push(listener);
}
pub fn add_listeners(&mut self, listeners: Listeners) {
self.listeners.extend(listeners);
}
#[doc(hidden)]
pub fn __macro_set_listeners(
&mut self,
listeners: impl IntoIterator<Item = Option<Rc<dyn Listener>>>,
) {
self.listeners = listeners.into_iter().flatten().collect();
}
fn recreate_listeners(&mut self, ancestor: &mut Option<Box<Self>>) {
if let Some(ancestor) = ancestor.as_mut() {
ancestor.captured.clear();
}
let element = self.reference.clone().expect("element expected");
for listener in self.listeners.drain(..) {
let handle = listener.attach(&element);
self.captured.push(handle);
}
}
fn refresh_value(&mut self) {
if self.value.is_none() {
return;
}
if let Some(element) = self.reference.as_ref() {
if self.element_type == ElementType::Input {
let input_el = cfg_match! {
feature = "std_web" => InputElement::try_from(element.clone()).ok(),
feature = "web_sys" => element.dyn_ref::<InputElement>(),
};
if let Some(input) = input_el {
let current_value = cfg_match! {
feature = "std_web" => input.raw_value(),
feature = "web_sys" => input.value(),
};
self.set_value(Cow::Owned(current_value));
}
} else if self.element_type == ElementType::Textarea {
let textarea_el = cfg_match! {
feature = "std_web" => TextAreaElement::try_from(element.clone()).ok(),
feature = "web_sys" => element.dyn_ref::<TextAreaElement>(),
};
if let Some(tae) = textarea_el {
let value = tae.value();
self.set_value(Cow::Owned(value));
}
}
}
}
fn diff_kind<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
match (
self.kind.as_ref(),
ancestor.as_ref().and_then(|anc| anc.kind.as_ref()),
) {
(Some(ref left), Some(ref right)) => {
if left != right {
Some(Patch::Replace(&**left, ()))
} else {
None
}
}
(Some(ref left), None) => Some(Patch::Add(&**left, ())),
(None, Some(right)) => Some(Patch::Remove(&**right)),
(None, None) => None,
}
}
fn diff_value<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
match (
self.value.as_ref(),
ancestor.as_ref().and_then(|anc| anc.value.as_ref()),
) {
(Some(ref left), Some(ref right)) => {
if left != right {
Some(Patch::Replace(&**left, ()))
} else {
None
}
}
(Some(ref left), None) => Some(Patch::Add(&**left, ())),
(None, Some(right)) => Some(Patch::Remove(&**right)),
(None, None) => None,
}
}
fn apply_diffs(&mut self, ancestor: &mut Option<Box<Self>>) {
let changes = if let Some(old_attributes) = ancestor.as_mut().map(|a| &mut a.attributes) {
Attributes::diff(&self.attributes, old_attributes)
} else {
self.attributes
.iter()
.map(|(k, v)| Patch::Add(k, v))
.collect()
};
let element = self.reference.as_ref().expect("element expected");
for change in changes {
match change {
Patch::Add(key, value) | Patch::Replace(key, value) => {
element
.set_attribute(&key, &value)
.expect("invalid attribute key");
}
Patch::Remove(key) => {
cfg_match! {
feature = "std_web" => element.remove_attribute(&key),
feature = "web_sys" => element.remove_attribute(&key)
.expect("could not remove attribute"),
};
}
}
}
#[cfg(feature = "web_sys")]
{
if self.element_type == ElementType::Button {
if let Some(button) = element.dyn_ref::<HtmlButtonElement>() {
if let Some(change) = self.diff_kind(ancestor) {
let kind = match change {
Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
Patch::Remove(_) => "",
};
button.set_type(kind);
}
}
}
}
if self.element_type == ElementType::Input {
if let Some(input) = {
cfg_match! {
feature = "std_web" => InputElement::try_from(element.clone()).ok(),
feature = "web_sys" => element.dyn_ref::<InputElement>(),
}
} {
if let Some(change) = self.diff_kind(ancestor) {
let kind = match change {
Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
Patch::Remove(_) => "",
};
cfg_match! {
feature = "std_web" => ({
let input = &input;
js! { @(no_return)
@{input}.type = @{kind};
}
}),
feature = "web_sys" => input.set_type(kind),
}
}
if let Some(change) = self.diff_value(ancestor) {
let raw_value = match change {
Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
Patch::Remove(_) => "",
};
cfg_match! {
feature = "std_web" => input.set_raw_value(raw_value),
feature = "web_sys" => input.set_value(raw_value),
};
}
set_checked(&input, self.checked);
}
} else if self.element_type == ElementType::Textarea {
if let Some(tae) = {
cfg_match! {
feature = "std_web" => TextAreaElement::try_from(element.clone()).ok(),
feature = "web_sys" => element.dyn_ref::<TextAreaElement>(),
}
} {
if let Some(change) = self.diff_value(ancestor) {
let value = match change {
Patch::Add(kind, _) | Patch::Replace(kind, _) => kind,
Patch::Remove(_) => "",
};
tae.set_value(value);
}
}
}
}
fn create_element(&self, parent: &Element) -> Element {
let tag = self.tag();
if tag == "svg"
|| parent
.namespace_uri()
.map_or(false, |ns| ns == SVG_NAMESPACE)
{
let namespace = cfg_match! {
feature = "std_web" => SVG_NAMESPACE,
feature = "web_sys" => Some(SVG_NAMESPACE),
};
document()
.create_element_ns(namespace, tag)
.expect("can't create namespaced element for vtag")
} else {
document()
.create_element(tag)
.expect("can't create element for vtag")
}
}
}
impl VDiff for VTag {
fn detach(&mut self, parent: &Element) {
let node = self
.reference
.take()
.expect("tried to remove not rendered VTag from DOM");
self.children.detach(&node);
if parent.remove_child(&node).is_err() {
warn!("Node not found to remove VTag");
}
self.node_ref.set(None);
}
fn apply(
&mut self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef {
let mut ancestor_tag = ancestor.and_then(|mut ancestor| {
match ancestor {
VNode::VTag(vtag) if self.tag() == vtag.tag() && self.key == vtag.key => Some(vtag),
_ => {
let element = self.create_element(parent);
super::insert_node(&element, parent, Some(ancestor.first_node()));
self.reference = Some(element);
ancestor.detach(parent);
None
}
}
});
if let Some(ref mut ancestor_tag) = &mut ancestor_tag {
ancestor_tag.refresh_value();
self.reference = ancestor_tag.reference.take();
} else if self.reference.is_none() {
let element = self.create_element(parent);
super::insert_node(&element, parent, next_sibling.get());
self.reference = Some(element);
}
self.apply_diffs(&mut ancestor_tag);
self.recreate_listeners(&mut ancestor_tag);
let element = self.reference.as_ref().expect("Reference should be set");
if !self.children.is_empty() {
self.children.apply(
parent_scope,
element,
NodeRef::default(),
ancestor_tag.map(|a| a.children.into()),
);
} else if let Some(mut ancestor_tag) = ancestor_tag {
ancestor_tag.children.detach(element);
}
let node = cfg_match! {
feature = "std_web" => element.as_node(),
feature = "web_sys" => element.deref(),
};
self.node_ref.set(Some(node.clone()));
self.node_ref.clone()
}
}
fn set_checked(input: &InputElement, value: bool) {
cfg_match! {
feature = "std_web" => js!( @(no_return) @{input}.checked = @{value}; ),
feature = "web_sys" => input.set_checked(value),
};
}
impl PartialEq for VTag {
fn eq(&self, other: &VTag) -> bool {
self.tag == other.tag
&& self.value == other.value
&& self.kind == other.kind
&& self.checked == other.checked
&& self.listeners.len() == other.listeners.len()
&& self
.listeners
.iter()
.map(|l| l.kind())
.eq(other.listeners.iter().map(|l| l.kind()))
&& self.attributes == other.attributes
&& self.children == other.children
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::html;
use std::any::TypeId;
#[cfg(feature = "std_web")]
use stdweb::web::{document, IElement};
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
fn test_scope() -> AnyScope {
AnyScope {
type_id: TypeId::of::<()>(),
parent: None,
state: Rc::new(()),
}
}
#[test]
fn it_compares_tags() {
let a = html! {
<div></div>
};
let b = html! {
<div></div>
};
let c = html! {
<p></p>
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn it_compares_text() {
let a = html! {
<div>{ "correct" }</div>
};
let b = html! {
<div>{ "correct" }</div>
};
let c = html! {
<div>{ "incorrect" }</div>
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn it_compares_attributes() {
let a = html! {
<div a="test"></div>
};
let b = html! {
<div a="test"></div>
};
let c = html! {
<div a="fail"></div>
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn it_compares_children() {
let a = html! {
<div>
<p></p>
</div>
};
let b = html! {
<div>
<p></p>
</div>
};
let c = html! {
<div>
<span></span>
</div>
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn it_compares_classes() {
let a = html! {
<div class="test"></div>
};
let b = html! {
<div class="test"></div>
};
let c = html! {
<div class="fail"></div>
};
let d = html! {
<div class=format!("fail{}", "")></div>
};
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(c, d);
}
fn assert_vtag(node: &mut VNode) -> &mut VTag {
if let VNode::VTag(vtag) = node {
return vtag;
}
panic!("should be vtag");
}
fn assert_namespace(vtag: &VTag, namespace: &'static str) {
assert_eq!(
vtag.reference.as_ref().unwrap().namespace_uri().unwrap(),
namespace
);
}
#[test]
fn supports_svg() {
#[cfg(feature = "std_web")]
let document = document();
#[cfg(feature = "web_sys")]
let document = web_sys::window().unwrap().document().unwrap();
let scope = test_scope();
let div_el = document.create_element("div").unwrap();
let namespace = SVG_NAMESPACE;
#[cfg(feature = "web_sys")]
let namespace = Some(namespace);
let svg_el = document.create_element_ns(namespace, "svg").unwrap();
let mut g_node = html! { <g class="segment"></g> };
let path_node = html! { <path></path> };
let mut svg_node = html! { <svg>{path_node}</svg> };
let svg_tag = assert_vtag(&mut svg_node);
svg_tag.apply(&scope, &div_el, NodeRef::default(), None);
assert_namespace(svg_tag, SVG_NAMESPACE);
let path_tag = assert_vtag(svg_tag.children.get_mut(0).unwrap());
assert_namespace(path_tag, SVG_NAMESPACE);
let g_tag = assert_vtag(&mut g_node);
g_tag.apply(&scope, &div_el, NodeRef::default(), None);
assert_namespace(g_tag, HTML_NAMESPACE);
g_tag.reference = None;
g_tag.apply(&scope, &svg_el, NodeRef::default(), None);
assert_namespace(g_tag, SVG_NAMESPACE);
}
#[test]
fn it_compares_values() {
let a = html! {
<input value="test"/>
};
let b = html! {
<input value="test"/>
};
let c = html! {
<input value="fail"/>
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn it_compares_kinds() {
let a = html! {
<input type="text"/>
};
let b = html! {
<input type="text"/>
};
let c = html! {
<input type="hidden"/>
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn it_compares_checked() {
let a = html! {
<input type="checkbox" checked=false />
};
let b = html! {
<input type="checkbox" checked=false />
};
let c = html! {
<input type="checkbox" checked=true />
};
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn it_allows_aria_attributes() {
let a = html! {
<p aria-controls="it-works">
<a class="btn btn-primary"
data-toggle="collapse"
href="#collapseExample"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
{ "Link with href" }
</a>
<button class="btn btn-primary"
type="button"
data-toggle="collapse"
data-target="#collapseExample"
aria-expanded="false"
aria-controls="collapseExample">
{ "Button with data-target" }
</button>
<div own-attribute-with-multiple-parts="works" />
</p>
};
if let VNode::VTag(vtag) = a {
assert_eq!(
vtag.attributes
.iter()
.find(|(k, _)| k == &"aria-controls")
.map(|(_, v)| v),
Some("it-works")
);
} else {
panic!("vtag expected");
}
}
#[test]
fn it_does_not_set_missing_class_name() {
let scope = test_scope();
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! { <div></div> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = assert_vtag(&mut elem);
assert!(!vtag.reference.as_ref().unwrap().has_attribute("class"));
}
#[test]
fn it_sets_class_name() {
let scope = test_scope();
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! { <div class="ferris the crab"></div> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = assert_vtag(&mut elem);
assert!(vtag.reference.as_ref().unwrap().has_attribute("class"));
}
#[test]
fn controlled_input_synced() {
let scope = test_scope();
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 expected = "not_changed_value";
let mut elem = html! { <input value=expected /> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = if let VNode::VTag(vtag) = elem {
vtag
} else {
panic!("should be vtag")
};
let input_ref = vtag.reference.as_ref().unwrap();
let input = cfg_match! {
feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
};
cfg_match! {
feature = "std_web" => input.unwrap().set_raw_value("User input"),
feature = "web_sys" => input.unwrap().set_value("User input"),
};
let ancestor = vtag;
let mut elem = html! { <input value=expected /> };
let vtag = assert_vtag(&mut elem);
vtag.apply(
&scope,
&parent,
NodeRef::default(),
Some(VNode::VTag(ancestor)),
);
let input_ref = vtag.reference.as_ref().unwrap();
let input = cfg_match! {
feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
}
.unwrap();
let current_value = cfg_match! {
feature = "std_web" => input.raw_value(),
feature = "web_sys" => input.value(),
};
assert_eq!(current_value, expected);
}
#[test]
fn uncontrolled_input_unsynced() {
let scope = test_scope();
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! { <input /> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = if let VNode::VTag(vtag) = elem {
vtag
} else {
panic!("should be vtag")
};
let input_ref = vtag.reference.as_ref().unwrap();
let input = cfg_match! {
feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
};
cfg_match! {
feature = "std_web" => input.unwrap().set_raw_value("User input"),
feature = "web_sys" => input.unwrap().set_value("User input"),
};
let ancestor = vtag;
let mut elem = html! { <input /> };
let vtag = assert_vtag(&mut elem);
vtag.apply(
&scope,
&parent,
NodeRef::default(),
Some(VNode::VTag(ancestor)),
);
let input_ref = vtag.reference.as_ref().unwrap();
let input = cfg_match! {
feature = "std_web" => InputElement::try_from(input_ref.clone()).ok(),
feature = "web_sys" => input_ref.dyn_ref::<InputElement>(),
}
.unwrap();
let current_value = cfg_match! {
feature = "std_web" => input.raw_value(),
feature = "web_sys" => input.value(),
};
assert_eq!(current_value, "User input");
}
#[test]
fn dynamic_tags_work() {
let scope = test_scope();
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! { <@{
let mut builder = String::new();
builder.push('a');
builder
}/> };
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = assert_vtag(&mut elem);
assert_eq!(vtag.tag(), "a");
#[cfg(feature = "web_sys")]
assert_eq!(vtag.reference.as_ref().unwrap().tag_name(), "A");
}
#[test]
fn dynamic_tags_handle_value_attribute() {
let mut div_el = html! {
<@{"div"} value="Hello"/>
};
let div_vtag = assert_vtag(&mut div_el);
assert!(div_vtag.value.is_none());
let v: Option<&str> = div_vtag
.attributes
.iter()
.find(|(k, _)| k == &"value")
.map(|(_, v)| AsRef::as_ref(v));
assert_eq!(v, Some("Hello"));
let mut input_el = html! {
<@{"input"} value="World"/>
};
let input_vtag = assert_vtag(&mut input_el);
assert_eq!(input_vtag.value, Some(Cow::Borrowed("World")));
assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value"));
}
#[test]
fn dynamic_tags_handle_weird_capitalization() {
let mut el = html! {
<@{"tExTAREa"}/>
};
let vtag = assert_vtag(&mut el);
assert_eq!(vtag.tag(), "textarea");
}
#[test]
fn reset_node_ref() {
let scope = test_scope();
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 node_ref = NodeRef::default();
let mut elem: VNode = html! { <div ref=node_ref.clone()></div> };
assert_vtag(&mut elem);
elem.apply(&scope, &parent, NodeRef::default(), None);
let parent_node = cfg_match! {
feature = "std_web" => parent.as_node(),
feature = "web_sys" => parent.deref(),
};
assert_eq!(node_ref.get(), parent_node.first_child());
elem.detach(&parent);
assert!(node_ref.get().is_none());
}
fn get_class_str(vtag: &VTag) -> &str {
vtag.attributes
.iter()
.find(|(k, _)| k == &"class")
.map(|(_, v)| AsRef::as_ref(v))
.unwrap_or("")
}
#[test]
fn old_class_syntax_is_still_supported() {
let a_classes = "class-1 class-2".to_string();
#[allow(deprecated)]
let a = html! {
<div class=("class-1", a_classes)></div>
};
if let VNode::VTag(vtag) = a {
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");
}
}
}
#[cfg(all(test, feature = "web_sys"))]
mod layout_tests {
extern crate self as yew;
use crate::html;
use crate::virtual_dom::layout_tests::{diff_layouts, TestLayout};
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn diff() {
let layout1 = TestLayout {
name: "1",
node: html! {
<ul>
<li>
{"a"}
</li>
<li>
{"b"}
</li>
</ul>
},
expected: "<ul><li>a</li><li>b</li></ul>",
};
let layout2 = TestLayout {
name: "2",
node: html! {
<ul>
<li>
{"a"}
</li>
<li>
{"b"}
</li>
<li>
{"d"}
</li>
</ul>
},
expected: "<ul><li>a</li><li>b</li><li>d</li></ul>",
};
let layout3 = TestLayout {
name: "3",
node: html! {
<ul>
<li>
{"a"}
</li>
<li>
{"b"}
</li>
<li>
{"c"}
</li>
<li>
{"d"}
</li>
</ul>
},
expected: "<ul><li>a</li><li>b</li><li>c</li><li>d</li></ul>",
};
let layout4 = TestLayout {
name: "4",
node: html! {
<ul>
<li>
<>
{"a"}
</>
</li>
<li>
{"b"}
<li>
{"c"}
</li>
<li>
{"d"}
</li>
</li>
</ul>
},
expected: "<ul><li>a</li><li>b<li>c</li><li>d</li></li></ul>",
};
diff_layouts(vec![layout1, layout2, layout3, layout4]);
}
}