mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
35f8dde77e
commit
0091679779
1133
Cargo.lock
generated
1133
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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)"
|
||||
]}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> };
|
||||
|
||||
|
||||
@ -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>`:
|
||||
|
||||
@ -42,3 +42,6 @@ version = "0.3"
|
||||
features = [
|
||||
"HtmlHeadElement",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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::*;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user