Skip to content

Commit

Permalink
Make <textarea> a void element (#3465)
Browse files Browse the repository at this point in the history
* made <textarea> a void element
* added defaultvalue special attr to <textarea>
* updated error message when trying to pass children to textarea

* updated docs, fixed formatting
* fixed hydration test
* fixed suspense test
* fixed heading in docs
* fixed clippy warnings
* fixed SSR, added SSR test for precedence of value over defaultvalue
* fixing wasm-bindgen-test screwups & replacing deprecated function use
  • Loading branch information
its-the-shrimp authored Feb 20, 2025
1 parent 35f8dde commit 0091679
Show file tree
Hide file tree
Showing 29 changed files with 973 additions and 544 deletions.
1,121 changes: 696 additions & 425 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ lto = true
codegen-units = 1
opt-level = 3

[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
"cfg(documenting)",
"cfg(verbose_tests)",
"cfg(yew_lints)",
"cfg(nightly_yew)",
"cfg(wasm_bindgen_unstable_test_coverage)"
]}
2 changes: 1 addition & 1 deletion examples/mount_point/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fn create_canvas(document: &Document) -> HtmlCanvasElement {
canvas.set_height(100);
let ctx =
CanvasRenderingContext2d::from(JsValue::from(canvas.get_context("2d").unwrap().unwrap()));
ctx.set_fill_style(&JsValue::from_str("green"));
ctx.set_fill_style_str("green");
ctx.fill_rect(10., 10., 50., 50.);

canvas
Expand Down
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
5 changes: 2 additions & 3 deletions packages/yew-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,5 @@ rustversion = "1"
trybuild = "1"
yew = { path = "../yew" }

[lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(nightly_yew)'] }

[lints]
workspace = true
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 @@ -92,6 +92,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 @@ -100,8 +110,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 @@ -156,23 +167,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 @@ -360,6 +382,8 @@ impl ToTokens for HtmlElement {
}
let node = match &*name {
"input" => {
let value = value();
let checked = checked();
quote! {
::std::convert::Into::<::yew::virtual_dom::VNode>::into(
::yew::virtual_dom::VTag::__new_input(
Expand All @@ -374,10 +398,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 @@ -439,6 +466,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 @@ -466,6 +496,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 @@ -500,7 +531,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 @@ -11,6 +11,7 @@ pub struct ElementProps {
pub classes: Option<Prop>,
pub booleans: Vec<Prop>,
pub value: Option<Prop>,
pub defaultvalue: Option<Prop>,
pub checked: Option<Prop>,
pub special: SpecialProps,
}
Expand All @@ -31,6 +32,7 @@ impl Parse for ElementProps {
let classes = props.pop("class");
let value = props.pop("value");
let checked = props.pop("checked");
let defaultvalue = props.pop("defaultvalue");
let special = props.special;

Ok(Self {
Expand All @@ -41,6 +43,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

0 comments on commit 0091679

Please sign in to comment.