From eeddcb95bef53be183f50327263c13e0450d522a Mon Sep 17 00:00:00 2001 From: Xavientois <34867186+Xavientois@users.noreply.github.com> Date: Mon, 6 Sep 2021 01:41:51 -0400 Subject: [PATCH] Add "struct update" syntax to pass props to component (`..props` instead of `with props`) (#2024) * Reword to use double-dot syntax instead of "with" * Implement double-dot syntax for props in components * Update documentation with new syntax * Update forgotten doc * Add descriptive comments * Check props and base expression * Make compatible with 1.49.0 by removing then * Fix website tests * Update error output * Implicitly convert string literals to String if they are listed as props * Remove unused keyword * Rename function for checking if string literal * Fix weird formatting * Update code based on review * Update website/docs/concepts/html/components.md Co-authored-by: mc1098 * Base expression span includes dot2 now * Improve specificity of error message * Chain together error messages * Add an example failure case to illustrate combined error message * Update based on review comments * Fix missing clones Co-authored-by: mc1098 --- .../yew-macro/src/html_tree/html_component.rs | 13 +- packages/yew-macro/src/props/component.rs | 212 +++++----- packages/yew-macro/src/props/mod.rs | 2 + packages/yew-macro/src/props/prop.rs | 105 +++-- .../tests/html_macro/component-fail.rs | 22 +- .../tests/html_macro/component-fail.stderr | 386 +++++++++++------- .../tests/html_macro/component-pass.rs | 41 +- packages/yew/src/lib.rs | 4 +- packages/yew/src/virtual_dom/vcomp.rs | 10 +- website/docs/concepts/html/components.md | 14 +- .../current/concepts/html/components.md | 5 +- .../current/concepts/html/components.md | 2 +- .../current/concepts/html/components.md | 2 +- 13 files changed, 509 insertions(+), 309 deletions(-) diff --git a/packages/yew-macro/src/html_tree/html_component.rs b/packages/yew-macro/src/html_tree/html_component.rs index f95a42327..6f7034404 100644 --- a/packages/yew-macro/src/html_tree/html_component.rs +++ b/packages/yew-macro/src/html_tree/html_component.rs @@ -68,14 +68,11 @@ impl Parse for HtmlComponent { input.parse::()?; if !children.is_empty() { - // check if the `children` prop is given explicitly - if let ComponentProps::List(props) = &open.props { - if let Some(children_prop) = props.get_by_label("children") { - return Err(syn::Error::new_spanned( - &children_prop.label, - "cannot specify the `children` prop when the component already has children", - )); - } + if let Some(children_prop) = open.props.children() { + return Err(syn::Error::new_spanned( + &children_prop.label, + "cannot specify the `children` prop when the component already has children", + )); } } diff --git a/packages/yew-macro/src/props/component.rs b/packages/yew-macro/src/props/component.rs index b777e943c..f8bade77f 100644 --- a/packages/yew-macro/src/props/component.rs +++ b/packages/yew-macro/src/props/component.rs @@ -1,99 +1,52 @@ -use super::{Prop, Props, SpecialProps}; -use proc_macro2::{Ident, TokenStream, TokenTree}; +use super::{Prop, Props, SpecialProps, CHILDREN_LABEL}; +use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use std::convert::TryFrom; use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, - Expr, Token, + token::Dot2, + Expr, ExprLit, Lit, }; -mod kw { - syn::custom_keyword!(with); -} - -pub struct WithProps { - pub special: SpecialProps, - pub with: kw::with, +struct BaseExpr { + pub dot2: Dot2, pub expr: Expr, } -impl WithProps { - /// Check if the `ParseStream` contains a `with expr` expression. - /// This function advances the given `ParseStream`! - fn contains_with_expr(input: ParseStream) -> bool { - while !input.is_empty() { - if input.peek(kw::with) && !input.peek2(Token![=]) { - return true; - } - input.parse::().ok(); - } - false - } -} -impl Parse for WithProps { +impl Parse for BaseExpr { fn parse(input: ParseStream) -> syn::Result { - let mut special = SpecialProps::default(); - let mut with_expr: Option<(kw::with, Expr)> = None; - while !input.is_empty() { - // no need to check if it's followed by `=` because `with` isn't a special prop - if input.peek(kw::with) { - if let Some((with, expr)) = with_expr { - return Err(syn::Error::new_spanned( - quote! { #with#expr }, - "there are two `with ` definitions for this component (note: you can only define `with ` once)" - )); - } - let with = input.parse::()?; - if input.is_empty() { - return Err(syn::Error::new_spanned( - with, - "expected expression following this `with`", - )); - } - with_expr = Some((with, input.parse()?)); - } else { - let prop = input.parse::()?; - - if let Some(slot) = special.get_slot_mut(&prop.label.to_string()) { - if slot.is_some() { - return Err(syn::Error::new_spanned( - &prop.label, - &format!("`{}` can only be set once", prop.label), - )); - } - slot.replace(prop); - } else { - return Err(syn::Error::new_spanned( - prop.label, - "Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`)", - )); - } - } - } - - let (with, expr) = - with_expr.ok_or_else(|| input.error("missing `with props` expression"))?; - - Ok(Self { - special, - with, - expr, - }) + let dot2 = input.parse()?; + let expr = input.parse().map_err(|expr_error| { + let mut error = + syn::Error::new_spanned(dot2, "expected base props expression after `..`"); + error.combine(expr_error); + error + })?; + Ok(Self { dot2, expr }) } } -pub enum ComponentProps { - List(Props), - With(Box), +impl ToTokens for BaseExpr { + fn to_tokens(&self, tokens: &mut TokenStream) { + let BaseExpr { dot2, expr } = self; + tokens.extend(quote! { #dot2#expr }); + } +} + +pub struct ComponentProps { + props: Props, + base_expr: Option, } impl ComponentProps { /// Get the special props supported by both variants pub fn special(&self) -> &SpecialProps { - match self { - Self::List(props) => &props.special, - Self::With(props) => &props.special, - } + &self.props.special + } + + // check if the `children` prop is given explicitly + pub fn children(&self) -> Option<&Prop> { + self.props.get_by_label(CHILDREN_LABEL) } fn prop_validation_tokens(&self, props_ty: impl ToTokens, has_children: bool) -> TokenStream { @@ -103,20 +56,16 @@ impl ComponentProps { None }; - let check_props = match self { - Self::List(props) => props - .iter() - .map(|Prop { label, .. }| { - quote_spanned! {label.span()=> __yew_props.#label; } - }) - .collect(), - Self::With(with_props) => { - let expr = &with_props.expr; + let check_props: TokenStream = self + .props + .iter() + .map(|Prop { label, .. }| quote_spanned! ( label.span()=> __yew_props.#label; )) + .chain(self.base_expr.iter().map(|expr| { quote_spanned! {props_ty.span()=> let _: #props_ty = #expr; } - } - }; + })) + .collect(); quote_spanned! {props_ty.span()=> #[allow(clippy::no_effect)] @@ -135,9 +84,9 @@ impl ComponentProps { children_renderer: Option, ) -> TokenStream { let validate_props = self.prop_validation_tokens(&props_ty, children_renderer.is_some()); - let build_props = match self { - Self::List(props) => { - let set_props = props.iter().map(|Prop { label, value, .. }| { + let build_props = match &self.base_expr { + None => { + let set_props = self.props.iter().map(|Prop { label, value, .. }| { quote_spanned! {value.span()=> .#label(#value) } @@ -156,17 +105,31 @@ impl ComponentProps { .build() } } - Self::With(with_props) => { + // Builder pattern is unnecessary in this case, since the base expression guarantees + // all values are initialized + Some(expr) => { let ident = Ident::new("__yew_props", props_ty.span()); + let set_props = self.props.iter().map(|Prop { label, value, .. }| { + if is_string_literal(value) { + // String literals should be implicitly converted into `String` + quote_spanned! {value.span()=> + #ident.#label = ::std::convert::Into::into(#value); + } + } else { + quote_spanned! {value.span()=> + #ident.#label = #value; + } + } + }); let set_children = children_renderer.map(|children| { quote_spanned! {props_ty.span()=> #ident.children = #children; } }); - let expr = &with_props.expr; quote! { let mut #ident = #expr; + #(#set_props)* #set_children #ident } @@ -181,12 +144,34 @@ impl ComponentProps { } } } + +fn is_string_literal(expr: &Expr) -> bool { + matches!( + expr, + Expr::Lit(ExprLit { + lit: Lit::Str(_), + .. + }) + ) +} + impl Parse for ComponentProps { fn parse(input: ParseStream) -> syn::Result { - if WithProps::contains_with_expr(&input.fork()) { - input.parse().map(Self::With) + let props = validate(input.parse()?)?; + let base_expr = if input.is_empty() { + None } else { - input.parse::().and_then(Self::try_from) + Some(input.parse::()?) + }; + + if input.is_empty() { + let base_expr = base_expr.map(|base| base.expr); + Ok(Self { props, base_expr }) + } else { + Err(syn::Error::new_spanned( + base_expr, + "base props expression must appear last in list of props", + )) } } } @@ -195,18 +180,25 @@ impl TryFrom for ComponentProps { type Error = syn::Error; fn try_from(props: Props) -> Result { - props.check_no_duplicates()?; - props.check_all(|prop| { - if !prop.label.extended.is_empty() { - Err(syn::Error::new_spanned( - &prop.label, - "expected a valid Rust identifier", - )) - } else { - Ok(()) - } - })?; - - Ok(Self::List(props)) + Ok(Self { + props: validate(props)?, + base_expr: None, + }) } } + +fn validate(props: Props) -> Result { + props.check_no_duplicates()?; + props.check_all(|prop| { + if !prop.label.extended.is_empty() { + Err(syn::Error::new_spanned( + &prop.label, + "expected a valid Rust identifier", + )) + } else { + Ok(()) + } + })?; + + Ok(props) +} diff --git a/packages/yew-macro/src/props/mod.rs b/packages/yew-macro/src/props/mod.rs index 9d7c18c32..42156df12 100644 --- a/packages/yew-macro/src/props/mod.rs +++ b/packages/yew-macro/src/props/mod.rs @@ -7,3 +7,5 @@ pub use component::*; pub use element::*; pub use prop::*; pub use prop_macro::PropsMacroInput; + +const CHILDREN_LABEL: &str = "children"; diff --git a/packages/yew-macro/src/props/prop.rs b/packages/yew-macro/src/props/prop.rs index 31b0e211e..856b0a25d 100644 --- a/packages/yew-macro/src/props/prop.rs +++ b/packages/yew-macro/src/props/prop.rs @@ -1,4 +1,6 @@ +use super::CHILDREN_LABEL; use crate::html_tree::HtmlDashedName; +use proc_macro2::{Spacing, TokenTree}; use std::{ cmp::Ordering, convert::TryFrom, @@ -6,9 +8,9 @@ use std::{ }; use syn::{ braced, - parse::{Parse, ParseStream}, + parse::{Parse, ParseBuffer, ParseStream}, token::Brace, - Block, Expr, ExprBlock, ExprPath, Stmt, Token, + Block, Expr, ExprBlock, ExprPath, ExprRange, Stmt, Token, }; pub struct Prop { @@ -74,14 +76,46 @@ impl Prop { "expected an expression following this equals sign", )); } - let value = strip_braces(input.parse::()?)?; + + let value = parse_prop_value(input)?; Ok(Self { label, value }) } } -fn strip_braces(expr: Expr) -> syn::Result { - match expr { - Expr::Block(ExprBlock { block: Block { mut stmts, .. }, .. }) if stmts.len() == 1 => { +fn parse_prop_value(input: &ParseBuffer) -> syn::Result { + if input.peek(Brace) { + strip_braces(input.parse()?) + } else { + let expr = if let Some(ExprRange { + from: Some(from), .. + }) = range_expression_peek(input) + { + // If a range expression is seen, treat the left-side expression as the value + // and leave the right-side expression to be parsed as a base expression + advance_until_next_dot2(input)?; + *from + } else { + input.parse()? + }; + + match &expr { + Expr::Lit(_) => Ok(expr), + _ => { + Err(syn::Error::new_spanned( + &expr, + "the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.", + )) + } + } + } +} + +fn strip_braces(block: ExprBlock) -> syn::Result { + match block { + ExprBlock { + block: Block { mut stmts, .. }, + .. + } if stmts.len() == 1 => { let stmt = stmts.remove(0); match stmt { Stmt::Expr(expr) => Ok(expr), @@ -95,14 +129,46 @@ fn strip_braces(expr: Expr) -> syn::Result { )) } } - Expr::Lit(_) | Expr::Block(_) => Ok(expr), - _ => Err(syn::Error::new_spanned( - &expr, - "the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.".to_string(), - )), + block => Ok(Expr::Block(block)), } } +// Without advancing cursor, returns the range expression at the current cursor position if any +fn range_expression_peek(input: &ParseBuffer) -> Option { + match input.fork().parse::().ok()? { + Expr::Range(range) => Some(range), + _ => None, + } +} + +fn advance_until_next_dot2(input: &ParseBuffer) -> syn::Result<()> { + input.step(|cursor| { + let mut rest = *cursor; + let mut first_dot = None; + while let Some((tt, next)) = rest.token_tree() { + match &tt { + TokenTree::Punct(punct) if punct.as_char() == '.' => { + if let Some(first_dot) = first_dot { + return Ok(((), first_dot)); + } else { + // Only consider dot as potential first if there is no spacing after it + first_dot = if punct.spacing() == Spacing::Joint { + Some(rest) + } else { + None + }; + } + } + _ => { + first_dot = None; + } + } + rest = next; + } + Err(cursor.error("no `..` found in expression")) + }) +} + /// List of props sorted in alphabetical order*. /// /// \*The "children" prop always comes last to match the behaviour of the `Properties` derive macro. @@ -111,8 +177,6 @@ fn strip_braces(expr: Expr) -> syn::Result { /// Use `check_no_duplicates` to ensure that there are no duplicates. pub struct SortedPropList(Vec); impl SortedPropList { - const CHILDREN_LABEL: &'static str = "children"; - /// Create a new `SortedPropList` from a vector of props. /// The given `props` doesn't need to be sorted. pub fn new(mut props: Vec) -> Self { @@ -123,9 +187,9 @@ impl SortedPropList { fn cmp_label(a: &str, b: &str) -> Ordering { if a == b { Ordering::Equal - } else if a == Self::CHILDREN_LABEL { + } else if a == CHILDREN_LABEL { Ordering::Greater - } else if b == Self::CHILDREN_LABEL { + } else if b == CHILDREN_LABEL { Ordering::Less } else { a.cmp(b) @@ -210,7 +274,8 @@ impl SortedPropList { impl Parse for SortedPropList { fn parse(input: ParseStream) -> syn::Result { let mut props: Vec = Vec::new(); - while !input.is_empty() { + // Stop parsing props if a base expression preceded by `..` is reached + while !input.is_empty() && !input.peek(Token![..]) { props.push(input.parse()?); } @@ -240,14 +305,6 @@ impl SpecialProps { Ok(Self { node_ref, key }) } - pub fn get_slot_mut(&mut self, key: &str) -> Option<&mut Option> { - match key { - Self::REF_LABEL => Some(&mut self.node_ref), - Self::KEY_LABEL => Some(&mut self.key), - _ => None, - } - } - fn iter(&self) -> impl Iterator { self.node_ref.as_ref().into_iter().chain(self.key.as_ref()) } diff --git a/packages/yew-macro/tests/html_macro/component-fail.rs b/packages/yew-macro/tests/html_macro/component-fail.rs index c1c3f39e7..12004c71a 100644 --- a/packages/yew-macro/tests/html_macro/component-fail.rs +++ b/packages/yew-macro/tests/html_macro/component-fail.rs @@ -43,20 +43,31 @@ fn compile_fail() { html! { }; html! { }; html! { }; + html! { }; + html! { }; html! { }; html! { }; + html! { }; let (p1, p2); html! { }; + html! { }; html! { }; + html! { }; html! { }; + html! { }; html! { }; + html! { }; html! { }; + html! { }; html! { }; + html! { }; html! { }; + html! { }; html! { }; - html! { }; - html! { }; - html! { }; + html! { }; + html! { }; + html! { }; + html! { }; html! { }; html! { }; html! { }; @@ -75,9 +86,12 @@ fn compile_fail() { html! { }; html! { { "Not allowed" } }; + let num = 1; + html! { }; + // trying to overwrite `children` on props which don't take any. html! { - + { "please error" } }; diff --git a/packages/yew-macro/tests/html_macro/component-fail.stderr b/packages/yew-macro/tests/html_macro/component-fail.stderr index 88456e635..f52324423 100644 --- a/packages/yew-macro/tests/html_macro/component-fail.stderr +++ b/packages/yew-macro/tests/html_macro/component-fail.stderr @@ -10,229 +10,335 @@ error: unexpected end of input, expected identifier 44 | html! { }; | ^^^^^^^^^^^ -error: expected expression following this `with` +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) --> $DIR/component-fail.rs:45:20 | 45 | html! { }; | ^^^^ -error: `props` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) +error: expected base props expression after `..` --> $DIR/component-fail.rs:46:20 | -46 | html! { }; +46 | html! { }; + | ^^ + +error: unexpected end of input, expected expression + --> $DIR/component-fail.rs:46:13 + | +46 | html! { }; + | ^^^^^^^^^^^^ + +error: expected base props expression after `..` + --> $DIR/component-fail.rs:47:20 + | +47 | html! { }; + | ^^ + +error: unexpected end of input, expected expression + --> $DIR/component-fail.rs:47:22 + | +47 | html! { }; + | ^^^^^^^ + +error: `props` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> $DIR/component-fail.rs:48:20 + | +48 | html! { }; | ^^^^^ -error: this opening tag has no corresponding closing tag - --> $DIR/component-fail.rs:47:13 - | -47 | html! { }; - | ^^^^^^^^^^^^^^^^^^^ - -error: there are two `with ` definitions for this component (note: you can only define `with ` once) +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) --> $DIR/component-fail.rs:49:20 | -49 | html! { }; - | ^^^^^^^ +49 | html! { }; + | ^^^^ -error: `ref` can only be set once - --> $DIR/component-fail.rs:50:40 +error: this opening tag has no corresponding closing tag + --> $DIR/component-fail.rs:50:13 | -50 | html! { }; - | ^^^ +50 | html! { }; + | ^^^^^^^^^^^^^^^^ -error: `ref` can only be set once - --> $DIR/component-fail.rs:51:40 +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> $DIR/component-fail.rs:52:20 | -51 | html! { }; - | ^^^ +52 | html! { }; + | ^^^^ -error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) - --> $DIR/component-fail.rs:52:40 - | -52 | html! { }; - | ^^^^^ - -error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) - --> $DIR/component-fail.rs:53:31 - | -53 | html! { }; - | ^^^^^ - -error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) --> $DIR/component-fail.rs:54:20 | -54 | html! { }; - | ^^^^^ +54 | html! { }; + | ^^^^ -error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) +error: base props expression must appear last in list of props --> $DIR/component-fail.rs:55:20 | -55 | html! { }; - | ^^^^^ +55 | html! { }; + | ^^^^^^^ -error: `ref` can only be set once - --> $DIR/component-fail.rs:56:29 +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> $DIR/component-fail.rs:56:20 | -56 | html! { }; - | ^^^ +56 | html! { }; + | ^^^^ -error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) +error: base props expression must appear last in list of props + --> $DIR/component-fail.rs:57:20 + | +57 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) --> $DIR/component-fail.rs:58:20 | -58 | html! { }; - | ^^^^^ +58 | html! { }; + | ^^^^ -error: Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to special props like `ref` and `key`) - --> $DIR/component-fail.rs:59:31 +error: base props expression must appear last in list of props + --> $DIR/component-fail.rs:59:20 | -59 | html! { }; - | ^^^^^ +59 | html! { }; + | ^^^^^^^ -error: expected identifier, found keyword `type` +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) --> $DIR/component-fail.rs:60:20 | -60 | html! { }; +60 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> $DIR/component-fail.rs:61:20 + | +61 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> $DIR/component-fail.rs:62:28 + | +62 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> $DIR/component-fail.rs:63:28 + | +63 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> $DIR/component-fail.rs:64:37 + | +64 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> $DIR/component-fail.rs:65:37 + | +65 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> $DIR/component-fail.rs:66:47 + | +66 | html! { }; + | ^^^^ + +error: `ref` can only be specified once + --> $DIR/component-fail.rs:67:20 + | +67 | html! { }; + | ^^^ + +error: base props expression must appear last in list of props + --> $DIR/component-fail.rs:70:20 + | +70 | html! { }; + | ^^^^^^^^ + +error: expected identifier, found keyword `type` + --> $DIR/component-fail.rs:71:20 + | +71 | html! { }; | ^^^^ expected identifier, found keyword | help: you can escape reserved keywords to use them as identifiers | -60 | html! { }; +71 | html! { }; | ^^^^^^ error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. - --> $DIR/component-fail.rs:61:24 + --> $DIR/component-fail.rs:72:24 | -61 | html! { }; +72 | html! { }; | ^^ error: expected a valid Rust identifier - --> $DIR/component-fail.rs:62:20 + --> $DIR/component-fail.rs:73:20 | -62 | html! { }; +73 | html! { }; | ^^^^^^^^^^^^^^^^^ error: expected an expression following this equals sign - --> $DIR/component-fail.rs:64:26 + --> $DIR/component-fail.rs:75:26 | -64 | html! { }; +75 | html! { }; | ^ error: `int` can only be specified once but is given here again - --> $DIR/component-fail.rs:65:26 + --> $DIR/component-fail.rs:76:26 | -65 | html! { }; +76 | html! { }; | ^^^ error: `int` can only be specified once but is given here again - --> $DIR/component-fail.rs:65:32 + --> $DIR/component-fail.rs:76:32 | -65 | html! { }; +76 | html! { }; | ^^^ error: `ref` can only be specified once - --> $DIR/component-fail.rs:70:26 + --> $DIR/component-fail.rs:81:26 | -70 | html! { }; +81 | html! { }; | ^^^ error: this closing tag has no corresponding opening tag - --> $DIR/component-fail.rs:73:13 + --> $DIR/component-fail.rs:84:13 | -73 | html! { }; +84 | html! { }; | ^^^^^^^^ error: this opening tag has no corresponding closing tag - --> $DIR/component-fail.rs:74:13 + --> $DIR/component-fail.rs:85:13 | -74 | html! { }; +85 | html! { }; | ^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) - --> $DIR/component-fail.rs:75:28 + --> $DIR/component-fail.rs:86:28 | -75 | html! { }; +86 | html! { }; | ^^^^^^^^^^^^^^^ -error: cannot specify the `children` prop when the component already has children - --> $DIR/component-fail.rs:94:26 +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. + --> $DIR/component-fail.rs:90:24 | -94 | - | ^^^^^^^^ +90 | html! { }; + | ^^^ + +error: cannot specify the `children` prop when the component already has children + --> $DIR/component-fail.rs:108:26 + | +108 | + | ^^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) - --> $DIR/component-fail.rs:101:9 + --> $DIR/component-fail.rs:115:9 | -101 | { 2 } +115 | { 2 } | ^^^^^^^^^^^^^^^^^^ error: only simple identifiers are allowed in the shorthand property syntax - --> $DIR/component-fail.rs:104:21 + --> $DIR/component-fail.rs:118:21 | -104 | html! { }; +118 | html! { }; | ^^^^^^^^^^^^^^^^^^^^ error: missing label for property value. If trying to use the shorthand property syntax, only identifiers may be used - --> $DIR/component-fail.rs:105:21 + --> $DIR/component-fail.rs:119:21 | -105 | html! { }; +119 | html! { }; | ^^^^^ error: missing label for property value. If trying to use the shorthand property syntax, only identifiers may be used - --> $DIR/component-fail.rs:106:21 + --> $DIR/component-fail.rs:120:21 | -106 | html! { }; +120 | html! { }; | ^^^^^^^^^^^^^^ error[E0425]: cannot find value `blah` in this scope - --> $DIR/component-fail.rs:57:25 + --> $DIR/component-fail.rs:68:22 | -57 | html! { }; - | ^^^^ not found in this scope +68 | html! { }; + | ^^^^ not found in this scope + +error[E0425]: cannot find value `props` in this scope + --> $DIR/component-fail.rs:69:30 + | +69 | html! { }; + | ^^^^^ not found in this scope + +error[E0308]: mismatched types + --> $DIR/component-fail.rs:53:22 + | +53 | html! { }; + | ----- ^^^^^^^ expected struct `ChildProperties`, found struct `std::ops::Range` + | | + | expected due to this + | + = note: expected struct `ChildProperties` + found struct `std::ops::Range<_>` + +error[E0308]: mismatched types + --> $DIR/component-fail.rs:53:14 + | +53 | html! { }; + | ^^^^^ expected struct `ChildProperties`, found struct `std::ops::Range` + | + = note: expected struct `ChildProperties` + found struct `std::ops::Range<_>` + +error[E0609]: no field `value` on type `ChildProperties` + --> $DIR/component-fail.rs:69:20 + | +69 | html! { }; + | ^^^^^ unknown field + | + = note: available fields are: `string`, `int` error[E0609]: no field `r#type` on type `ChildProperties` - --> $DIR/component-fail.rs:60:20 + --> $DIR/component-fail.rs:71:20 | -60 | html! { }; +71 | html! { }; | ^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `r#type` found for struct `ChildPropertiesBuilder` in the current scope - --> $DIR/component-fail.rs:60:20 + --> $DIR/component-fail.rs:71:20 | 4 | #[derive(Clone, Properties, PartialEq)] | ---------- method `r#type` not found for this ... -60 | html! { }; +71 | html! { }; | ^^^^ method not found in `ChildPropertiesBuilder` error[E0609]: no field `unknown` on type `ChildProperties` - --> $DIR/component-fail.rs:63:20 + --> $DIR/component-fail.rs:74:20 | -63 | html! { }; +74 | html! { }; | ^^^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `unknown` found for struct `ChildPropertiesBuilder` in the current scope - --> $DIR/component-fail.rs:63:20 + --> $DIR/component-fail.rs:74:20 | 4 | #[derive(Clone, Properties, PartialEq)] | ---------- method `unknown` not found for this ... -63 | html! { }; +74 | html! { }; | ^^^^^^^ method not found in `ChildPropertiesBuilder` error[E0277]: the trait bound `(): IntoPropValue` is not satisfied - --> $DIR/component-fail.rs:66:33 + --> $DIR/component-fail.rs:77:33 | -66 | html! { }; +77 | html! { }; | ^^ the trait `IntoPropValue` is not implemented for `()` error[E0277]: the trait bound `{integer}: IntoPropValue` is not satisfied - --> $DIR/component-fail.rs:67:33 + --> $DIR/component-fail.rs:78:33 | -67 | html! { }; +78 | html! { }; | ^ the trait `IntoPropValue` is not implemented for `{integer}` | = help: the following implementations were found: @@ -243,9 +349,9 @@ error[E0277]: the trait bound `{integer}: IntoPropValue` is not satisfie and 11 others error[E0277]: the trait bound `{integer}: IntoPropValue` is not satisfied - --> $DIR/component-fail.rs:68:34 + --> $DIR/component-fail.rs:79:34 | -68 | html! { }; +79 | html! { }; | ^ the trait `IntoPropValue` is not implemented for `{integer}` | = help: the following implementations were found: @@ -256,92 +362,92 @@ error[E0277]: the trait bound `{integer}: IntoPropValue` is not satisfie and 11 others error[E0308]: mismatched types - --> $DIR/component-fail.rs:69:31 + --> $DIR/component-fail.rs:80:31 | -69 | html! { }; +80 | html! { }; | ^^ expected struct `NodeRef`, found `()` error[E0277]: the trait bound `u32: IntoPropValue` is not satisfied - --> $DIR/component-fail.rs:71:24 + --> $DIR/component-fail.rs:82:24 | -71 | html! { }; +82 | html! { }; | ^^^^ the trait `IntoPropValue` is not implemented for `u32` error[E0599]: no method named `string` found for struct `ChildPropertiesBuilder` in the current scope - --> $DIR/component-fail.rs:72:20 + --> $DIR/component-fail.rs:83:20 | 4 | #[derive(Clone, Properties, PartialEq)] | ---------- method `string` not found for this ... -72 | html! { }; +83 | html! { }; | ^^^^^^ method not found in `ChildPropertiesBuilder` error[E0609]: no field `children` on type `ChildProperties` - --> $DIR/component-fail.rs:76:14 + --> $DIR/component-fail.rs:87:14 | -76 | html! { { "Not allowed" } }; +87 | html! { { "Not allowed" } }; | ^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `children` found for struct `ChildPropertiesBuilder` in the current scope - --> $DIR/component-fail.rs:76:14 + --> $DIR/component-fail.rs:87:14 | 4 | #[derive(Clone, Properties, PartialEq)] | ---------- method `children` not found for this ... -76 | html! { { "Not allowed" } }; +87 | html! { { "Not allowed" } }; | ^^^^^ method not found in `ChildPropertiesBuilder` error[E0609]: no field `children` on type `ChildProperties` - --> $DIR/component-fail.rs:80:10 + --> $DIR/component-fail.rs:94:10 | -80 | +94 | | ^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder` in the current scope - --> $DIR/component-fail.rs:85:14 + --> $DIR/component-fail.rs:99:14 | 24 | #[derive(Clone, Properties, PartialEq)] | ---------- method `build` not found for this ... -85 | html! { }; +99 | html! { }; | ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder` error[E0599]: no method named `build` found for struct `ChildContainerPropertiesBuilder` in the current scope - --> $DIR/component-fail.rs:86:14 - | -24 | #[derive(Clone, Properties, PartialEq)] - | ---------- method `build` not found for this + --> $DIR/component-fail.rs:100:14 + | +24 | #[derive(Clone, Properties, PartialEq)] + | ---------- method `build` not found for this ... -86 | html! { }; - | ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder` +100 | html! { }; + | ^^^^^^^^^^^^^^ method not found in `ChildContainerPropertiesBuilder` error[E0277]: the trait bound `VChild: From` is not satisfied - --> $DIR/component-fail.rs:87:31 - | -87 | html! { { "Not allowed" } }; - | ^^^^^^^^^^^^^ the trait `From` is not implemented for `VChild` - | - = note: required because of the requirements on the impl of `Into>` for `yew::virtual_dom::VText` - = note: required by `into` + --> $DIR/component-fail.rs:101:31 + | +101 | html! { { "Not allowed" } }; + | ^^^^^^^^^^^^^ the trait `From` is not implemented for `VChild` + | + = note: required because of the requirements on the impl of `Into>` for `yew::virtual_dom::VText` + = note: required by `into` error[E0277]: the trait bound `VChild: From` is not satisfied - --> $DIR/component-fail.rs:88:29 - | -88 | html! { <> }; - | ^ the trait `From` is not implemented for `VChild` - | - = note: required because of the requirements on the impl of `Into>` for `VNode` - = note: required by `into` + --> $DIR/component-fail.rs:102:29 + | +102 | html! { <> }; + | ^ the trait `From` is not implemented for `VChild` + | + = note: required because of the requirements on the impl of `Into>` for `VNode` + = note: required by `into` error[E0277]: the trait bound `VChild: From` is not satisfied - --> $DIR/component-fail.rs:89:30 - | -89 | html! { }; - | ^^^^^ the trait `From` is not implemented for `VChild` - | - = note: required because of the requirements on the impl of `Into>` for `VNode` - = note: required by `into` + --> $DIR/component-fail.rs:103:30 + | +103 | html! { }; + | ^^^^^ the trait `From` is not implemented for `VChild` + | + = note: required because of the requirements on the impl of `Into>` for `VNode` + = note: required by `into` diff --git a/packages/yew-macro/tests/html_macro/component-pass.rs b/packages/yew-macro/tests/html_macro/component-pass.rs index d585cba04..b07a54394 100644 --- a/packages/yew-macro/tests/html_macro/component-pass.rs +++ b/packages/yew-macro/tests/html_macro/component-pass.rs @@ -1,6 +1,8 @@ #![no_implicit_prelude] -#[derive(::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq)] +#[derive( + ::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq, +)] pub struct ContainerProperties { pub int: i32, #[prop_or_default] @@ -41,13 +43,19 @@ impl ::std::convert::From<::yew::virtual_dom::VChild> for ChildrenVari impl ::std::convert::Into<::yew::virtual_dom::VNode> for ChildrenVariants { fn into(self) -> ::yew::virtual_dom::VNode { match self { - Self::Child(comp) => ::yew::virtual_dom::VNode::VComp(::std::convert::Into::<::yew::virtual_dom::VComp>::into(comp)), - Self::AltChild(comp) => ::yew::virtual_dom::VNode::VComp(::std::convert::Into::<::yew::virtual_dom::VComp>::into(comp)), + Self::Child(comp) => ::yew::virtual_dom::VNode::VComp(::std::convert::Into::< + ::yew::virtual_dom::VComp, + >::into(comp)), + Self::AltChild(comp) => ::yew::virtual_dom::VNode::VComp(::std::convert::Into::< + ::yew::virtual_dom::VComp, + >::into(comp)), } } } -#[derive(::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq)] +#[derive( + ::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq, +)] pub struct ChildProperties { #[prop_or_default] pub string: ::std::string::String, @@ -86,7 +94,9 @@ impl ::yew::Component for AltChild { } } -#[derive(::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq)] +#[derive( + ::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq, +)] pub struct ChildContainerProperties { pub int: i32, #[prop_or_default] @@ -125,9 +135,12 @@ fn compile_pass() { let node_ref = <::yew::NodeRef as ::std::default::Default>::default(); ::yew::html! { <> - - - ::Properties as ::std::default::Default>::default() ref={node_ref} /> + + + + + + ::Properties as ::std::default::Default>::default() /> }; @@ -182,15 +195,25 @@ fn compile_pass() { }; let props = <::Properties as ::std::default::Default>::default(); + let child_props = + <::Properties as ::std::default::Default>::default(); ::yew::html! { <> - +
{ "hello world" }
+ +
{ "hello world" }
+
+ + + + + diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index c413809df..ccdbd6be6 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -236,9 +236,9 @@ pub use yew_macro::html_nested; /// let props = yew::props!(Model::Properties { id: 2, name: Cow::from("Lemmy") }); /// # assert_eq!(props.id, 2); /// -/// // Use the `with props` syntax to create a component with the props. +/// // Use the Rust-like struct update syntax to create a component with the props. /// html! { -/// +/// /// } /// # } /// ``` diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 8e5851e21..2a3102266 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -296,7 +296,7 @@ mod tests { }; html! { - + }; } @@ -316,8 +316,8 @@ mod tests { check_key(html! { }); check_key(html! { }); check_key(html! { }); - check_key(html! { }); - check_key(html! { }); + check_key(html! { }); + check_key(html! { }); } #[test] @@ -337,8 +337,8 @@ mod tests { check_node_ref(html! { }); check_node_ref(html! { }); check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); + check_node_ref(html! { }); + check_node_ref(html! { }); } #[test] diff --git a/website/docs/concepts/html/components.md b/website/docs/concepts/html/components.md index 38f0b3307..9cda522d1 100644 --- a/website/docs/concepts/html/components.md +++ b/website/docs/concepts/html/components.md @@ -27,7 +27,7 @@ impl Component for MyComponent { } } -#[derive(PartialEq, Properties)] +#[derive(Clone, PartialEq, Properties)] struct Props { prop1: String, prop2: String, @@ -71,7 +71,10 @@ html!{ // With the whole set of props provided at once - + + + // With Properties from a variable and specific values overridden + }; ``` @@ -116,7 +119,10 @@ html! { }; ``` -When using the `with props` syntax, the children passed in the `html!` macro overwrite the ones already present in the props. +The `html!` macro allows you to pass a base expression with the `..props` syntax instead of specifying each property individually, +similar to Rust's [Functional Update Syntax](https://doc.rust-lang.org/stable/reference/expressions/struct-expr.html#functional-update-syntax). +This base expression must occur after any individual props are passed. +When passing a base props expression with a `children` field, the children passed in the `html!` macro overwrite the ones already present in the props. ```rust use yew::{Children, Component, Context, html, Html, props, Properties}; @@ -152,7 +158,7 @@ let props = yew::props!(Container::Properties { }); html! { - + // props.children will be overwritten with this { "I am a child, as you can see" } diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/components.md b/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/components.md index ea9bf84c2..797515f99 100644 --- a/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/components.md +++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/concepts/html/components.md @@ -16,7 +16,10 @@ html!{ // With the whole set of props provided at once - + + + // With Properties from a variable and specific values overridden + } ``` diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/concepts/html/components.md b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/concepts/html/components.md index f4b7f502e..36e0cd840 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/concepts/html/components.md +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/concepts/html/components.md @@ -18,7 +18,7 @@ html!{ // 同时提供全套的 props - + } ``` diff --git a/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/concepts/html/components.md b/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/concepts/html/components.md index 5c994f4c2..d09332262 100644 --- a/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/concepts/html/components.md +++ b/website/i18n/zh-TW/docusaurus-plugin-content-docs/current/concepts/html/components.md @@ -18,7 +18,7 @@ html!{ // 一次提供很多屬性 - + } ```