From 1c2dff18b10fd2dc03022095f655684d412ef6ab Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 28 May 2020 15:39:26 +0200 Subject: [PATCH] Implement dynamic tags (#1266) * implement dynamic tags * very minor code cleanup * Add hint to 'closing tag with body' error * correcting a few ill-formed quote_spanned usages * improve html prop error message * use 'fragment' instead of 'tag' * add runtime checks for dynamic tags * fix tests for stdweb By only running it for web-sys. I know, I know. But seriously, stdweb doesn't seem to have a way to get Element.tagName... * allow entire ascii range for dynamic tag names * handle weird lettercasing for tag names --- yew-macro/src/html_tree/html_component.rs | 2 +- yew-macro/src/html_tree/html_dashed_name.rs | 8 +- yew-macro/src/html_tree/html_list.rs | 8 +- yew-macro/src/html_tree/html_prop.rs | 8 +- yew-macro/src/html_tree/html_tag/mod.rs | 264 +++++++++++++++--- .../tests/macro/html-component-fail.stderr | 6 +- yew-macro/tests/macro/html-list-fail.stderr | 18 +- yew-macro/tests/macro/html-tag-fail.rs | 6 + yew-macro/tests/macro/html-tag-fail.stderr | 47 ++++ yew-macro/tests/macro/html-tag-pass.rs | 17 ++ yew-macro/tests/macro_test.rs | 22 ++ yew/src/virtual_dom/vtag.rs | 55 ++++ 12 files changed, 398 insertions(+), 63 deletions(-) diff --git a/yew-macro/src/html_tree/html_component.rs b/yew-macro/src/html_tree/html_component.rs index ecbbc014c66..8e59e44c59a 100644 --- a/yew-macro/src/html_tree/html_component.rs +++ b/yew-macro/src/html_tree/html_component.rs @@ -155,7 +155,7 @@ impl ToTokens for HtmlComponent { }; let key = if let Some(key) = props.key() { - quote_spanned! { key.span() => Some(#key) } + quote_spanned! { key.span()=> Some(#key) } } else { quote! {None } }; diff --git a/yew-macro/src/html_tree/html_dashed_name.rs b/yew-macro/src/html_tree/html_dashed_name.rs index f6b894fddf3..380828f163d 100644 --- a/yew-macro/src/html_tree/html_dashed_name.rs +++ b/yew-macro/src/html_tree/html_dashed_name.rs @@ -9,7 +9,7 @@ use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream, Result as ParseResult}; use syn::Token; -#[derive(PartialEq)] +#[derive(Clone, PartialEq)] pub struct HtmlDashedName { pub name: Ident, pub extended: Vec<(Token![-], Ident)>, @@ -22,6 +22,12 @@ impl HtmlDashedName { extended: Vec::new(), } } + + pub fn to_ascii_lowercase_string(&self) -> String { + let mut s = self.to_string(); + s.make_ascii_lowercase(); + s + } } impl fmt::Display for HtmlDashedName { diff --git a/yew-macro/src/html_tree/html_list.rs b/yew-macro/src/html_tree/html_list.rs index 5c08fbc5043..49c5f3f0ea5 100644 --- a/yew-macro/src/html_tree/html_list.rs +++ b/yew-macro/src/html_tree/html_list.rs @@ -28,7 +28,7 @@ impl Parse for HtmlList { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( close, - "this closing tag has no corresponding opening tag", + "this closing fragment has no corresponding opening fragment", )), Err(err) => Err(err), }; @@ -38,7 +38,7 @@ impl Parse for HtmlList { if !HtmlList::verify_end(input.cursor()) { return Err(syn::Error::new_spanned( open, - "this opening tag has no corresponding closing tag", + "this opening fragment has no corresponding closing fragment", )); } @@ -60,9 +60,9 @@ impl ToTokens for HtmlList { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let children = &self.children; let key = if let Some(key) = &self.key { - quote_spanned! {key.span() => Some(#key)} + quote_spanned! {key.span()=> Some(#key)} } else { - quote! {None } + quote! {None} }; tokens.extend(quote! { ::yew::virtual_dom::VNode::VList( diff --git a/yew-macro/src/html_tree/html_prop.rs b/yew-macro/src/html_tree/html_prop.rs index 538b146186c..c8b12fc9a9b 100644 --- a/yew-macro/src/html_tree/html_prop.rs +++ b/yew-macro/src/html_tree/html_prop.rs @@ -20,9 +20,15 @@ impl PeekValue<()> for HtmlProp { impl Parse for HtmlProp { fn parse(input: ParseStream) -> ParseResult { let label = input.parse::()?; - input + let equals = input .parse::() .map_err(|_| syn::Error::new_spanned(&label, "this prop doesn't have a value"))?; + if input.is_empty() { + return Err(syn::Error::new_spanned( + equals, + "expected an expression following this equals sign", + )); + } let value = input.parse::()?; // backwards compat let _ = input.parse::(); diff --git a/yew-macro/src/html_tree/html_tag/mod.rs b/yew-macro/src/html_tree/html_tag/mod.rs index c07920247a3..50898820b0e 100644 --- a/yew-macro/src/html_tree/html_tag/mod.rs +++ b/yew-macro/src/html_tree/html_tag/mod.rs @@ -1,18 +1,18 @@ mod tag_attributes; -use super::HtmlDashedName as TagName; +use super::HtmlDashedName; use super::HtmlProp as TagAttribute; use super::HtmlPropSuffix as TagSuffix; use super::HtmlTree; use crate::{non_capitalized_ascii, Peek, PeekValue}; use boolinator::Boolinator; -use proc_macro2::Span; +use proc_macro2::{Delimiter, Span}; use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; use syn::parse; use syn::parse::{Parse, ParseStream, Result as ParseResult}; use syn::spanned::Spanned; -use syn::{Ident, Token}; +use syn::{Block, Ident, Token}; use tag_attributes::{ClassesForm, TagAttributes}; pub struct HtmlTag { @@ -51,16 +51,21 @@ impl Parse for HtmlTag { }); } - // Void elements should not have children. - // See https://html.spec.whatwg.org/multipage/syntax.html#void-elements - match open.tag_name.to_string().as_str() { - "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta" - | "param" | "source" | "track" | "wbr" => { - return Err(syn::Error::new_spanned(&open, format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", open.tag_name))); + if let TagName::Lit(name) = &open.tag_name { + // Void elements should not have children. + // See https://html.spec.whatwg.org/multipage/syntax.html#void-elements + // + // For dynamic tags this is done at runtime! + match name.to_ascii_lowercase_string().as_str() { + "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" + | "meta" | "param" | "source" | "track" | "wbr" => { + return Err(syn::Error::new_spanned(&open, format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", name))); + } + _ => {} } - _ => {} } + let open_key = open.tag_name.get_key(); let mut children: Vec = vec![]; loop { if input.is_empty() { @@ -69,8 +74,8 @@ impl Parse for HtmlTag { "this opening tag has no corresponding closing tag", )); } - if let Some(next_close_tag_name) = HtmlTagClose::peek(input.cursor()) { - if open.tag_name == next_close_tag_name { + if let Some(close_key) = HtmlTagClose::peek(input.cursor()) { + if open_key == close_key { break; } } @@ -97,7 +102,26 @@ impl ToTokens for HtmlTag { children, } = self; - let name = tag_name.to_string(); + let name = match &tag_name { + TagName::Lit(name) => { + let name_str = name.to_string(); + quote! {#name_str} + } + TagName::Expr(name) => { + let expr = &name.expr; + let vtag_name = Ident::new("__yew_vtag_name", expr.span()); + // this way we get a nice error message (with the correct span) when the expression doesn't return a valid value + quote_spanned! {expr.span()=> { + let mut #vtag_name = ::std::borrow::Cow::<'static, str>::from(#expr); + if !#vtag_name.is_ascii() { + ::std::panic!("a dynamic tag returned a tag name containing non ASCII characters: `{}`", #vtag_name); + } + // convert to lowercase because the runtime checks rely on it. + #vtag_name.to_mut().make_ascii_lowercase(); + #vtag_name + }} + } + }; let TagAttributes { classes, @@ -115,11 +139,11 @@ impl ToTokens for HtmlTag { let vtag = Ident::new("__yew_vtag", tag_name.span()); let attr_pairs = attributes.iter().map(|TagAttribute { label, value }| { let label_str = label.to_string(); - quote_spanned! {value.span() => (#label_str.to_owned(), (#value).to_string()) } + quote_spanned! {value.span()=> (#label_str.to_owned(), (#value).to_string()) } }); let set_booleans = booleans.iter().map(|TagAttribute { label, value }| { let label_str = label.to_string(); - quote_spanned! {value.span() => + quote_spanned! {value.span()=> if #value { #vtag.add_attribute(&#label_str, &#label_str); } @@ -177,6 +201,37 @@ impl ToTokens for HtmlTag { }} }); + // These are the runtime-checks exclusive to dynamic tags. + // For literal tags this is already done at compile-time. + let dyn_tag_runtime_checks = if matches!(&tag_name, TagName::Expr(_)) { + // when Span::source_file Span::start get stabilised or yew-macro introduces a nightly feature flag + // we should expand the panic message to contain the exact location of the dynamic tag. + Some(quote! { + // check void element + if !#vtag.children.is_empty() { + match #vtag.tag() { + "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" + | "meta" | "param" | "source" | "track" | "wbr" => { + ::std::panic!("a dynamic tag tried to create a `<{0}>` tag with children. `<{0}>` is a void element which can't have any children.", #vtag.tag()); + } + _ => {} + } + } + + // handle special attribute value + match #vtag.tag() { + "input" | "textarea" => {} + _ => { + if let ::std::option::Option::Some(value) = #vtag.value.take() { + #vtag.attributes.insert("value".to_string(), value); + } + } + } + }) + } else { + None + }; + tokens.extend(quote! {{ let mut #vtag = ::yew::virtual_dom::VTag::new(#name); #(#set_kind)* @@ -190,11 +245,102 @@ impl ToTokens for HtmlTag { #vtag.add_attributes(vec![#(#attr_pairs),*]); #vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]); #vtag.add_children(vec![#(#children),*]); + #dyn_tag_runtime_checks ::yew::virtual_dom::VNode::from(#vtag) }}); } } +struct DynamicName { + at: Token![@], + expr: Option, +} + +impl Peek<'_, ()> for DynamicName { + fn peek(cursor: Cursor) -> Option<((), Cursor)> { + let (punct, cursor) = cursor.punct()?; + (punct.as_char() == '@').as_option()?; + + // move cursor past block if there is one + let cursor = cursor + .group(Delimiter::Brace) + .map(|(_, _, cursor)| cursor) + .unwrap_or(cursor); + + Some(((), cursor)) + } +} + +impl Parse for DynamicName { + fn parse(input: ParseStream) -> ParseResult { + let at = input.parse()?; + // the expression block is optional, closing tags don't have it. + let expr = if input.cursor().group(Delimiter::Brace).is_some() { + Some(input.parse()?) + } else { + None + }; + + Ok(Self { at, expr }) + } +} + +impl ToTokens for DynamicName { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { at, expr } = self; + tokens.extend(quote! {#at#expr}); + } +} + +#[derive(PartialEq)] +enum TagKey { + Lit(HtmlDashedName), + Expr, +} + +enum TagName { + Lit(HtmlDashedName), + Expr(DynamicName), +} + +impl TagName { + fn get_key(&self) -> TagKey { + match self { + TagName::Lit(name) => TagKey::Lit(name.clone()), + TagName::Expr(_) => TagKey::Expr, + } + } +} + +impl Peek<'_, TagKey> for TagName { + fn peek(cursor: Cursor) -> Option<(TagKey, Cursor)> { + if let Some((_, cursor)) = DynamicName::peek(cursor) { + Some((TagKey::Expr, cursor)) + } else { + HtmlDashedName::peek(cursor).map(|(name, cursor)| (TagKey::Lit(name), cursor)) + } + } +} + +impl Parse for TagName { + fn parse(input: ParseStream) -> ParseResult { + if DynamicName::peek(input.cursor()).is_some() { + DynamicName::parse(input).map(Self::Expr) + } else { + HtmlDashedName::parse(input).map(Self::Lit) + } + } +} + +impl ToTokens for TagName { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + TagName::Lit(name) => name.to_tokens(tokens), + TagName::Expr(name) => name.to_tokens(tokens), + } + } +} + struct HtmlTagOpen { lt: Token![<], tag_name: TagName, @@ -203,23 +349,24 @@ struct HtmlTagOpen { gt: Token![>], } -impl PeekValue for HtmlTagOpen { - fn peek(cursor: Cursor) -> Option { +impl PeekValue for HtmlTagOpen { + fn peek(cursor: Cursor) -> Option { let (punct, cursor) = cursor.punct()?; (punct.as_char() == '<').as_option()?; - let (name, cursor) = TagName::peek(cursor)?; - if name.to_string() == "key" { - let (punct, _) = cursor.punct()?; - if punct.as_char() == '=' { - None + let (tag_key, cursor) = TagName::peek(cursor)?; + if let TagKey::Lit(name) = &tag_key { + // Avoid parsing `` as an HtmlTag. It needs to be parsed as an HtmlList. + if name.to_string() == "key" { + let (punct, _) = cursor.punct()?; + // ... unless it isn't followed by a '='. `` is a valid HtmlTag! + (punct.as_char() != '=').as_option()?; } else { - Some(name) + non_capitalized_ascii(&name.to_string()).as_option()?; } - } else { - non_capitalized_ascii(&name.to_string()).as_option()?; - Some(name) } + + Some(tag_key) } } @@ -230,15 +377,28 @@ impl Parse for HtmlTagOpen { let TagSuffix { stream, div, gt } = input.parse()?; let mut attributes: TagAttributes = parse(stream)?; - // Don't treat value as special for non input / textarea fields - match tag_name.to_string().as_str() { - "input" | "textarea" => {} - _ => { - if let Some(value) = attributes.value.take() { - attributes.attributes.push(TagAttribute { - label: TagName::new(Ident::new("value", Span::call_site())), - value, - }); + match &tag_name { + TagName::Lit(name) => { + // Don't treat value as special for non input / textarea fields + // For dynamic tags this is done at runtime! + match name.to_ascii_lowercase_string().as_str() { + "input" | "textarea" => {} + _ => { + if let Some(value) = attributes.value.take() { + attributes.attributes.push(TagAttribute { + label: HtmlDashedName::new(Ident::new("value", Span::call_site())), + value, + }); + } + } + } + } + TagName::Expr(name) => { + if name.expr.is_none() { + return Err(syn::Error::new_spanned( + tag_name, + "this dynamic tag is missing an expression block defining its value", + )); } } } @@ -267,31 +427,47 @@ struct HtmlTagClose { gt: Token![>], } -impl PeekValue for HtmlTagClose { - fn peek(cursor: Cursor) -> Option { +impl PeekValue for HtmlTagClose { + fn peek(cursor: Cursor) -> Option { let (punct, cursor) = cursor.punct()?; (punct.as_char() == '<').as_option()?; let (punct, cursor) = cursor.punct()?; (punct.as_char() == '/').as_option()?; - let (name, cursor) = TagName::peek(cursor)?; - non_capitalized_ascii(&name.to_string()).as_option()?; + let (tag_key, cursor) = TagName::peek(cursor)?; + if let TagKey::Lit(name) = &tag_key { + non_capitalized_ascii(&name.to_string()).as_option()?; + } let (punct, _) = cursor.punct()?; (punct.as_char() == '>').as_option()?; - Some(name) + Some(tag_key) } } impl Parse for HtmlTagClose { fn parse(input: ParseStream) -> ParseResult { + let lt = input.parse()?; + let div = input.parse()?; + let tag_name = input.parse()?; + let gt = input.parse()?; + + if let TagName::Expr(name) = &tag_name { + if let Some(expr) = &name.expr { + return Err(syn::Error::new_spanned( + expr, + "dynamic closing tags must not have a body (hint: replace it with just ``)", + )); + } + } + Ok(HtmlTagClose { - lt: input.parse()?, - div: input.parse()?, - tag_name: input.parse()?, - gt: input.parse()?, + lt, + div, + tag_name, + gt, }) } } diff --git a/yew-macro/tests/macro/html-component-fail.stderr b/yew-macro/tests/macro/html-component-fail.stderr index 1a978df97cc..0e56447c5dc 100644 --- a/yew-macro/tests/macro/html-component-fail.stderr +++ b/yew-macro/tests/macro/html-component-fail.stderr @@ -134,11 +134,11 @@ error: expected identifier | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: unexpected end of input, expected expression - --> $DIR/html-component-fail.rs:98:5 +error: expected an expression following this equals sign + --> $DIR/html-component-fail.rs:98:26 | 98 | html! { }; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^ | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/yew-macro/tests/macro/html-list-fail.stderr b/yew-macro/tests/macro/html-list-fail.stderr index 971f77bdbfe..90a6b7d7342 100644 --- a/yew-macro/tests/macro/html-list-fail.stderr +++ b/yew-macro/tests/macro/html-list-fail.stderr @@ -1,4 +1,4 @@ -error: this opening tag has no corresponding closing tag +error: this opening fragment has no corresponding closing fragment --> $DIR/html-list-fail.rs:4:13 | 4 | html! { <> }; @@ -6,7 +6,7 @@ error: this opening tag has no corresponding closing tag | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: this closing tag has no corresponding opening tag +error: this closing fragment has no corresponding opening fragment --> $DIR/html-list-fail.rs:5:13 | 5 | html! { }; @@ -14,7 +14,7 @@ error: this closing tag has no corresponding opening tag | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: this opening tag has no corresponding closing tag +error: this opening fragment has no corresponding closing fragment --> $DIR/html-list-fail.rs:6:13 | 6 | html! { <><> }; @@ -22,7 +22,7 @@ error: this opening tag has no corresponding closing tag | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: this closing tag has no corresponding opening tag +error: this closing fragment has no corresponding opening fragment --> $DIR/html-list-fail.rs:7:13 | 7 | html! { }; @@ -30,7 +30,7 @@ error: this closing tag has no corresponding opening tag | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: this opening tag has no corresponding closing tag +error: this opening fragment has no corresponding closing fragment --> $DIR/html-list-fail.rs:8:13 | 8 | html! { <><> }; @@ -54,15 +54,15 @@ error: expected a valid html element | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: unexpected end of input, expected expression - --> $DIR/html-list-fail.rs:11:5 +error: expected an expression following this equals sign + --> $DIR/html-list-fail.rs:11:17 | 11 | html! { } - | ^^^^^^^^^^^^^^^^^^ + | ^^ | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: this opening tag has no corresponding closing tag +error: this opening fragment has no corresponding closing fragment --> $DIR/html-list-fail.rs:12:13 | 12 | html! { invalid} diff --git a/yew-macro/tests/macro/html-tag-fail.rs b/yew-macro/tests/macro/html-tag-fail.rs index ace113e6f21..7666106364a 100644 --- a/yew-macro/tests/macro/html-tag-fail.rs +++ b/yew-macro/tests/macro/html-tag-fail.rs @@ -38,6 +38,12 @@ fn compile_fail() { html! { }; html! { }; + html! { }; + + html! { <@> }; + html! { <@{"test"}> }; + html! { <@{55}> }; + html! { <@/> }; } fn main() {} diff --git a/yew-macro/tests/macro/html-tag-fail.stderr b/yew-macro/tests/macro/html-tag-fail.stderr index 75b4f447b5c..bcd196d4cad 100644 --- a/yew-macro/tests/macro/html-tag-fail.stderr +++ b/yew-macro/tests/macro/html-tag-fail.stderr @@ -150,6 +150,38 @@ error: the tag `` is a void element and cannot have children (hint: rewri | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) +error: the tag `` is a void element and cannot have children (hint: rewrite this as ``) + --> $DIR/html-tag-fail.rs:41:13 + | +41 | html! { }; + | ^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: this dynamic tag is missing an expression block defining its value + --> $DIR/html-tag-fail.rs:43:14 + | +43 | html! { <@> }; + | ^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: dynamic closing tags must not have a body (hint: replace it with just ``) + --> $DIR/html-tag-fail.rs:44:27 + | +44 | html! { <@{"test"}> }; + | ^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error: this dynamic tag is missing an expression block defining its value + --> $DIR/html-tag-fail.rs:46:14 + | +46 | html! { <@/> }; + | ^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0308]: mismatched types --> $DIR/html-tag-fail.rs:25:28 | @@ -253,3 +285,18 @@ error[E0308]: mismatched types | ^^ expected struct `yew::html::NodeRef`, found `()` | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `std::borrow::Cow<'static, str>: std::convert::From<{integer}>` is not satisfied + --> $DIR/html-tag-fail.rs:45:15 + | +45 | html! { <@{55}> }; + | ^^^^ the trait `std::convert::From<{integer}>` is not implemented for `std::borrow::Cow<'static, str>` + | + = help: the following implementations were found: + as std::convert::From<&'a [T]>> + as std::convert::From<&'a std::vec::Vec>> + as std::convert::From>> + as std::convert::From<&'a std::ffi::CStr>> + and 11 others + = note: required by `std::convert::From::from` + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/yew-macro/tests/macro/html-tag-pass.rs b/yew-macro/tests/macro/html-tag-pass.rs index 6ac49d8e03f..c36990a3ead 100644 --- a/yew-macro/tests/macro/html-tag-pass.rs +++ b/yew-macro/tests/macro/html-tag-pass.rs @@ -4,6 +4,10 @@ use yew::prelude::*; fn compile_pass() { let onclick = Callback::from(|_: MouseEvent| ()); let parent_ref = NodeRef::default(); + + let dyn_tag = || String::from("test"); + let mut extra_tags_iter = vec!["a", "b"].into_iter(); + html! {
@@ -41,6 +45,19 @@ fn compile_pass() { + <@{dyn_tag()}> + <@{extra_tags_iter.next().unwrap()} class="extra-a"/> + <@{extra_tags_iter.next().unwrap()} class="extra-b"/> + + + <@{ + let tag = dyn_tag(); + if tag == "test" { + "div" + } else { + "a" + } + }/>
}; } diff --git a/yew-macro/tests/macro_test.rs b/yew-macro/tests/macro_test.rs index 645f8c64290..0967570458b 100644 --- a/yew-macro/tests/macro_test.rs +++ b/yew-macro/tests/macro_test.rs @@ -1,3 +1,5 @@ +use yew::html; + #[allow(dead_code)] #[rustversion::attr(stable(1.43), test)] fn tests() { @@ -22,3 +24,23 @@ fn tests() { t.pass("tests/macro/html-tag-pass.rs"); t.compile_fail("tests/macro/html-tag-fail.rs"); } + +#[test] +#[should_panic( + expected = "a dynamic tag tried to create a `
` tag with children. `
` is a void element which can't have any children." +)] +fn dynamic_tags_catch_void_elements() { + html! { + <@{"br"}> + { "No children allowed" } + + }; +} + +#[test] +#[should_panic(expected = "a dynamic tag returned a tag name containing non ASCII characters: `❤`")] +fn dynamic_tags_catch_non_ascii() { + html! { + <@{"❤"}/> + }; +} diff --git a/yew/src/virtual_dom/vtag.rs b/yew/src/virtual_dom/vtag.rs index 0ed95b482e1..bdb47e4bf89 100644 --- a/yew/src/virtual_dom/vtag.rs +++ b/yew/src/virtual_dom/vtag.rs @@ -1195,4 +1195,59 @@ mod tests { expected ); } + + #[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_str("a"); + builder + }/> }; + + elem.apply(&scope, &parent, None, None); + let vtag = assert_vtag(&mut elem); + // make sure the new tag name is used internally + assert_eq!(vtag.tag, "a"); + + #[cfg(feature = "web_sys")] + // Element.tagName is always in the canonical upper-case form. + 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()); + assert_eq!( + div_vtag.attributes.get("value").map(String::as_str), + Some("Hello") + ); + + let mut input_el = html! { + <@{"input"} value="World"/> + }; + let input_vtag = assert_vtag(&mut input_el); + assert_eq!(input_vtag.value, Some("World".to_string())); + assert!(!input_vtag.attributes.contains_key("value")); + } + + #[test] + fn dynamic_tags_handle_weird_capitalization() { + let mut el = html! { + <@{"tExTAREa"}/> + }; + let vtag = assert_vtag(&mut el); + assert_eq!(vtag.tag(), "textarea"); + } }