Make <textarea> a void element (#3465)

* 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
This commit is contained in:
Tim Kurdov 2025-02-20 18:13:07 +00:00 committed by GitHub
parent 35f8dde77e
commit 0091679779
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 979 additions and 550 deletions

1133
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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)"
]}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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(
@ -100,8 +110,9 @@ impl Parse for HtmlElement {
"the tag `<{name}>` is a void element and cannot have children (hint: \
rewrite this as `<{name} />`)",
),
));
))
}
_ => {}
}
}
@ -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
@ -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(
@ -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,
@ -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()=> {
@ -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,
@ -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(),

View File

@ -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,
}
@ -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 {
@ -41,6 +43,7 @@ impl Parse for ElementProps {
booleans: booleans.into_vec(),
value,
special,
defaultvalue,
})
}
}

View File

@ -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> };

View File

@ -118,28 +118,34 @@ error: the tag `<input>` is a void element and cannot have children (hint: rewri
66 | html! { <input type="text"></input> };
| ^^^^^^^^^^^^^^^^^^^
error: the tag `<iNpUt>` is a void element and cannot have children (hint: rewrite this as `<iNpUt />`)
error: 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} />`)
--> tests/html_macro/element-fail.rs:68:13
|
68 | html! { <iNpUt type="text"></iNpUt> };
68 | html! { <textarea>{"default value"}</textarea> }
| ^^^^^^^^^^
error: the tag `<iNpUt>` is a void element and cannot have children (hint: rewrite this as `<iNpUt />`)
--> tests/html_macro/element-fail.rs:70:13
|
70 | html! { <iNpUt type="text"></iNpUt> };
| ^^^^^^^^^^^^^^^^^^^
error: this dynamic tag is missing an expression block defining its value
--> tests/html_macro/element-fail.rs:71:14
--> tests/html_macro/element-fail.rs:73:14
|
71 | html! { <@></@> };
73 | html! { <@></@> };
| ^
error: this dynamic tag is missing an expression block defining its value
--> tests/html_macro/element-fail.rs:72:14
--> tests/html_macro/element-fail.rs:74:14
|
72 | html! { <@/> };
74 | html! { <@/> };
| ^
error: dynamic closing tags must not have a body (hint: replace it with just `</@>`)
--> tests/html_macro/element-fail.rs:75:27
--> tests/html_macro/element-fail.rs:77:27
|
75 | html! { <@{"test"}></@{"test"}> };
77 | html! { <@{"test"}></@{"test"}> };
| ^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple {
@ -147,9 +153,9 @@ error: the property value must be either a literal or enclosed in braces. Consid
paren_token: Paren,
elems: [],
}
--> tests/html_macro/element-fail.rs:80:24
--> tests/html_macro/element-fail.rs:82:24
|
80 | html! { <input ref=() /> };
82 | html! { <input ref=() /> };
| ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple {
@ -157,9 +163,9 @@ error: the property value must be either a literal or enclosed in braces. Consid
paren_token: Paren,
elems: [],
}
--> tests/html_macro/element-fail.rs:81:24
--> tests/html_macro/element-fail.rs:83:24
|
81 | html! { <input ref=() ref=() /> };
83 | html! { <input ref=() ref=() /> };
| ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call {
@ -173,7 +179,7 @@ error: the property value must be either a literal or enclosed in braces. Consid
PathSegment {
ident: Ident {
ident: "Some",
span: #0 bytes(2482..2486),
span: #0 bytes(2628..2632),
},
arguments: PathArguments::None,
},
@ -190,9 +196,9 @@ error: the property value must be either a literal or enclosed in braces. Consid
},
],
}
--> tests/html_macro/element-fail.rs:82:28
--> tests/html_macro/element-fail.rs:84:28
|
82 | html! { <input onfocus=Some(5) /> };
84 | html! { <input onfocus=Some(5) /> };
| ^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Path {
@ -204,16 +210,16 @@ error: the property value must be either a literal or enclosed in braces. Consid
PathSegment {
ident: Ident {
ident: "NotToString",
span: #0 bytes(2522..2533),
span: #0 bytes(2668..2679),
},
arguments: PathArguments::None,
},
],
},
}
--> tests/html_macro/element-fail.rs:83:27
--> tests/html_macro/element-fail.rs:85:27
|
83 | html! { <input string=NotToString /> };
85 | html! { <input string=NotToString /> };
| ^^^^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call {
@ -227,7 +233,7 @@ error: the property value must be either a literal or enclosed in braces. Consid
PathSegment {
ident: Ident {
ident: "Some",
span: #0 bytes(2561..2565),
span: #0 bytes(2707..2711),
},
arguments: PathArguments::None,
},
@ -245,7 +251,7 @@ error: the property value must be either a literal or enclosed in braces. Consid
PathSegment {
ident: Ident {
ident: "NotToString",
span: #0 bytes(2566..2577),
span: #0 bytes(2712..2723),
},
arguments: PathArguments::None,
},
@ -254,9 +260,9 @@ error: the property value must be either a literal or enclosed in braces. Consid
},
],
}
--> tests/html_macro/element-fail.rs:84:22
--> tests/html_macro/element-fail.rs:86:22
|
84 | html! { <a media=Some(NotToString) /> };
86 | html! { <a media=Some(NotToString) /> };
| ^^^^^^^^^^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call {
@ -270,7 +276,7 @@ error: the property value must be either a literal or enclosed in braces. Consid
PathSegment {
ident: Ident {
ident: "Some",
span: #0 bytes(2605..2609),
span: #0 bytes(2751..2755),
},
arguments: PathArguments::None,
},
@ -287,9 +293,9 @@ error: the property value must be either a literal or enclosed in braces. Consid
},
],
}
--> tests/html_macro/element-fail.rs:85:21
--> tests/html_macro/element-fail.rs:87:21
|
85 | html! { <a href=Some(5) /> };
87 | html! { <a href=Some(5) /> };
| ^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple {
@ -297,9 +303,9 @@ error: the property value must be either a literal or enclosed in braces. Consid
paren_token: Paren,
elems: [],
}
--> tests/html_macro/element-fail.rs:86:25
--> tests/html_macro/element-fail.rs:88:25
|
86 | html! { <input type=() /> };
88 | html! { <input type=() /> };
| ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple {
@ -307,9 +313,9 @@ error: the property value must be either a literal or enclosed in braces. Consid
paren_token: Paren,
elems: [],
}
--> tests/html_macro/element-fail.rs:87:26
--> tests/html_macro/element-fail.rs:89:26
|
87 | html! { <input value=() /> };
89 | html! { <input value=() /> };
| ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Path {
@ -321,16 +327,16 @@ error: the property value must be either a literal or enclosed in braces. Consid
PathSegment {
ident: Ident {
ident: "NotToString",
span: #0 bytes(2712..2723),
span: #0 bytes(2858..2869),
},
arguments: PathArguments::None,
},
],
},
}
--> tests/html_macro/element-fail.rs:88:27
--> tests/html_macro/element-fail.rs:90:27
|
88 | html! { <input string=NotToString /> };
90 | html! { <input string=NotToString /> };
| ^^^^^^^^^^^
error[E0308]: mismatched types
@ -685,9 +691,9 @@ error[E0277]: the trait bound `(): IntoPropValue<yew::NodeRef>` is not satisfied
= help: for that trait implementation, expected `VNode`, found `yew::NodeRef`
error[E0277]: the trait bound `implicit_clone::unsync::string::IString: From<{integer}>` is not satisfied
--> tests/html_macro/element-fail.rs:77:16
--> tests/html_macro/element-fail.rs:79:16
|
77 | html! { <@{55}></@> };
79 | html! { <@{55}></@> };
| ^^ the trait `From<{integer}>` is not implemented for `implicit_clone::unsync::string::IString`
|
= help: the following other types implement trait `From<T>`:

View File

@ -42,3 +42,6 @@ version = "0.3"
features = [
"HtmlHeadElement",
]
[lints]
workspace = true

View File

@ -66,7 +66,12 @@ pub fn compose_path(pathname: &str, query: &str) -> Option<String> {
}
}
#[cfg(test)]
// TODO: remove the cfg after wasm-bindgen-test stops emitting the function unconditionally
#[cfg(all(
test,
target_arch = "wasm32",
any(target_os = "unknown", target_os = "none")
))]
mod tests {
use gloo::utils::document;
use wasm_bindgen_test::wasm_bindgen_test as test;

View File

@ -1,4 +1,5 @@
#![cfg(not(target_os = "wasi"))]
// TODO: remove the cfg after wasm-bindgen-test stops emitting the function unconditionally
#![cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use std::time::Duration;

View File

@ -1,4 +1,5 @@
#![cfg(not(target_os = "wasi"))]
// TODO: remove the cfg after wasm-bindgen-test stops emitting the function unconditionally
#![cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use std::time::Duration;

View File

@ -1,4 +1,5 @@
#![cfg(not(target_os = "wasi"))]
// TODO: remove the cfg after wasm-bindgen-test stops emitting the function unconditionally
#![cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use std::time::Duration;

View File

@ -1,4 +1,5 @@
#![cfg(not(target_os = "wasi"))]
// TODO: remove the cfg after wasm-bindgen-test stops emitting the function unconditionally
#![cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;

View File

@ -1,3 +1,6 @@
// TODO: remove the cfg after wasm-bindgen-test stops emitting the function unconditionally
#![cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
use yew_router::prelude::*;

View File

@ -1,4 +1,5 @@
#![cfg(not(target_os = "wasi"))]
// TODO: remove the cfg after wasm-bindgen-test stops emitting the function unconditionally
#![cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))]
use std::time::Duration;

View File

@ -106,6 +106,5 @@ test = []
all-features = true
rustdoc-args = ["--cfg", "documenting"]
[lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(nightly_yew)'] }
[lints]
workspace = true

View File

@ -69,7 +69,7 @@ impl NodeWriter<'_> {
}
/// Shift a bundle into place without patching it
fn shift(&self, bundle: &mut BNode) {
fn shift(&self, bundle: &BNode) {
bundle.shift(self.parent, self.slot.clone());
}
@ -364,7 +364,7 @@ impl BList {
// it should be
Ordering::Equal => barrier_idx += 1,
// shift the node unconditionally, don't start a run
Ordering::Less => writer.shift(&mut r_bundle),
Ordering::Less => writer.shift(&r_bundle),
// start a run
Ordering::Greater => {
current_run = Some(RunInformation {

View File

@ -74,14 +74,11 @@ impl Reconcilable for VRaw {
parent: &Element,
slot: DomSlot,
) -> (DomSlot, Self::Bundle) {
let namespace = if parent
.namespace_uri()
.map_or(false, |ns| ns == SVG_NAMESPACE)
{
let namespace = if parent.namespace_uri().is_some_and(|ns| ns == SVG_NAMESPACE) {
Some(SVG_NAMESPACE)
} else if parent
.namespace_uri()
.map_or(false, |ns| ns == MATHML_NAMESPACE)
.is_some_and(|ns| ns == MATHML_NAMESPACE)
{
Some(MATHML_NAMESPACE)
} else {

View File

@ -8,7 +8,7 @@ use yew::AttrValue;
use super::Apply;
use crate::dom_bundle::BSubtree;
use crate::virtual_dom::vtag::{InputFields, Value};
use crate::virtual_dom::vtag::{InputFields, TextareaFields, Value};
use crate::virtual_dom::{AttributeOrProperty, Attributes};
impl<T: AccessValue> Apply for Value<T> {
@ -88,6 +88,22 @@ impl Apply for InputFields {
}
}
impl Apply for TextareaFields {
type Bundle = Value<TextAreaElement>;
type Element = TextAreaElement;
fn apply(self, root: &BSubtree, el: &Self::Element) -> Self::Bundle {
if let Some(def) = self.defaultvalue {
_ = el.set_default_value(def.as_str());
}
self.value.apply(root, el)
}
fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self::Bundle) {
self.value.apply_diff(root, el, bundle)
}
}
impl Attributes {
#[cold]
fn apply_diff_index_maps(

View File

@ -16,7 +16,9 @@ use web_sys::{Element, HtmlTextAreaElement as TextAreaElement};
use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, MATHML_NAMESPACE, SVG_NAMESPACE};
use crate::virtual_dom::vtag::{
InputFields, TextareaFields, VTagInner, Value, MATHML_NAMESPACE, SVG_NAMESPACE,
};
use crate::virtual_dom::{AttrValue, Attributes, Key, VTag};
use crate::NodeRef;
@ -129,8 +131,8 @@ impl Reconcilable for VTag {
let f = f.apply(root, el.unchecked_ref());
BTagInner::Input(f)
}
VTagInner::Textarea { value } => {
let value = value.apply(root, el.unchecked_ref());
VTagInner::Textarea(f) => {
let value = f.apply(root, el.unchecked_ref());
BTagInner::Textarea { value }
}
VTagInner::Other { children, tag } => {
@ -201,7 +203,10 @@ impl Reconcilable for VTag {
(VTagInner::Input(new), BTagInner::Input(old)) => {
new.apply_diff(root, el.unchecked_ref(), old);
}
(VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => {
(
VTagInner::Textarea(TextareaFields { value: new, .. }),
BTagInner::Textarea { value: old },
) => {
new.apply_diff(root, el.unchecked_ref(), old);
}
(
@ -234,11 +239,7 @@ impl VTag {
fn create_element(&self, parent: &Element) -> Element {
let tag = self.tag();
if tag == "svg"
|| parent
.namespace_uri()
.map_or(false, |ns| ns == SVG_NAMESPACE)
{
if tag == "svg" || parent.namespace_uri().is_some_and(|ns| ns == SVG_NAMESPACE) {
let namespace = Some(SVG_NAMESPACE);
document()
.create_element_ns(namespace, tag)
@ -246,7 +247,7 @@ impl VTag {
} else if tag == "math"
|| parent
.namespace_uri()
.map_or(false, |ns| ns == MATHML_NAMESPACE)
.is_some_and(|ns| ns == MATHML_NAMESPACE)
{
let namespace = Some(MATHML_NAMESPACE);
document()
@ -362,7 +363,7 @@ mod feat_hydration {
el.tag_name().to_lowercase(),
);
// We simply registers listeners and updates all attributes.
// We simply register listeners and update all attributes.
let attributes = attributes.apply(root, &el);
let listeners = listeners.apply(root, &el);
@ -372,8 +373,8 @@ mod feat_hydration {
let f = f.apply(root, el.unchecked_ref());
BTagInner::Input(f)
}
VTagInner::Textarea { value } => {
let value = value.apply(root, el.unchecked_ref());
VTagInner::Textarea(f) => {
let value = f.apply(root, el.unchecked_ref());
BTagInner::Textarea { value }
}

View File

@ -199,7 +199,7 @@ impl PartialEq for Listeners {
})
}
}
(None, Pending(pending)) | (Pending(pending), None) => pending.len() == 0,
(None, Pending(pending)) | (Pending(pending), None) => pending.is_empty(),
}
}
}

View File

@ -48,7 +48,7 @@ impl<T> Value<T> {
/// Set a new value. The caller should take care that the value is valid for the element's
/// `value` property
fn set(&mut self, value: Option<AttrValue>) {
pub(crate) fn set(&mut self, value: Option<AttrValue>) {
self.0 = value;
}
}
@ -93,7 +93,7 @@ impl DerefMut for InputFields {
}
impl InputFields {
/// Crate new attributes for an [InputElement] element
/// Create new attributes for an [InputElement] element
fn new(value: Option<AttrValue>, checked: Option<bool>) -> Self {
Self {
value: Value::new(value),
@ -102,6 +102,17 @@ impl InputFields {
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct TextareaFields {
/// Contains the value of an
/// [TextAreaElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
pub(crate) value: Value<TextAreaElement>,
/// Contains the default value of
/// [TextAreaElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
#[allow(unused)] // unused only if both "csr" and "ssr" features are off
pub(crate) defaultvalue: Option<AttrValue>,
}
/// [VTag] fields that are specific to different [VTag] kinds.
/// Decreases the memory footprint of [VTag] by avoiding impossible field and value combinations.
#[derive(Debug, Clone)]
@ -113,11 +124,7 @@ pub(crate) enum VTagInner {
/// Fields specific to
/// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
/// [VTag]s
Textarea {
/// Contains a value of an
/// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
value: Value<TextAreaElement>,
},
Textarea(TextareaFields),
/// Fields for all other kinds of [VTag]s
Other {
/// A tag of the element.
@ -154,9 +161,7 @@ impl VTag {
Self::new_base(
match &*tag.to_ascii_lowercase() {
"input" => VTagInner::Input(Default::default()),
"textarea" => VTagInner::Textarea {
value: Default::default(),
},
"textarea" => VTagInner::Textarea(Default::default()),
_ => VTagInner::Other {
tag,
children: Default::default(),
@ -184,7 +189,7 @@ impl VTag {
checked: Option<bool>,
node_ref: NodeRef,
key: Option<Key>,
// at bottom for more readable macro-expanded coded
// at the bottom for more readable macro-expanded code
attributes: Attributes,
listeners: Listeners,
) -> Self {
@ -214,16 +219,18 @@ impl VTag {
#[allow(clippy::too_many_arguments)]
pub fn __new_textarea(
value: Option<AttrValue>,
defaultvalue: Option<AttrValue>,
node_ref: NodeRef,
key: Option<Key>,
// at bottom for more readable macro-expanded coded
// at the bottom for more readable macro-expanded code
attributes: Attributes,
listeners: Listeners,
) -> Self {
VTag::new_base(
VTagInner::Textarea {
VTagInner::Textarea(TextareaFields {
value: Value::new(value),
},
defaultvalue,
}),
node_ref,
key,
attributes,
@ -243,7 +250,7 @@ impl VTag {
tag: AttrValue,
node_ref: NodeRef,
key: Option<Key>,
// at bottom for more readable macro-expanded coded
// at the bottom for more readable macro-expanded code
attributes: Attributes,
listeners: Listeners,
children: VNode,
@ -332,7 +339,7 @@ impl VTag {
pub fn value(&self) -> Option<&AttrValue> {
match &self.inner {
VTagInner::Input(f) => f.as_ref(),
VTagInner::Textarea { value } => value.as_ref(),
VTagInner::Textarea(TextareaFields { value, .. }) => value.as_ref(),
VTagInner::Other { .. } => None,
}
}
@ -345,7 +352,7 @@ impl VTag {
VTagInner::Input(f) => {
f.set(value.into_prop_value());
}
VTagInner::Textarea { value: dst } => {
VTagInner::Textarea(TextareaFields { value: dst, .. }) => {
dst.set(value.into_prop_value());
}
VTagInner::Other { .. } => (),
@ -447,7 +454,7 @@ impl PartialEq for VTag {
(match (&self.inner, &other.inner) {
(Input(l), Input(r)) => l == r,
(Textarea { value: value_l }, Textarea { value: value_r }) => value_l == value_r,
(Textarea (TextareaFields{ value: value_l, .. }), Textarea (TextareaFields{ value: value_r, .. })) => value_l == value_r,
(Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r,
_ => false,
}) && self.listeners.eq(&other.listeners)
@ -471,9 +478,9 @@ mod feat_ssr {
use crate::virtual_dom::VText;
// Elements that cannot have any child elements.
static VOID_ELEMENTS: &[&str; 14] = &[
static VOID_ELEMENTS: &[&str; 15] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
"source", "track", "wbr",
"source", "track", "wbr", "textarea",
];
impl VTag {
@ -497,14 +504,14 @@ mod feat_ssr {
}
};
if let VTagInner::Input(_) = self.inner {
if let Some(m) = self.value() {
write_attr(w, "value", Some(m));
if let VTagInner::Input(InputFields { value, checked }) = &self.inner {
if let Some(value) = value.as_deref() {
write_attr(w, "value", Some(value));
}
// Setting is as an attribute sets the `defaultChecked` property. Only emit this
// if it's explicitly set to checked.
if self.checked() == Some(true) {
if *checked == Some(true) {
write_attr(w, "checked", None);
}
}
@ -515,22 +522,21 @@ mod feat_ssr {
let _ = w.write_str(">");
match self.inner {
match &self.inner {
VTagInner::Input(_) => {}
VTagInner::Textarea { .. } => {
if let Some(m) = self.value() {
VText::new(m.to_owned())
VTagInner::Textarea(TextareaFields {
value,
defaultvalue,
}) => {
if let Some(def) = value.as_ref().or(defaultvalue.as_ref()) {
VText::new(def.clone())
.render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
.await;
}
let _ = w.write_str("</textarea>");
}
VTagInner::Other {
ref tag,
ref children,
..
} => {
VTagInner::Other { tag, children } => {
if !VOID_ELEMENTS.contains(&tag.as_ref()) {
children
.render_into_stream(w, parent_scope, hydratable, tag.into())
@ -644,6 +650,38 @@ mod ssr_tests {
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
}
#[cfg_attr(not(target_os = "wasi"), test)]
#[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
async fn test_textarea_w_defaultvalue() {
#[function_component]
fn Comp() -> Html {
html! { <textarea defaultvalue="teststring" /> }
}
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
}
#[cfg_attr(not(target_os = "wasi"), test)]
#[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
async fn test_value_precedence_over_defaultvalue() {
#[function_component]
fn Comp() -> Html {
html! { <textarea defaultvalue="defaultvalue" value="value" /> }
}
let s = ServerRenderer::<Comp>::new()
.hydratable(false)
.render()
.await;
assert_eq!(s, r#"<textarea>value</textarea>"#);
}
#[cfg_attr(not(target_os = "wasi"), test)]
#[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
async fn test_escaping_in_style_tag() {

View File

@ -364,7 +364,7 @@ async fn hydration_with_suspense_not_suspended_at_start() {
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 class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>

View File

@ -227,7 +227,7 @@ async fn suspense_not_suspended_at_start() {
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 class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
</div>

View File

@ -16,3 +16,6 @@ yew = { version = "0.21.0", features = ["csr"], path = "../../packages/yew" }
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['-O4']
[lints]
workspace = true

View File

@ -140,6 +140,45 @@ html! {
If the attribute is set to `None`, the attribute will not be set in the DOM.
## Children
Most HTML elements accept arbitrary HTML as children, however, there is a set of them that doesn't accept any children at all.
These elements are called _void_ elements, and they are:
- `<area />`
- `<base />`
- `<base />`
- `<br />`
- `<col />`
- `<embed />`
- `<hr />`
- `<img />`
- `<input />`
- `<link />`
- `<meta />`
- `<param />`
- `<source />`
- `<track />`
- `<wbr />`
- `<textarea />`
Attempting to provide children to these elements will result in a compilation error or, if the element tag is chosen dynamically, in a panic.
### The case of `<textarea>`
The `<textarea>` element is special; The modern HTML specification states that children of `<textarea>` define its default value, however in Yew it's specified differently.
Instead of writing
```html
<textarea>{"default value"}</textarea>
```
Which would fail to compile, it's customary to write
```html
<textarea defaultvalue="default value" />
```
## Relevant examples
- [Inner HTML](https://github.com/yewstack/yew/tree/master/examples/inner_html)