Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "struct update" syntax to pass props to component (..props instead of with props) #2024

Merged
merged 24 commits into from
Sep 6, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9759c39
Reword to use double-dot syntax instead of "with"
Xavientois Aug 29, 2021
ca7479b
Implement double-dot syntax for props in components
Xavientois Aug 30, 2021
2b53e00
Update documentation with new syntax
Xavientois Aug 30, 2021
3bc1def
Update forgotten doc
Xavientois Aug 30, 2021
102ccaf
Merge branch 'master' into add-double-dot-prop-syntax
Xavientois Aug 30, 2021
afc22d0
Add descriptive comments
Xavientois Aug 30, 2021
de0c69a
Merge branch 'add-double-dot-prop-syntax' of github.com:Xavientois/ye…
Xavientois Aug 30, 2021
db2598f
Check props and base expression
Xavientois Aug 30, 2021
503ef88
Make compatible with 1.49.0 by removing then
Xavientois Aug 30, 2021
aa94eff
Fix website tests
Xavientois Aug 30, 2021
04c90de
Update error output
Xavientois Aug 30, 2021
e2ef587
Implicitly convert string literals to String if they are listed as props
Xavientois Aug 30, 2021
6f9ea0f
Remove unused keyword
Xavientois Aug 30, 2021
920daf3
Rename function for checking if string literal
Xavientois Aug 30, 2021
dc43274
Fix weird formatting
Xavientois Aug 30, 2021
b7a312b
Update code based on review
Xavientois Aug 31, 2021
e78cb2a
Update website/docs/concepts/html/components.md
Xavientois Aug 31, 2021
0922b19
Base expression span includes dot2 now
Xavientois Sep 1, 2021
aba0cdc
Improve specificity of error message
Xavientois Sep 1, 2021
7ab9719
Chain together error messages
Xavientois Sep 1, 2021
e79ea1d
Add an example failure case to illustrate combined error message
Xavientois Sep 1, 2021
b75e16a
Update based on review comments
Xavientois Sep 4, 2021
d68826e
Merge branch 'master' into add-double-dot-prop-syntax
Xavientois Sep 5, 2021
79599d9
Fix missing clones
Xavientois Sep 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/yew-macro/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,11 @@ impl Parse for HtmlComponent {
input.parse::<HtmlComponentClose>()?;

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",
));
}
}

Expand Down
195 changes: 82 additions & 113 deletions packages/yew-macro/src/props/component.rs
Original file line number Diff line number Diff line change
@@ -1,99 +1,31 @@
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);
}
Xavientois marked this conversation as resolved.
Show resolved Hide resolved

pub struct WithProps {
pub special: SpecialProps,
pub with: kw::with,
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::<TokenTree>().ok();
}

false
}
}
impl Parse for WithProps {
fn parse(input: ParseStream) -> syn::Result<Self> {
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 <props>` definitions for this component (note: you can only define `with <props>` once)"
));
}
let with = input.parse::<kw::with>()?;
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::<Prop>()?;

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,
})
}
}

pub enum ComponentProps {
List(Props),
With(Box<WithProps>),
pub struct ComponentProps {
props: Props,
base_expr: Option<Expr>,
}
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 {
Expand All @@ -103,20 +35,18 @@ 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; }
Xavientois marked this conversation as resolved.
Show resolved Hide resolved
})
.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)]
Expand All @@ -135,9 +65,9 @@ impl ComponentProps {
children_renderer: Option<CR>,
) -> 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)
}
Expand All @@ -156,17 +86,30 @@ 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_implicitly_converted(value) {
quote_spanned! {value.span()=>
#ident.#label = #value.into();
Xavientois marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
Xavientois marked this conversation as resolved.
Show resolved Hide resolved
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
}
Expand All @@ -181,12 +124,31 @@ impl ComponentProps {
}
}
}

fn is_implicitly_converted(expr: &Expr) -> bool {
matches!(
expr,
Expr::Lit(ExprLit {
lit: Lit::Str(_),
..
})
)
}

impl Parse for ComponentProps {
fn parse(input: ParseStream) -> syn::Result<Self> {
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 {
let _dots: Dot2 = input.parse()?;
Some(input.parse()?)
};

if input.is_empty() {
Ok(Self { props, base_expr })
} else {
input.parse::<Props>().and_then(Self::try_from)
Err(input.error("base props expression must appear last in list of props"))
}
}
}
Expand All @@ -195,18 +157,25 @@ impl TryFrom<Props> for ComponentProps {
type Error = syn::Error;

fn try_from(props: Props) -> Result<Self, Self::Error> {
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, syn::Error> {
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)
}
2 changes: 2 additions & 0 deletions packages/yew-macro/src/props/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ pub use component::*;
pub use element::*;
pub use prop::*;
pub use prop_macro::PropsMacroInput;

const CHILDREN_LABEL: &str = "children";
Loading