mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Bind to properties instead of attributes by default (#2819)
* Set to properties, not attributes * fix tests * Add tests * enable disabled test, fmt * Introduce @key syntax to forcefully set as attribute * Everything compiles * More tests * id as property * This was not meant to be committed * Make test pass, fmt + clippy * fucking rustfmt * is this enough formatting * that was not supposed to be commited * apply review * fmt * fix CI * will you be happy now, clippy?
This commit is contained in:
parent
a4e70914ac
commit
4c35f95350
@ -14,13 +14,13 @@ fn App() -> Html {
|
|||||||
Callback::from(move |_| state.set(*state - 1))
|
Callback::from(move |_| state.set(*state - 1))
|
||||||
};
|
};
|
||||||
|
|
||||||
html!(
|
html! {
|
||||||
<>
|
<>
|
||||||
<p> {"current count: "} {*state} </p>
|
<p> {"current count: "} {*state} </p>
|
||||||
<button onclick={incr_counter}> {"+"} </button>
|
<button onclick={incr_counter}> {"+"} </button>
|
||||||
<button onclick={decr_counter}> {"-"} </button>
|
<button onclick={decr_counter}> {"-"} </button>
|
||||||
</>
|
</>
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use syn::spanned::Spanned;
|
|||||||
use syn::{Block, Expr, Ident, Lit, LitStr, Token};
|
use syn::{Block, Expr, Ident, Lit, LitStr, Token};
|
||||||
|
|
||||||
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
|
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
|
||||||
use crate::props::{ClassesForm, ElementProps, Prop};
|
use crate::props::{ClassesForm, ElementProps, Prop, PropDirective};
|
||||||
use crate::stringify::{Stringify, Value};
|
use crate::stringify::{Stringify, Value};
|
||||||
use crate::{non_capitalized_ascii, Peek, PeekValue};
|
use crate::{non_capitalized_ascii, Peek, PeekValue};
|
||||||
|
|
||||||
@ -135,39 +135,58 @@ impl ToTokens for HtmlElement {
|
|||||||
// other attributes
|
// other attributes
|
||||||
|
|
||||||
let attributes = {
|
let attributes = {
|
||||||
let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| {
|
let normal_attrs = attributes.iter().map(
|
||||||
(label.to_lit_str(), value.optimize_literals_tagged())
|
|Prop {
|
||||||
});
|
label,
|
||||||
let boolean_attrs = booleans.iter().filter_map(|Prop { label, value, .. }| {
|
value,
|
||||||
let key = label.to_lit_str();
|
directive,
|
||||||
Some((
|
..
|
||||||
key.clone(),
|
}| {
|
||||||
match value {
|
(
|
||||||
Expr::Lit(e) => match &e.lit {
|
label.to_lit_str(),
|
||||||
Lit::Bool(b) => Value::Static(if b.value {
|
value.optimize_literals_tagged(),
|
||||||
quote! { #key }
|
*directive,
|
||||||
} else {
|
)
|
||||||
return None;
|
},
|
||||||
}),
|
);
|
||||||
_ => Value::Dynamic(quote_spanned! {value.span()=> {
|
let boolean_attrs = booleans.iter().filter_map(
|
||||||
::yew::utils::__ensure_type::<::std::primitive::bool>(#value);
|
|Prop {
|
||||||
#key
|
label,
|
||||||
}}),
|
value,
|
||||||
},
|
directive,
|
||||||
expr => Value::Dynamic(
|
..
|
||||||
quote_spanned! {expr.span().resolved_at(Span::call_site())=>
|
}| {
|
||||||
if #expr {
|
let key = label.to_lit_str();
|
||||||
::std::option::Option::Some(
|
Some((
|
||||||
::yew::virtual_dom::AttrValue::Static(#key)
|
key.clone(),
|
||||||
)
|
match value {
|
||||||
|
Expr::Lit(e) => match &e.lit {
|
||||||
|
Lit::Bool(b) => Value::Static(if b.value {
|
||||||
|
quote! { #key }
|
||||||
} else {
|
} else {
|
||||||
::std::option::Option::None
|
return None;
|
||||||
}
|
}),
|
||||||
|
_ => Value::Dynamic(quote_spanned! {value.span()=> {
|
||||||
|
::yew::utils::__ensure_type::<::std::primitive::bool>(#value);
|
||||||
|
#key
|
||||||
|
}}),
|
||||||
},
|
},
|
||||||
),
|
expr => Value::Dynamic(
|
||||||
},
|
quote_spanned! {expr.span().resolved_at(Span::call_site())=>
|
||||||
))
|
if #expr {
|
||||||
});
|
::std::option::Option::Some(
|
||||||
|
::yew::virtual_dom::AttrValue::Static(#key)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
::std::option::Option::None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
*directive,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
);
|
||||||
let class_attr = classes.as_ref().and_then(|classes| match classes {
|
let class_attr = classes.as_ref().and_then(|classes| match classes {
|
||||||
ClassesForm::Tuple(classes) => {
|
ClassesForm::Tuple(classes) => {
|
||||||
let span = classes.span();
|
let span = classes.span();
|
||||||
@ -196,6 +215,7 @@ impl ToTokens for HtmlElement {
|
|||||||
__yew_classes
|
__yew_classes
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
ClassesForm::Single(classes) => {
|
ClassesForm::Single(classes) => {
|
||||||
@ -207,6 +227,7 @@ impl ToTokens for HtmlElement {
|
|||||||
Some((
|
Some((
|
||||||
LitStr::new("class", lit.span()),
|
LitStr::new("class", lit.span()),
|
||||||
Value::Static(quote! { #lit }),
|
Value::Static(quote! { #lit }),
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,21 +237,34 @@ impl ToTokens for HtmlElement {
|
|||||||
Value::Dynamic(quote! {
|
Value::Dynamic(quote! {
|
||||||
::std::convert::Into::<::yew::html::Classes>::into(#classes)
|
::std::convert::Into::<::yew::html::Classes>::into(#classes)
|
||||||
}),
|
}),
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fn apply_as(directive: Option<&PropDirective>) -> TokenStream {
|
||||||
|
match directive {
|
||||||
|
Some(PropDirective::ApplyAsProperty(token)) => {
|
||||||
|
quote_spanned!(token.span()=> ::yew::virtual_dom::ApplyAttributeAs::Property)
|
||||||
|
}
|
||||||
|
None => quote!(::yew::virtual_dom::ApplyAttributeAs::Attribute),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
|
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
|
||||||
fn try_into_static(src: &[(LitStr, Value)]) -> Option<TokenStream> {
|
fn try_into_static(
|
||||||
|
src: &[(LitStr, Value, Option<PropDirective>)],
|
||||||
|
) -> Option<TokenStream> {
|
||||||
let mut kv = Vec::with_capacity(src.len());
|
let mut kv = Vec::with_capacity(src.len());
|
||||||
for (k, v) in src.iter() {
|
for (k, v, directive) in src.iter() {
|
||||||
let v = match v {
|
let v = match v {
|
||||||
Value::Static(v) => quote! { #v },
|
Value::Static(v) => quote! { #v },
|
||||||
Value::Dynamic(_) => return None,
|
Value::Dynamic(_) => return None,
|
||||||
};
|
};
|
||||||
kv.push(quote! { [ #k, #v ] });
|
let apply_as = apply_as(directive.as_ref());
|
||||||
|
kv.push(quote! { ( #k, #v, #apply_as ) });
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
|
Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
|
||||||
@ -239,10 +273,14 @@ impl ToTokens for HtmlElement {
|
|||||||
let attrs = normal_attrs
|
let attrs = normal_attrs
|
||||||
.chain(boolean_attrs)
|
.chain(boolean_attrs)
|
||||||
.chain(class_attr)
|
.chain(class_attr)
|
||||||
.collect::<Vec<(LitStr, Value)>>();
|
.collect::<Vec<(LitStr, Value, Option<PropDirective>)>>();
|
||||||
try_into_static(&attrs).unwrap_or_else(|| {
|
try_into_static(&attrs).unwrap_or_else(|| {
|
||||||
let keys = attrs.iter().map(|(k, _)| quote! { #k });
|
let keys = attrs.iter().map(|(k, ..)| quote! { #k });
|
||||||
let values = attrs.iter().map(|(_, v)| wrap_attr_value(v));
|
let values = attrs.iter().map(|(_, v, directive)| {
|
||||||
|
let apply_as = apply_as(directive.as_ref());
|
||||||
|
let value = wrap_attr_value(v);
|
||||||
|
quote! { ::std::option::Option::map(#value, |it| (it, #apply_as)) }
|
||||||
|
});
|
||||||
quote! {
|
quote! {
|
||||||
::yew::virtual_dom::Attributes::Dynamic{
|
::yew::virtual_dom::Attributes::Dynamic{
|
||||||
keys: &[#(#keys),*],
|
keys: &[#(#keys),*],
|
||||||
|
|||||||
@ -13,17 +13,27 @@ use super::CHILDREN_LABEL;
|
|||||||
use crate::html_tree::HtmlDashedName;
|
use crate::html_tree::HtmlDashedName;
|
||||||
use crate::stringify::Stringify;
|
use crate::stringify::Stringify;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum PropDirective {
|
||||||
|
ApplyAsProperty(Token![~]),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Prop {
|
pub struct Prop {
|
||||||
|
pub directive: Option<PropDirective>,
|
||||||
pub label: HtmlDashedName,
|
pub label: HtmlDashedName,
|
||||||
/// Punctuation between `label` and `value`.
|
/// Punctuation between `label` and `value`.
|
||||||
pub value: Expr,
|
pub value: Expr,
|
||||||
}
|
}
|
||||||
impl Parse for Prop {
|
impl Parse for Prop {
|
||||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let directive = input
|
||||||
|
.parse::<Token![~]>()
|
||||||
|
.map(PropDirective::ApplyAsProperty)
|
||||||
|
.ok();
|
||||||
if input.peek(Brace) {
|
if input.peek(Brace) {
|
||||||
Self::parse_shorthand_prop_assignment(input)
|
Self::parse_shorthand_prop_assignment(input, directive)
|
||||||
} else {
|
} else {
|
||||||
Self::parse_prop_assignment(input)
|
Self::parse_prop_assignment(input, directive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +43,10 @@ impl Prop {
|
|||||||
/// Parse a prop using the shorthand syntax `{value}`, short for `value={value}`
|
/// Parse a prop using the shorthand syntax `{value}`, short for `value={value}`
|
||||||
/// This only allows for labels with no hyphens, as it would otherwise create
|
/// This only allows for labels with no hyphens, as it would otherwise create
|
||||||
/// an ambiguity in the syntax
|
/// an ambiguity in the syntax
|
||||||
fn parse_shorthand_prop_assignment(input: ParseStream) -> syn::Result<Self> {
|
fn parse_shorthand_prop_assignment(
|
||||||
|
input: ParseStream,
|
||||||
|
directive: Option<PropDirective>,
|
||||||
|
) -> syn::Result<Self> {
|
||||||
let value;
|
let value;
|
||||||
let _brace = braced!(value in input);
|
let _brace = braced!(value in input);
|
||||||
let expr = value.parse::<Expr>()?;
|
let expr = value.parse::<Expr>()?;
|
||||||
@ -44,7 +57,7 @@ impl Prop {
|
|||||||
}) = expr
|
}) = expr
|
||||||
{
|
{
|
||||||
if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) {
|
if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) {
|
||||||
syn::Result::Ok(HtmlDashedName::from(ident.clone()))
|
Ok(HtmlDashedName::from(ident.clone()))
|
||||||
} else {
|
} else {
|
||||||
Err(syn::Error::new_spanned(
|
Err(syn::Error::new_spanned(
|
||||||
path,
|
path,
|
||||||
@ -59,11 +72,18 @@ impl Prop {
|
|||||||
));
|
));
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
Ok(Self { label, value: expr })
|
Ok(Self {
|
||||||
|
label,
|
||||||
|
value: expr,
|
||||||
|
directive,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a prop of the form `label={value}`
|
/// Parse a prop of the form `label={value}`
|
||||||
fn parse_prop_assignment(input: ParseStream) -> syn::Result<Self> {
|
fn parse_prop_assignment(
|
||||||
|
input: ParseStream,
|
||||||
|
directive: Option<PropDirective>,
|
||||||
|
) -> syn::Result<Self> {
|
||||||
let label = input.parse::<HtmlDashedName>()?;
|
let label = input.parse::<HtmlDashedName>()?;
|
||||||
let equals = input.parse::<Token![=]>().map_err(|_| {
|
let equals = input.parse::<Token![=]>().map_err(|_| {
|
||||||
syn::Error::new_spanned(
|
syn::Error::new_spanned(
|
||||||
@ -83,7 +103,11 @@ impl Prop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let value = parse_prop_value(input)?;
|
let value = parse_prop_value(input)?;
|
||||||
Ok(Self { label, value })
|
Ok(Self {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
directive,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,10 +129,13 @@ fn parse_prop_value(input: &ParseBuffer) -> syn::Result<Expr> {
|
|||||||
|
|
||||||
match &expr {
|
match &expr {
|
||||||
Expr::Lit(_) => Ok(expr),
|
Expr::Lit(_) => Ok(expr),
|
||||||
_ => Err(syn::Error::new_spanned(
|
ref exp => Err(syn::Error::new_spanned(
|
||||||
&expr,
|
&expr,
|
||||||
"the property value must be either a literal or enclosed in braces. Consider \
|
format!(
|
||||||
adding braces around your expression.",
|
"the property value must be either a literal or enclosed in braces. Consider \
|
||||||
|
adding braces around your expression.: {:#?}",
|
||||||
|
exp
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,11 @@ impl Parse for PropValue {
|
|||||||
impl From<PropValue> for Prop {
|
impl From<PropValue> for Prop {
|
||||||
fn from(prop_value: PropValue) -> Prop {
|
fn from(prop_value: PropValue) -> Prop {
|
||||||
let PropValue { label, value } = prop_value;
|
let PropValue { label, value } = prop_value;
|
||||||
Prop { label, value }
|
Prop {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
directive: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -249,7 +249,13 @@ help: escape `type` to use it as an identifier
|
|||||||
85 | html! { <Child r#type=0 /> };
|
85 | html! { <Child r#type=0 /> };
|
||||||
| ++
|
| ++
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
|
||||||
|
ExprTuple {
|
||||||
|
attrs: [],
|
||||||
|
paren_token: Paren,
|
||||||
|
elems: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/component-fail.rs:86:24
|
--> tests/html_macro/component-fail.rs:86:24
|
||||||
|
|
|
|
||||||
86 | html! { <Child ref=() /> };
|
86 | html! { <Child ref=() /> };
|
||||||
@ -309,7 +315,24 @@ error: only one root html element is allowed (hint: you can wrap multiple html e
|
|||||||
102 | html! { <Child></Child><Child></Child> };
|
102 | html! { <Child></Child><Child></Child> };
|
||||||
| ^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Path(
|
||||||
|
ExprPath {
|
||||||
|
attrs: [],
|
||||||
|
qself: None,
|
||||||
|
path: Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment {
|
||||||
|
ident: Ident {
|
||||||
|
ident: "num",
|
||||||
|
span: #0 bytes(3894..3897),
|
||||||
|
},
|
||||||
|
arguments: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/component-fail.rs:106:24
|
--> tests/html_macro/component-fail.rs:106:24
|
||||||
|
|
|
|
||||||
106 | html! { <Child int=num ..props /> };
|
106 | html! { <Child int=num ..props /> };
|
||||||
|
|||||||
@ -142,61 +142,260 @@ error: dynamic closing tags must not have a body (hint: replace it with just `</
|
|||||||
75 | html! { <@{"test"}></@{"test"}> };
|
75 | html! { <@{"test"}></@{"test"}> };
|
||||||
| ^^^^^^^^
|
| ^^^^^^^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
|
||||||
|
ExprTuple {
|
||||||
|
attrs: [],
|
||||||
|
paren_token: Paren,
|
||||||
|
elems: [
|
||||||
|
Lit(
|
||||||
|
ExprLit {
|
||||||
|
attrs: [],
|
||||||
|
lit: Str(
|
||||||
|
LitStr {
|
||||||
|
token: "deprecated",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Comma,
|
||||||
|
Lit(
|
||||||
|
ExprLit {
|
||||||
|
attrs: [],
|
||||||
|
lit: Str(
|
||||||
|
LitStr {
|
||||||
|
token: "warning",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:83:24
|
--> tests/html_macro/element-fail.rs:83:24
|
||||||
|
|
|
|
||||||
83 | html! { <div class=("deprecated", "warning") /> };
|
83 | html! { <div class=("deprecated", "warning") /> };
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
|
||||||
|
ExprTuple {
|
||||||
|
attrs: [],
|
||||||
|
paren_token: Paren,
|
||||||
|
elems: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:84:24
|
--> tests/html_macro/element-fail.rs:84:24
|
||||||
|
|
|
|
||||||
84 | html! { <input ref=() /> };
|
84 | html! { <input ref=() /> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
|
||||||
|
ExprTuple {
|
||||||
|
attrs: [],
|
||||||
|
paren_token: Paren,
|
||||||
|
elems: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:85:24
|
--> tests/html_macro/element-fail.rs:85:24
|
||||||
|
|
|
|
||||||
85 | html! { <input ref=() ref=() /> };
|
85 | html! { <input ref=() ref=() /> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Call(
|
||||||
|
ExprCall {
|
||||||
|
attrs: [],
|
||||||
|
func: Path(
|
||||||
|
ExprPath {
|
||||||
|
attrs: [],
|
||||||
|
qself: None,
|
||||||
|
path: Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment {
|
||||||
|
ident: Ident {
|
||||||
|
ident: "Some",
|
||||||
|
span: #0 bytes(2632..2636),
|
||||||
|
},
|
||||||
|
arguments: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
paren_token: Paren,
|
||||||
|
args: [
|
||||||
|
Lit(
|
||||||
|
ExprLit {
|
||||||
|
attrs: [],
|
||||||
|
lit: Int(
|
||||||
|
LitInt {
|
||||||
|
token: 5,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:86:28
|
--> tests/html_macro/element-fail.rs:86:28
|
||||||
|
|
|
|
||||||
86 | html! { <input onfocus=Some(5) /> };
|
86 | html! { <input onfocus=Some(5) /> };
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Path(
|
||||||
|
ExprPath {
|
||||||
|
attrs: [],
|
||||||
|
qself: None,
|
||||||
|
path: Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment {
|
||||||
|
ident: Ident {
|
||||||
|
ident: "NotToString",
|
||||||
|
span: #0 bytes(2672..2683),
|
||||||
|
},
|
||||||
|
arguments: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:87:27
|
--> tests/html_macro/element-fail.rs:87:27
|
||||||
|
|
|
|
||||||
87 | html! { <input string=NotToString /> };
|
87 | html! { <input string=NotToString /> };
|
||||||
| ^^^^^^^^^^^
|
| ^^^^^^^^^^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Call(
|
||||||
|
ExprCall {
|
||||||
|
attrs: [],
|
||||||
|
func: Path(
|
||||||
|
ExprPath {
|
||||||
|
attrs: [],
|
||||||
|
qself: None,
|
||||||
|
path: Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment {
|
||||||
|
ident: Ident {
|
||||||
|
ident: "Some",
|
||||||
|
span: #0 bytes(2711..2715),
|
||||||
|
},
|
||||||
|
arguments: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
paren_token: Paren,
|
||||||
|
args: [
|
||||||
|
Path(
|
||||||
|
ExprPath {
|
||||||
|
attrs: [],
|
||||||
|
qself: None,
|
||||||
|
path: Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment {
|
||||||
|
ident: Ident {
|
||||||
|
ident: "NotToString",
|
||||||
|
span: #0 bytes(2716..2727),
|
||||||
|
},
|
||||||
|
arguments: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:88:22
|
--> tests/html_macro/element-fail.rs:88:22
|
||||||
|
|
|
|
||||||
88 | html! { <a media=Some(NotToString) /> };
|
88 | html! { <a media=Some(NotToString) /> };
|
||||||
| ^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Call(
|
||||||
|
ExprCall {
|
||||||
|
attrs: [],
|
||||||
|
func: Path(
|
||||||
|
ExprPath {
|
||||||
|
attrs: [],
|
||||||
|
qself: None,
|
||||||
|
path: Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment {
|
||||||
|
ident: Ident {
|
||||||
|
ident: "Some",
|
||||||
|
span: #0 bytes(2755..2759),
|
||||||
|
},
|
||||||
|
arguments: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
paren_token: Paren,
|
||||||
|
args: [
|
||||||
|
Lit(
|
||||||
|
ExprLit {
|
||||||
|
attrs: [],
|
||||||
|
lit: Int(
|
||||||
|
LitInt {
|
||||||
|
token: 5,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:89:21
|
--> tests/html_macro/element-fail.rs:89:21
|
||||||
|
|
|
|
||||||
89 | html! { <a href=Some(5) /> };
|
89 | html! { <a href=Some(5) /> };
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
|
||||||
|
ExprTuple {
|
||||||
|
attrs: [],
|
||||||
|
paren_token: Paren,
|
||||||
|
elems: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:90:25
|
--> tests/html_macro/element-fail.rs:90:25
|
||||||
|
|
|
|
||||||
90 | html! { <input type=() /> };
|
90 | html! { <input type=() /> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
|
||||||
|
ExprTuple {
|
||||||
|
attrs: [],
|
||||||
|
paren_token: Paren,
|
||||||
|
elems: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:91:26
|
--> tests/html_macro/element-fail.rs:91:26
|
||||||
|
|
|
|
||||||
91 | html! { <input value=() /> };
|
91 | html! { <input value=() /> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Path(
|
||||||
|
ExprPath {
|
||||||
|
attrs: [],
|
||||||
|
qself: None,
|
||||||
|
path: Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment {
|
||||||
|
ident: Ident {
|
||||||
|
ident: "NotToString",
|
||||||
|
span: #0 bytes(2862..2873),
|
||||||
|
},
|
||||||
|
arguments: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
--> tests/html_macro/element-fail.rs:92:27
|
--> tests/html_macro/element-fail.rs:92:27
|
||||||
|
|
|
|
||||||
92 | html! { <input string=NotToString /> };
|
92 | html! { <input string=NotToString /> };
|
||||||
|
|||||||
@ -1,65 +1,87 @@
|
|||||||
error: this opening fragment has no corresponding closing fragment
|
error: this opening fragment has no corresponding closing fragment
|
||||||
--> $DIR/list-fail.rs:5:13
|
--> tests/html_macro/list-fail.rs:5:13
|
||||||
|
|
|
|
||||||
5 | html! { <> };
|
5 | html! { <> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: this opening fragment has no corresponding closing fragment
|
error: this opening fragment has no corresponding closing fragment
|
||||||
--> $DIR/list-fail.rs:6:15
|
--> tests/html_macro/list-fail.rs:6:15
|
||||||
|
|
|
|
||||||
6 | html! { <><> };
|
6 | html! { <><> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: this opening fragment has no corresponding closing fragment
|
error: this opening fragment has no corresponding closing fragment
|
||||||
--> $DIR/list-fail.rs:7:13
|
--> tests/html_macro/list-fail.rs:7:13
|
||||||
|
|
|
|
||||||
7 | html! { <><></> };
|
7 | html! { <><></> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: this closing fragment has no corresponding opening fragment
|
error: this closing fragment has no corresponding opening fragment
|
||||||
--> $DIR/list-fail.rs:10:13
|
--> tests/html_macro/list-fail.rs:10:13
|
||||||
|
|
|
|
||||||
10 | html! { </> };
|
10 | html! { </> };
|
||||||
| ^^^
|
| ^^^
|
||||||
|
|
||||||
error: this closing fragment has no corresponding opening fragment
|
error: this closing fragment has no corresponding opening fragment
|
||||||
--> $DIR/list-fail.rs:11:13
|
--> tests/html_macro/list-fail.rs:11:13
|
||||||
|
|
|
|
||||||
11 | html! { </></> };
|
11 | html! { </></> };
|
||||||
| ^^^
|
| ^^^
|
||||||
|
|
||||||
error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`)
|
error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`)
|
||||||
--> $DIR/list-fail.rs:14:18
|
--> tests/html_macro/list-fail.rs:14:18
|
||||||
|
|
|
|
||||||
14 | html! { <></><></> };
|
14 | html! { <></><></> };
|
||||||
| ^^^^^
|
| ^^^^^
|
||||||
|
|
||||||
error: expected a valid html element
|
error: expected a valid html element
|
||||||
--> $DIR/list-fail.rs:16:15
|
--> tests/html_macro/list-fail.rs:16:15
|
||||||
|
|
|
|
||||||
16 | html! { <>invalid</> };
|
16 | html! { <>invalid</> };
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
||||||
error: expected an expression following this equals sign
|
error: expected an expression following this equals sign
|
||||||
--> $DIR/list-fail.rs:18:17
|
--> tests/html_macro/list-fail.rs:18:17
|
||||||
|
|
|
|
||||||
18 | html! { <key=></> };
|
18 | html! { <key=></> };
|
||||||
| ^^
|
| ^^
|
||||||
|
|
||||||
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
|
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: MethodCall(
|
||||||
--> $DIR/list-fail.rs:20:18
|
ExprMethodCall {
|
||||||
|
attrs: [],
|
||||||
|
receiver: Lit(
|
||||||
|
ExprLit {
|
||||||
|
attrs: [],
|
||||||
|
lit: Str(
|
||||||
|
LitStr {
|
||||||
|
token: "key",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
dot_token: Dot,
|
||||||
|
method: Ident {
|
||||||
|
ident: "to_string",
|
||||||
|
span: #0 bytes(404..413),
|
||||||
|
},
|
||||||
|
turbofish: None,
|
||||||
|
paren_token: Paren,
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
--> tests/html_macro/list-fail.rs:20:18
|
||||||
|
|
|
|
||||||
20 | html! { <key="key".to_string()></key> };
|
20 | html! { <key="key".to_string()></key> };
|
||||||
| ^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: only a single `key` prop is allowed on a fragment
|
error: only a single `key` prop is allowed on a fragment
|
||||||
--> $DIR/list-fail.rs:23:30
|
--> tests/html_macro/list-fail.rs:23:30
|
||||||
|
|
|
|
||||||
23 | html! { <key="first key" key="second key" /> };
|
23 | html! { <key="first key" key="second key" /> };
|
||||||
| ^^^
|
| ^^^
|
||||||
|
|
||||||
error: fragments only accept the `key` prop
|
error: fragments only accept the `key` prop
|
||||||
--> $DIR/list-fail.rs:25:14
|
--> tests/html_macro/list-fail.rs:25:14
|
||||||
|
|
|
|
||||||
25 | html! { <some_attr="test"></> };
|
25 | html! { <some_attr="test"></> };
|
||||||
| ^^^^^^^^^
|
| ^^^^^^^^^
|
||||||
|
|||||||
@ -2,13 +2,14 @@ use std::collections::HashMap;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
|
use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
|
||||||
use yew::AttrValue;
|
use yew::AttrValue;
|
||||||
|
|
||||||
use super::Apply;
|
use super::Apply;
|
||||||
use crate::dom_bundle::BSubtree;
|
use crate::dom_bundle::BSubtree;
|
||||||
use crate::virtual_dom::vtag::{InputFields, Value};
|
use crate::virtual_dom::vtag::{InputFields, Value};
|
||||||
use crate::virtual_dom::Attributes;
|
use crate::virtual_dom::{ApplyAttributeAs, Attributes};
|
||||||
|
|
||||||
impl<T: AccessValue> Apply for Value<T> {
|
impl<T: AccessValue> Apply for Value<T> {
|
||||||
type Bundle = Self;
|
type Bundle = Self;
|
||||||
@ -87,23 +88,23 @@ impl Attributes {
|
|||||||
#[cold]
|
#[cold]
|
||||||
fn apply_diff_index_maps(
|
fn apply_diff_index_maps(
|
||||||
el: &Element,
|
el: &Element,
|
||||||
new: &IndexMap<AttrValue, AttrValue>,
|
new: &IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)>,
|
||||||
old: &IndexMap<AttrValue, AttrValue>,
|
old: &IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)>,
|
||||||
) {
|
) {
|
||||||
for (key, value) in new.iter() {
|
for (key, value) in new.iter() {
|
||||||
match old.get(key) {
|
match old.get(key) {
|
||||||
Some(old_value) => {
|
Some(old_value) => {
|
||||||
if value != old_value {
|
if value != old_value {
|
||||||
Self::set_attribute(el, key, value);
|
Self::set(el, key, value.0.as_ref(), value.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Self::set_attribute(el, key, value),
|
None => Self::set(el, key, value.0.as_ref(), value.1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, _value) in old.iter() {
|
for (key, (_, apply_as)) in old.iter() {
|
||||||
if !new.contains_key(key) {
|
if !new.contains_key(key) {
|
||||||
Self::remove_attribute(el, key);
|
Self::remove(el, key, *apply_as);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,17 +113,26 @@ impl Attributes {
|
|||||||
/// Works with any [Attributes] variants.
|
/// Works with any [Attributes] variants.
|
||||||
#[cold]
|
#[cold]
|
||||||
fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
|
fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
|
||||||
fn collect(src: &Attributes) -> HashMap<&str, &str> {
|
fn collect(src: &Attributes) -> HashMap<&str, (&str, ApplyAttributeAs)> {
|
||||||
use Attributes::*;
|
use Attributes::*;
|
||||||
|
|
||||||
match src {
|
match src {
|
||||||
Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(),
|
Static(arr) => (*arr)
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v, apply_as)| (*k, (*v, *apply_as)))
|
||||||
|
.collect(),
|
||||||
Dynamic { keys, values } => keys
|
Dynamic { keys, values } => keys
|
||||||
.iter()
|
.iter()
|
||||||
.zip(values.iter())
|
.zip(values.iter())
|
||||||
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref())))
|
.filter_map(|(k, v)| {
|
||||||
|
v.as_ref()
|
||||||
|
.map(|(v, apply_as)| (*k, (v.as_ref(), *apply_as)))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
IndexMap(m) => m
|
||||||
|
.iter()
|
||||||
|
.map(|(k, (v, apply_as))| (k.as_ref(), (v.as_ref(), *apply_as)))
|
||||||
.collect(),
|
.collect(),
|
||||||
IndexMap(m) => m.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,25 +145,42 @@ impl Attributes {
|
|||||||
Some(old) => old != new,
|
Some(old) => old != new,
|
||||||
None => true,
|
None => true,
|
||||||
} {
|
} {
|
||||||
el.set_attribute(k, new).unwrap();
|
Self::set(el, k, new.0, new.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove missing
|
// Remove missing
|
||||||
for k in old.keys() {
|
for (k, (_, apply_as)) in old.iter() {
|
||||||
if !new.contains_key(k) {
|
if !new.contains_key(k) {
|
||||||
Self::remove_attribute(el, k);
|
Self::remove(el, k, *apply_as);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_attribute(el: &Element, key: &str, value: &str) {
|
fn set(el: &Element, key: &str, value: &str, apply_as: ApplyAttributeAs) {
|
||||||
el.set_attribute(key, value).expect("invalid attribute key")
|
match apply_as {
|
||||||
|
ApplyAttributeAs::Attribute => {
|
||||||
|
el.set_attribute(key, value).expect("invalid attribute key")
|
||||||
|
}
|
||||||
|
ApplyAttributeAs::Property => {
|
||||||
|
let key = JsValue::from_str(key);
|
||||||
|
let value = JsValue::from_str(value);
|
||||||
|
js_sys::Reflect::set(el.as_ref(), &key, &value).expect("could not set property");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_attribute(el: &Element, key: &str) {
|
fn remove(el: &Element, key: &str, apply_as: ApplyAttributeAs) {
|
||||||
el.remove_attribute(key)
|
match apply_as {
|
||||||
.expect("could not remove attribute")
|
ApplyAttributeAs::Attribute => el
|
||||||
|
.remove_attribute(key)
|
||||||
|
.expect("could not remove attribute"),
|
||||||
|
ApplyAttributeAs::Property => {
|
||||||
|
let key = JsValue::from_str(key);
|
||||||
|
js_sys::Reflect::set(el.as_ref(), &key, &JsValue::UNDEFINED)
|
||||||
|
.expect("could not remove property");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,20 +191,20 @@ impl Apply for Attributes {
|
|||||||
fn apply(self, _root: &BSubtree, el: &Element) -> Self {
|
fn apply(self, _root: &BSubtree, el: &Element) -> Self {
|
||||||
match &self {
|
match &self {
|
||||||
Self::Static(arr) => {
|
Self::Static(arr) => {
|
||||||
for kv in arr.iter() {
|
for (k, v, apply_as) in arr.iter() {
|
||||||
Self::set_attribute(el, kv[0], kv[1]);
|
Self::set(el, *k, *v, *apply_as);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Dynamic { keys, values } => {
|
Self::Dynamic { keys, values } => {
|
||||||
for (k, v) in keys.iter().zip(values.iter()) {
|
for (k, v) in keys.iter().zip(values.iter()) {
|
||||||
if let Some(v) = v {
|
if let Some((v, apply_as)) = v {
|
||||||
Self::set_attribute(el, k, v)
|
Self::set(el, k, v, *apply_as)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::IndexMap(m) => {
|
Self::IndexMap(m) => {
|
||||||
for (k, v) in m.iter() {
|
for (k, (v, apply_as)) in m.iter() {
|
||||||
Self::set_attribute(el, k, v)
|
Self::set(el, k, v, *apply_as)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,7 +244,7 @@ impl Apply for Attributes {
|
|||||||
}
|
}
|
||||||
macro_rules! set {
|
macro_rules! set {
|
||||||
($new:expr) => {
|
($new:expr) => {
|
||||||
Self::set_attribute(el, key!(), $new)
|
Self::set(el, key!(), $new.0.as_ref(), $new.1)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,8 +255,8 @@ impl Apply for Attributes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(new), None) => set!(new),
|
(Some(new), None) => set!(new),
|
||||||
(None, Some(_)) => {
|
(None, Some(old)) => {
|
||||||
Self::remove_attribute(el, key!());
|
Self::remove(el, key!(), old.1);
|
||||||
}
|
}
|
||||||
(None, None) => (),
|
(None, None) => (),
|
||||||
}
|
}
|
||||||
@ -247,3 +274,111 @@ impl Apply for Attributes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gloo::utils::document;
|
||||||
|
use js_sys::Reflect;
|
||||||
|
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{function_component, html, Html};
|
||||||
|
|
||||||
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
fn create_element() -> (Element, BSubtree) {
|
||||||
|
let element = document()
|
||||||
|
.create_element("a")
|
||||||
|
.expect("failed to create element");
|
||||||
|
let btree = BSubtree::create_root(&element);
|
||||||
|
(element, btree)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn properties_are_set() {
|
||||||
|
let attrs = Attributes::Static(&[
|
||||||
|
("href", "https://example.com/", ApplyAttributeAs::Property),
|
||||||
|
("alt", "somewhere", ApplyAttributeAs::Property),
|
||||||
|
]);
|
||||||
|
let (element, btree) = create_element();
|
||||||
|
attrs.apply(&btree, &element);
|
||||||
|
assert_eq!(
|
||||||
|
Reflect::get(element.as_ref(), &JsValue::from_str("href"))
|
||||||
|
.expect("no href")
|
||||||
|
.as_string()
|
||||||
|
.expect("not a string"),
|
||||||
|
"https://example.com/",
|
||||||
|
"property `href` not set properly"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
|
||||||
|
.expect("no alt")
|
||||||
|
.as_string()
|
||||||
|
.expect("not a string"),
|
||||||
|
"somewhere",
|
||||||
|
"property `alt` not set properly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn respects_apply_as() {
|
||||||
|
let attrs = Attributes::Static(&[
|
||||||
|
("href", "https://example.com/", ApplyAttributeAs::Attribute),
|
||||||
|
("alt", "somewhere", ApplyAttributeAs::Property),
|
||||||
|
]);
|
||||||
|
let (element, btree) = create_element();
|
||||||
|
attrs.apply(&btree, &element);
|
||||||
|
assert_eq!(
|
||||||
|
element.outer_html(),
|
||||||
|
"<a href=\"https://example.com/\"></a>",
|
||||||
|
"should be set as attribute"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
|
||||||
|
.expect("no alt")
|
||||||
|
.as_string()
|
||||||
|
.expect("not a string"),
|
||||||
|
"somewhere",
|
||||||
|
"property `alt` not set properly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn class_is_always_attrs() {
|
||||||
|
let attrs = Attributes::Static(&[("class", "thing", ApplyAttributeAs::Attribute)]);
|
||||||
|
|
||||||
|
let (element, btree) = create_element();
|
||||||
|
attrs.apply(&btree, &element);
|
||||||
|
assert_eq!(element.get_attribute("class").unwrap(), "thing");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
async fn macro_syntax_works() {
|
||||||
|
#[function_component]
|
||||||
|
fn Comp() -> Html {
|
||||||
|
html! { <a href="https://example.com/" ~alt="abc" /> }
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = gloo::utils::document().get_element_by_id("output").unwrap();
|
||||||
|
yew::Renderer::<Comp>::with_root(output.clone()).render();
|
||||||
|
|
||||||
|
gloo::timers::future::sleep(Duration::from_secs(1)).await;
|
||||||
|
let element = output.query_selector("a").unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
element.get_attribute("href").unwrap(),
|
||||||
|
"https://example.com/"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
|
||||||
|
.expect("no alt")
|
||||||
|
.as_string()
|
||||||
|
.expect("not a string"),
|
||||||
|
"abc",
|
||||||
|
"property `alt` not set properly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -970,7 +970,7 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.outer_html(),
|
.outer_html(),
|
||||||
"<div tabindex=\"0\"></div>"
|
"<div tabindex=\"0\"></div>"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -145,6 +145,14 @@ mod feat_ssr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defines if the [`Attributes`] is set as element's attribute or property
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
|
pub enum ApplyAttributeAs {
|
||||||
|
Attribute,
|
||||||
|
Property,
|
||||||
|
}
|
||||||
|
|
||||||
/// A collection of attributes for an element
|
/// A collection of attributes for an element
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
pub enum Attributes {
|
pub enum Attributes {
|
||||||
@ -152,7 +160,7 @@ pub enum Attributes {
|
|||||||
///
|
///
|
||||||
/// Allows optimizing comparison to a simple pointer equality check and reducing allocations,
|
/// Allows optimizing comparison to a simple pointer equality check and reducing allocations,
|
||||||
/// if the attributes do not change on a node.
|
/// if the attributes do not change on a node.
|
||||||
Static(&'static [[&'static str; 2]]),
|
Static(&'static [(&'static str, &'static str, ApplyAttributeAs)]),
|
||||||
|
|
||||||
/// Static list of attribute keys with possibility to exclude attributes and dynamic attribute
|
/// Static list of attribute keys with possibility to exclude attributes and dynamic attribute
|
||||||
/// values.
|
/// values.
|
||||||
@ -165,12 +173,12 @@ pub enum Attributes {
|
|||||||
|
|
||||||
/// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are
|
/// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are
|
||||||
/// designated by setting [None].
|
/// designated by setting [None].
|
||||||
values: Box<[Option<AttrValue>]>,
|
values: Box<[Option<(AttrValue, ApplyAttributeAs)>]>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro
|
/// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro
|
||||||
/// was not used to guarantee it.
|
/// was not used to guarantee it.
|
||||||
IndexMap(IndexMap<AttrValue, AttrValue>),
|
IndexMap(IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Attributes {
|
impl Attributes {
|
||||||
@ -183,19 +191,19 @@ impl Attributes {
|
|||||||
/// This function is suboptimal and does not inline well. Avoid on hot paths.
|
/// This function is suboptimal and does not inline well. Avoid on hot paths.
|
||||||
pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'a str, &'a str)> + 'a> {
|
pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'a str, &'a str)> + 'a> {
|
||||||
match self {
|
match self {
|
||||||
Self::Static(arr) => Box::new(arr.iter().map(|kv| (kv[0], kv[1] as &'a str))),
|
Self::Static(arr) => Box::new(arr.iter().map(|(k, v, _)| (*k, *v as &'a str))),
|
||||||
Self::Dynamic { keys, values } => Box::new(
|
Self::Dynamic { keys, values } => Box::new(
|
||||||
keys.iter()
|
keys.iter()
|
||||||
.zip(values.iter())
|
.zip(values.iter())
|
||||||
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref()))),
|
.filter_map(|(k, v)| v.as_ref().map(|(v, _)| (*k, v.as_ref()))),
|
||||||
),
|
),
|
||||||
Self::IndexMap(m) => Box::new(m.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))),
|
Self::IndexMap(m) => Box::new(m.iter().map(|(k, (v, _))| (k.as_ref(), v.as_ref()))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a mutable reference to the underlying `IndexMap`.
|
/// Get a mutable reference to the underlying `IndexMap`.
|
||||||
/// If the attributes are stored in the `Vec` variant, it will be converted.
|
/// If the attributes are stored in the `Vec` variant, it will be converted.
|
||||||
pub fn get_mut_index_map(&mut self) -> &mut IndexMap<AttrValue, AttrValue> {
|
pub fn get_mut_index_map(&mut self) -> &mut IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)> {
|
||||||
macro_rules! unpack {
|
macro_rules! unpack {
|
||||||
() => {
|
() => {
|
||||||
match self {
|
match self {
|
||||||
@ -209,7 +217,11 @@ impl Attributes {
|
|||||||
match self {
|
match self {
|
||||||
Self::IndexMap(m) => m,
|
Self::IndexMap(m) => m,
|
||||||
Self::Static(arr) => {
|
Self::Static(arr) => {
|
||||||
*self = Self::IndexMap(arr.iter().map(|kv| (kv[0].into(), kv[1].into())).collect());
|
*self = Self::IndexMap(
|
||||||
|
arr.iter()
|
||||||
|
.map(|(k, v, ty)| ((*k).into(), ((*v).into(), *ty)))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
unpack!()
|
unpack!()
|
||||||
}
|
}
|
||||||
Self::Dynamic { keys, values } => {
|
Self::Dynamic { keys, values } => {
|
||||||
@ -227,7 +239,11 @@ impl Attributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<IndexMap<AttrValue, AttrValue>> for Attributes {
|
impl From<IndexMap<AttrValue, AttrValue>> for Attributes {
|
||||||
fn from(v: IndexMap<AttrValue, AttrValue>) -> Self {
|
fn from(map: IndexMap<AttrValue, AttrValue>) -> Self {
|
||||||
|
let v = map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, (v, ApplyAttributeAs::Attribute)))
|
||||||
|
.collect();
|
||||||
Self::IndexMap(v)
|
Self::IndexMap(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,7 +252,7 @@ impl From<IndexMap<&'static str, AttrValue>> for Attributes {
|
|||||||
fn from(v: IndexMap<&'static str, AttrValue>) -> Self {
|
fn from(v: IndexMap<&'static str, AttrValue>) -> Self {
|
||||||
let v = v
|
let v = v
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| (AttrValue::Static(k), v))
|
.map(|(k, v)| (AttrValue::Static(k), (v, ApplyAttributeAs::Attribute)))
|
||||||
.collect();
|
.collect();
|
||||||
Self::IndexMap(v)
|
Self::IndexMap(v)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
|
use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
|
||||||
|
|
||||||
use super::{AttrValue, Attributes, Key, Listener, Listeners, VList, VNode};
|
use super::{ApplyAttributeAs, AttrValue, Attributes, Key, Listener, Listeners, VList, VNode};
|
||||||
use crate::html::{IntoPropValue, NodeRef};
|
use crate::html::{IntoPropValue, NodeRef};
|
||||||
|
|
||||||
/// SVG namespace string used for creating svg elements
|
/// SVG namespace string used for creating svg elements
|
||||||
@ -363,9 +363,20 @@ impl VTag {
|
|||||||
/// Not every attribute works when it set as an attribute. We use workarounds for:
|
/// Not every attribute works when it set as an attribute. We use workarounds for:
|
||||||
/// `value` and `checked`.
|
/// `value` and `checked`.
|
||||||
pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
|
pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
|
||||||
self.attributes
|
self.attributes.get_mut_index_map().insert(
|
||||||
.get_mut_index_map()
|
AttrValue::Static(key),
|
||||||
.insert(AttrValue::Static(key), value.into());
|
(value.into(), ApplyAttributeAs::Attribute),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the given key as property on the element
|
||||||
|
///
|
||||||
|
/// [`js_sys::Reflect`] is used for setting properties.
|
||||||
|
pub fn add_property(&mut self, key: &'static str, value: impl Into<AttrValue>) {
|
||||||
|
self.attributes.get_mut_index_map().insert(
|
||||||
|
AttrValue::Static(key),
|
||||||
|
(value.into(), ApplyAttributeAs::Property),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets attributes to a virtual node.
|
/// Sets attributes to a virtual node.
|
||||||
@ -378,9 +389,10 @@ impl VTag {
|
|||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) {
|
pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) {
|
||||||
self.attributes
|
self.attributes.get_mut_index_map().insert(
|
||||||
.get_mut_index_map()
|
AttrValue::from(key),
|
||||||
.insert(AttrValue::from(key), value.into_prop_value());
|
(value.into_prop_value(), ApplyAttributeAs::Property),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add event listener on the [VTag]'s [Element](web_sys::Element).
|
/// Add event listener on the [VTag]'s [Element](web_sys::Element).
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use yew::platform::time::sleep;
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew::suspense::{use_future, use_future_with_deps, Suspension, SuspensionResult};
|
use yew::suspense::{use_future, use_future_with_deps, Suspension, SuspensionResult};
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
async fn suspense_works() {
|
async fn suspense_works() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user