Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make <textarea> a void element #3465

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/suspense/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn app_content() -> HtmlResult {

Ok(html! {
<div class="content-area">
<textarea value={value.to_string()} oninput={on_text_input}></textarea>
<textarea value={value.to_string()} oninput={on_text_input} />
<div class="action-area">
<button onclick={on_take_a_break}>{"Take a break!"}</button>
<div class="hint">{"You can take a break at anytime"}<br />{"and your work will be preserved."}</div>
Expand Down
2 changes: 1 addition & 1 deletion examples/suspense/src/struct_consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl Component for BaseAppContent {
let on_take_a_break = ctx.link().callback(|_| Msg::TakeABreak);
html! {
<div class="content-area">
<textarea value={self.value.clone()} {oninput}></textarea>
<textarea value={self.value.clone()} {oninput} />
<div class="action-area">
<button onclick={on_take_a_break}>{"Take a break!"}</button>
<div class="hint">{"You can take a break at anytime"}<br />{"and your work will be preserved."}</div>
Expand Down
57 changes: 44 additions & 13 deletions packages/yew-macro/src/html_tree/html_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ impl Parse for HtmlElement {
//
// For dynamic tags this is done at runtime!
match name.to_ascii_lowercase_string().as_str() {
"textarea" => {
return Err(syn::Error::new_spanned(
open.to_spanned(),
"the tag `<textarea>` is a void element and cannot have children (hint: \
to provide value to it, rewrite it as `<textarea value={x} />`. If you \
wish to set the default value, rewrite it as `<textarea defaultvalue={x} \
/>`)",
))
}

"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link"
| "meta" | "param" | "source" | "track" | "wbr" => {
return Err(syn::Error::new_spanned(
Expand All @@ -61,8 +71,9 @@ impl Parse for HtmlElement {
"the tag `<{name}>` is a void element and cannot have children (hint: \
rewrite this as `<{name} />`)",
),
));
))
}

_ => {}
}
}
Expand Down Expand Up @@ -117,23 +128,34 @@ impl ToTokens for HtmlElement {
checked,
listeners,
special,
defaultvalue,
} = &props;

// attributes with special treatment

let node_ref = special.wrap_node_ref_attr();
let key = special.wrap_key_attr();
let value = value
.as_ref()
.map(|prop| wrap_attr_value(prop.value.optimize_literals()))
.unwrap_or(quote! { ::std::option::Option::None });
let checked = checked
.as_ref()
.map(|attr| {
let value = &attr.value;
quote! { ::std::option::Option::Some( #value ) }
})
.unwrap_or(quote! { ::std::option::Option::None });
let value = || {
value
.as_ref()
.map(|prop| wrap_attr_value(prop.value.optimize_literals()))
.unwrap_or(quote! { ::std::option::Option::None })
};
let checked = || {
checked
.as_ref()
.map(|attr| {
let value = &attr.value;
quote! { ::std::option::Option::Some( #value ) }
})
.unwrap_or(quote! { ::std::option::Option::None })
};
let defaultvalue = || {
defaultvalue
.as_ref()
.map(|prop| wrap_attr_value(prop.value.optimize_literals()))
.unwrap_or(quote! { ::std::option::Option::None })
};

// other attributes

Expand Down Expand Up @@ -329,6 +351,8 @@ impl ToTokens for HtmlElement {
}
let node = match &*name {
"input" => {
let value = value();
futursolo marked this conversation as resolved.
Show resolved Hide resolved
let checked = checked();
quote! {
::std::convert::Into::<::yew::virtual_dom::VNode>::into(
::yew::virtual_dom::VTag::__new_input(
Expand All @@ -343,10 +367,13 @@ impl ToTokens for HtmlElement {
}
}
"textarea" => {
let value = value();
let defaultvalue = defaultvalue();
quote! {
::std::convert::Into::<::yew::virtual_dom::VNode>::into(
::yew::virtual_dom::VTag::__new_textarea(
#value,
#defaultvalue,
#node_ref,
#key,
#attributes,
Expand Down Expand Up @@ -408,6 +435,9 @@ impl ToTokens for HtmlElement {
#[cfg(not(nightly_yew))]
let invalid_void_tag_msg_start = "";

let value = value();
let checked = checked();
let defaultvalue = defaultvalue();
// 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()=> {
Expand Down Expand Up @@ -439,6 +469,7 @@ impl ToTokens for HtmlElement {
_ if "textarea".eq_ignore_ascii_case(::std::convert::AsRef::<::std::primitive::str>::as_ref(&#vtag_name)) => {
::yew::virtual_dom::VTag::__new_textarea(
#value,
#defaultvalue,
#node_ref,
#key,
#attributes,
Expand Down Expand Up @@ -473,7 +504,7 @@ impl ToTokens for HtmlElement {
::std::debug_assert!(
!::std::matches!(#vtag.tag().to_ascii_lowercase().as_str(),
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input"
| "link" | "meta" | "param" | "source" | "track" | "wbr"
| "link" | "meta" | "param" | "source" | "track" | "wbr" | "textarea"
),
concat!(#invalid_void_tag_msg_start, "a dynamic tag tried to create a `<{0}>` tag with children. `<{0}>` is a void element which can't have any children."),
#vtag.tag(),
Expand Down
3 changes: 3 additions & 0 deletions packages/yew-macro/src/props/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct ElementProps {
pub classes: Option<ClassesForm>,
pub booleans: Vec<Prop>,
pub value: Option<Prop>,
pub defaultvalue: Option<Prop>,
pub checked: Option<Prop>,
pub special: SpecialProps,
}
Expand All @@ -47,6 +48,7 @@ impl Parse for ElementProps {
.map(|prop| ClassesForm::from_expr(prop.value));
let value = props.pop("value");
let checked = props.pop("checked");
let defaultvalue = props.pop("defaultvalue");
let special = props.special;

Ok(Self {
Expand All @@ -57,6 +59,7 @@ impl Parse for ElementProps {
booleans: booleans.into_vec(),
value,
special,
defaultvalue,
})
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/yew-macro/tests/html_macro/element-fail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ fn compile_fail() {

// void element with children
html! { <input type="text"></input> };
// <textarea> should have a custom error message explaining how to set its default value
html! { <textarea>{"default value"}</textarea> }
// make sure that capitalization doesn't matter for the void children check
html! { <iNpUt type="text"></iNpUt> };

Expand Down
Loading
Loading