diff --git a/Cargo.toml b/Cargo.toml index 9eb2adaad..a9c153287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ slab = "0.4" stdweb = "^0.4.16" toml = { version = "0.4", optional = true } yew-macro = { version = "0.8.0", path = "crates/macro" } +yew-props-derive = { path = "crates/props-derive" } [target.'cfg(all(target_arch = "wasm32", not(cargo_web)))'.dependencies] wasm-bindgen = "0.2" @@ -54,6 +55,7 @@ cbor = ["serde_cbor"] [workspace] members = [ "crates/macro", + "crates/props-derive", "examples/counter", "examples/crm", "examples/custom_components", diff --git a/README.md b/README.md index 720aa7f81..402875813 100644 --- a/README.md +++ b/README.md @@ -204,11 +204,27 @@ Components live in an Angular-like scopes with **parent-to-child** *(properties) Properties are also pure Rust types with strict type-checking during the compilation. ```rust +// my_button.rs + +#[derive(Properties, PartialEq)] +pub struct Properties { + pub hidden: bool, + #[props(required)] + pub color: Color, + #[props(required)] + pub onclick: Callback<()>, +} + +``` + +```rust +// confirm_dialog.rs + html! { - +
+
} ``` diff --git a/ci/run_tests.sh b/ci/run_tests.sh index 99f85710e..131b5d648 100755 --- a/ci/run_tests.sh +++ b/ci/run_tests.sh @@ -29,6 +29,9 @@ cargo test --target=wasm32-unknown-unknown echo "Testing macro..." cargo test --test macro_test +echo "Testing props derive macro..." +(cd crates/props-derive && cargo test) + check_example() { echo "Checking example [$2]" pushd $2 > /dev/null diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index c4103bfc2..a04bc7680 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -25,5 +25,5 @@ proc-macro2 = "0.4" quote = "0.6" syn = { version = "^0.15.34", features = ["full"] } -[dev-dependencies] -yew = { path = "../.." } +[build-dependencies] +autocfg = "0.1.3" diff --git a/crates/macro/build.rs b/crates/macro/build.rs new file mode 100644 index 000000000..a5de3dc6e --- /dev/null +++ b/crates/macro/build.rs @@ -0,0 +1,7 @@ +extern crate autocfg; + +pub fn main() { + if autocfg::new().probe_rustc_version(1, 36) { + println!("cargo:rustc-cfg=has_maybe_uninit"); + } +} diff --git a/crates/macro/src/html_tree/html_component.rs b/crates/macro/src/html_tree/html_component.rs index 23a366a4e..fe22abe06 100644 --- a/crates/macro/src/html_tree/html_component.rs +++ b/crates/macro/src/html_tree/html_component.rs @@ -48,35 +48,72 @@ impl Parse for HtmlComponent { impl ToTokens for HtmlComponent { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let HtmlComponentInner { ty, props } = &self.0; - let vcomp_props = Ident::new("__yew_vcomp_props", Span::call_site()); let vcomp_scope = Ident::new("__yew_vcomp_scope", Span::call_site()); - let override_props = props.iter().map(|props| match props { - Props::List(ListProps(vec_props)) => { - let check_props = vec_props.iter().map(|HtmlProp { label, .. }| { - quote_spanned! { label.span()=> #vcomp_props.#label; } - }); - let set_props = vec_props.iter().map(|HtmlProp { label, value }| { - quote_spanned! { value.span()=> - #vcomp_props.#label = <::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value); + let validate_props = if let Some(Props::List(ListProps(vec_props))) = props { + let prop_ref = Ident::new("__yew_prop_ref", Span::call_site()); + let check_props = vec_props.iter().map(|HtmlProp { label, .. }| { + quote! { #prop_ref.#label; } + }); + + // This is a hack to avoid allocating memory but still have a reference to a props + // struct so that attributes can be checked against it + + #[cfg(has_maybe_uninit)] + let unallocated_prop_ref = quote! { + let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { ::std::mem::MaybeUninit::uninit().assume_init() }; + }; + + #[cfg(not(has_maybe_uninit))] + let unallocated_prop_ref = quote! { + let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { ::std::mem::uninitialized() }; + }; + + quote! { + #unallocated_prop_ref + #(#check_props)* + } + } else { + quote! {} + }; + + let init_props = if let Some(props) = props { + match props { + Props::List(ListProps(vec_props)) => { + let set_props = vec_props.iter().map(|HtmlProp { label, value }| { + quote_spanned! { value.span()=> + .#label(<::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value)) + } + }); + + quote! { + <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder() + #(#set_props)* + .build() } - }); - - quote! { - #(#check_props#set_props)* } + Props::With(WithProps(props)) => quote! { #props }, } - Props::With(WithProps(props)) => { - quote_spanned! { props.span()=> #vcomp_props = #props; } + } else { + quote! { + <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder().build() + } + }; + + let validate_comp = quote_spanned! { ty.span()=> + struct __yew_validate_comp where #ty: ::yew::html::Component; + }; + + tokens.extend(quote! {{ + // Validation nevers executes at runtime + if false { + #validate_comp + #validate_props } - }); - tokens.extend(quote_spanned! { ty.span()=> { let #vcomp_scope: ::yew::virtual_dom::vcomp::ScopeHolder<_> = ::std::default::Default::default(); - let mut #vcomp_props: <#ty as ::yew::html::Component>::Properties = ::std::default::Default::default(); - #(#override_props)* ::yew::virtual_dom::VNode::VComp( - ::yew::virtual_dom::VComp::new::<#ty>(#vcomp_props, #vcomp_scope) + ::yew::virtual_dom::VComp::new::<#ty>(#init_props, #vcomp_scope) ) }}); } @@ -202,6 +239,14 @@ impl Parse for ListProps { } } + // alphabetize + props.sort_by(|a, b| { + a.label + .to_string() + .partial_cmp(&b.label.to_string()) + .unwrap() + }); + Ok(ListProps(props)) } } diff --git a/crates/props-derive/Cargo.toml b/crates/props-derive/Cargo.toml new file mode 100644 index 000000000..641a5298a --- /dev/null +++ b/crates/props-derive/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "yew-props-derive" +version = "0.7.0" +edition = "2018" +autotests = false + +[lib] +proc-macro = true + +[[test]] +name = "tests" +path = "tests/cases.rs" + +[dev-dependencies] +trybuild = "1.0" +yew = { path = "../.." } + +[dependencies] +proc-macro2 = "0.4" +syn = "0.15" +quote = "0.6" diff --git a/crates/props-derive/src/lib.rs b/crates/props-derive/src/lib.rs new file mode 100644 index 000000000..316d9128d --- /dev/null +++ b/crates/props-derive/src/lib.rs @@ -0,0 +1,344 @@ +#![recursion_limit = "128"] +extern crate proc_macro; +extern crate quote; +extern crate syn; + +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::{quote, ToTokens}; +use std::iter; +use syn::parse_macro_input; +use syn::punctuated; +use syn::spanned::Spanned; +use syn::{ + DeriveInput, Error, GenericParam, Generics, Meta, MetaList, NestedMeta, Type, TypeParam, + Visibility, WhereClause, +}; + +struct PropField { + ty: Type, + name: Ident, + wrapped_name: Option, +} + +#[proc_macro_derive(Properties, attributes(props))] +pub fn derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let props_name = input.ident; + let vis = input.vis; + + let generics = input.generics; + let generic_params = &generics.params; + let generic_where = &generics.where_clause; + let generic_idents = { + let generic_idents = generics.params.iter().filter_map(|param| match param { + GenericParam::Type(TypeParam { ident, .. }) => Some(quote! { #ident }), + _ => unimplemented!(), + }); + + quote! {#(#generic_idents),*} + }; + + let named_fields = match input.data { + syn::Data::Struct(data) => match data.fields { + syn::Fields::Named(fields) => fields, + _ => unimplemented!(), + }, + _ => unimplemented!(), + }; + + let prop_fields: Vec = { + let res: Result, Error> = named_fields + .named + .into_iter() + .map(|field| { + Ok(PropField { + wrapped_name: required_wrapper(&field)?, + ty: field.ty, + name: field.ident.unwrap(), + }) + }) + .collect(); + + match res { + Err(err) => return TokenStream::from(err.to_compile_error()), + Ok(mut prop_fields) => { + // Alphabetize + prop_fields.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); + prop_fields + } + } + }; + + // Build Idents + let step_name = Ident::new(&format!("{}Step", props_name), Span::call_site()); + let wrapped_name = Ident::new(&format!("Wrapped{}", props_name), Span::call_site()); + let builder_name = Ident::new(&format!("{}Builder", props_name), Span::call_site()); + let mut step_names: Vec = prop_fields + .iter() + .filter(|field| field.wrapped_name.is_some()) + .map(|field| { + Ident::new( + &format!("{}_{}_is_required", props_name, field.name), + Span::call_site(), + ) + }) + .collect(); + + step_names.push(Ident::new( + &format!("{}BuildStep", props_name), + Span::call_site(), + )); + + let start_step_name = &step_names[0]; + let build_step_name = &step_names[step_names.len() - 1]; + let all_step_names = &step_names; + + let step_methods = step_methods( + &vis, + &generics, + &generic_idents, + &generic_where, + &builder_name, + &step_names, + &prop_fields, + ); + let wrapped_fields = wrapped_fields(prop_fields.iter()); + let wrapped_default_setters = wrapped_default_setters(prop_fields.iter()); + let prop_field_setters = prop_field_setters(prop_fields.iter()); + let step_name_impls = step_name_impls(&step_name, step_names.iter()); + let vis_repeat = iter::repeat(&vis); + + let expanded = quote! { + #( + #[doc(hidden)] + #vis_repeat struct #all_step_names; + )* + + #[doc(hidden)] + #vis trait #step_name {} + #(#step_name_impls)* + + struct #wrapped_name#generics { + #(#wrapped_fields)* + } + + impl#generics ::std::default::Default for #wrapped_name<#generic_idents> #generic_where { + fn default() -> Self { + #wrapped_name::<#generic_idents> { + #(#wrapped_default_setters)* + } + } + } + + #[doc(hidden)] + #vis struct #builder_name #generic_where { + wrapped: ::std::boxed::Box<#wrapped_name<#generic_idents>>, + _marker: ::std::marker::PhantomData

, + } + + impl #generics ::yew::html::Properties for #props_name<#generic_idents> #generic_where { + type Builder = #builder_name<#start_step_name, #generic_idents>; + + fn builder() -> Self::Builder { + #builder_name { + wrapped: ::std::boxed::Box::new(::std::default::Default::default()), + _marker: ::std::marker::PhantomData, + } + } + } + + #(#step_methods)* + + impl #generics #builder_name<#build_step_name, #generic_idents> #generic_where { + #[doc(hidden)] + #vis fn build(self) -> #props_name<#generic_idents> { + #props_name::<#generic_idents> { + #(#prop_field_setters)* + } + } + } + }; + + TokenStream::from(expanded) +} + +fn wrapped_fields<'a>( + prop_fields: impl Iterator, +) -> impl Iterator { + prop_fields.map(|pf| { + let PropField { name, ty, .. } = &pf; + if let Some(wrapped_name) = &pf.wrapped_name { + quote! { + #wrapped_name: ::std::option::Option<#ty>, + } + } else { + quote! { + #name: #ty, + } + } + }) +} + +fn wrapped_default_setters<'a>( + prop_fields: impl Iterator, +) -> impl Iterator { + prop_fields.map(|pf| { + if let Some(wrapped_name) = &pf.wrapped_name { + quote! { + #wrapped_name: ::std::default::Default::default(), + } + } else { + let name = &pf.name; + quote! { + #name: ::std::default::Default::default(), + } + } + }) +} + +fn prop_field_setters<'a>( + prop_fields: impl Iterator, +) -> impl Iterator { + prop_fields.map(|pf| { + let name = &pf.name; + if let Some(wrapped_name) = &pf.wrapped_name { + quote! { + #name: self.wrapped.#wrapped_name.unwrap(), + } + } else { + quote! { + #name: self.wrapped.#name, + } + } + }) +} + +fn find_props_meta_list(field: &syn::Field) -> Option { + let meta_list = field + .attrs + .iter() + .find_map(|attr| match attr.parse_meta().ok()? { + Meta::List(meta_list) => Some(meta_list), + _ => None, + })?; + + if meta_list.ident == "props" { + Some(meta_list) + } else { + None + } +} + +fn required_wrapper(named_field: &syn::Field) -> Result, Error> { + let meta_list = if let Some(meta_list) = find_props_meta_list(named_field) { + meta_list + } else { + return Ok(None); + }; + + let expected_required = syn::Error::new(meta_list.span(), "expected `props(required)`"); + let first_nested = if let Some(first_nested) = meta_list.nested.first() { + first_nested + } else { + return Err(expected_required); + }; + + let word_ident = match first_nested { + punctuated::Pair::End(NestedMeta::Meta(Meta::Word(ident))) => ident, + _ => return Err(expected_required), + }; + + if word_ident != "required" { + return Err(expected_required); + } + + if let Some(ident) = &named_field.ident { + Ok(Some(Ident::new( + &format!("{}_wrapper", ident), + Span::call_site(), + ))) + } else { + unreachable!() + } +} + +fn step_name_impls<'a>( + step_trait_name: &'a Ident, + step_names: impl Iterator, +) -> impl Iterator { + step_names.map(move |name| { + let trait_name = step_trait_name; + quote! { + impl #trait_name for #name {} + } + }) +} + +fn step_methods<'a>( + vis: &'a Visibility, + generics: &'a Generics, + generic_idents: &'a proc_macro2::TokenStream, + generic_where: &'a Option, + builder_name: &'a Ident, + step_names: &'a [Ident], + prop_fields: &'a [PropField], +) -> proc_macro2::TokenStream { + let mut prop_fields_index = 0; + let mut token_stream = proc_macro2::TokenStream::new(); + + for (step, step_name) in step_names.iter().enumerate() { + let mut optional_fields = Vec::new(); + let mut required_field = None; + + if prop_fields_index >= prop_fields.len() { + break; + } + + while let Some(pf) = prop_fields.get(prop_fields_index) { + prop_fields_index += 1; + if pf.wrapped_name.is_some() { + required_field = Some(pf); + break; + } else { + optional_fields.push((&pf.name, &pf.ty)); + } + } + + let optional_prop_fn = optional_fields.into_iter().map(|(prop_name, prop_type)| { + quote! { + #[doc(hidden)] + #vis fn #prop_name(mut self, #prop_name: #prop_type) -> #builder_name<#step_name, #generic_idents> { + self.wrapped.#prop_name = #prop_name; + self + } + } + }); + + let required_prop_fn = required_field.iter().map(|p| { + let prop_name = &p.name; + let prop_type = &p.ty; + let wrapped_name = p.wrapped_name.as_ref().unwrap(); + let next_step_name = &step_names[step + 1]; + + quote! { + #[doc(hidden)] + #vis fn #prop_name(mut self, #prop_name: #prop_type) -> #builder_name<#next_step_name, #generic_idents> { + self.wrapped.#wrapped_name = ::std::option::Option::Some(#prop_name); + #builder_name { + wrapped: self.wrapped, + _marker: ::std::marker::PhantomData, + } + } + } + }); + + token_stream.extend(quote! { + impl #generics #builder_name<#step_name, #generic_idents> #generic_where { + #(#optional_prop_fn)* + #(#required_prop_fn)* + } + }); + } + token_stream +} diff --git a/crates/props-derive/tests/cases.rs b/crates/props-derive/tests/cases.rs new file mode 100644 index 000000000..0659ef9b2 --- /dev/null +++ b/crates/props-derive/tests/cases.rs @@ -0,0 +1,6 @@ +#[test] +fn tests() { + let t = trybuild::TestCases::new(); + t.pass("tests/pass.rs"); + t.compile_fail("tests/fail.rs"); +} diff --git a/crates/props-derive/tests/fail.rs b/crates/props-derive/tests/fail.rs new file mode 100644 index 000000000..ec6bf1a46 --- /dev/null +++ b/crates/props-derive/tests/fail.rs @@ -0,0 +1,53 @@ +#![recursion_limit = "128"] + +use yew::html::Properties; +use yew_props_derive::Properties; + +mod t1 { + use super::*; + struct Value; + #[derive(Properties)] + pub struct Props { + // ERROR: optional params must implement default + value: Value, + } +} + +mod t2 { + use super::*; + #[derive(Properties)] + pub struct Props { + // ERROR: optional is not a tag + #[props(optional)] + value: String, + } +} + +mod t3 { + use super::*; + #[derive(Properties)] + pub struct Props { + #[props(required)] + value: String, + } + + fn required_props_should_be_set() { + Props::builder().build(); + } +} + +mod t4 { + use super::*; + #[derive(Properties)] + pub struct Props { + b: i32, + #[props(required)] + a: i32, + } + + fn enforce_ordering() { + Props::builder().b(1).a(2).build(); + } +} + +fn main() {} diff --git a/crates/props-derive/tests/fail.stderr b/crates/props-derive/tests/fail.stderr new file mode 100644 index 000000000..00bb9eb4d --- /dev/null +++ b/crates/props-derive/tests/fail.stderr @@ -0,0 +1,34 @@ +error: expected `props(required)` + --> $DIR/fail.rs:21:11 + | +21 | #[props(optional)] + | ^^^^^ + +error[E0277]: the trait bound `t1::Value: std::default::Default` is not satisfied + --> $DIR/fail.rs:9:14 + | +9 | #[derive(Properties)] + | ^^^^^^^^^^ the trait `std::default::Default` is not implemented for `t1::Value` + | + = note: required by `std::default::Default::default` + +error[E0599]: no method named `build` found for type `t3::PropsBuilder` in the current scope + --> $DIR/fail.rs:35:26 + | +28 | #[derive(Properties)] + | - method `build` not found for this +... +35 | Props::builder().build(); + | ^^^^^ + +error[E0599]: no method named `b` found for type `t4::PropsBuilder` in the current scope + --> $DIR/fail.rs:49:26 + | +41 | #[derive(Properties)] + | - method `b` not found for this +... +49 | Props::builder().b(1).a(2).build(); + | ^ help: there is a method with a similar name: `a` + +Some errors have detailed explanations: E0277, E0599. +For more information about an error, try `rustc --explain E0277`. diff --git a/crates/props-derive/tests/pass.rs b/crates/props-derive/tests/pass.rs new file mode 100644 index 000000000..fc1013294 --- /dev/null +++ b/crates/props-derive/tests/pass.rs @@ -0,0 +1,68 @@ +#![recursion_limit = "128"] + +use yew::html::Properties; +use yew_props_derive::Properties; + +mod t1 { + use super::*; + + #[derive(Properties)] + pub struct Props { + value: T, + } + + fn optional_prop_generics_should_work() { + Props::::builder().build(); + Props::::builder().value(true).build(); + } +} + +mod t2 { + use super::*; + + struct Value; + #[derive(Properties)] + pub struct Props { + #[props(required)] + value: T, + } + + fn required_prop_generics_should_work() { + Props::::builder().value(Value).build(); + } +} + +mod t3 { + use super::*; + + #[derive(Properties)] + pub struct Props { + #[props(required)] + b: i32, + a: i32, + } + + fn order_is_alphabetized() { + Props::builder().b(1).build(); + Props::builder().a(1).b(2).build(); + } +} + +mod t4 { + use super::*; + + #[derive(Properties)] + pub struct Props + where + T: Default, + { + value: T, + } + + fn optional_prop_generics_should_work() { + Props::::builder().build(); + Props::::builder().value(true).build(); + } +} + +fn main() {} diff --git a/examples/custom_components/src/barrier.rs b/examples/custom_components/src/barrier.rs index d24bf68e6..dcc606427 100644 --- a/examples/custom_components/src/barrier.rs +++ b/examples/custom_components/src/barrier.rs @@ -1,32 +1,23 @@ use crate::button::Button; -use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::prelude::*; pub struct Barrier { limit: u32, counter: u32, - onsignal: Option>, + onsignal: Callback<()>, } pub enum Msg { ChildClicked, } -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Properties)] pub struct Props { pub limit: u32, - pub onsignal: Option>, + #[props(required)] + pub onsignal: Callback<()>, } -impl Default for Props { - fn default() -> Self { - Props { - limit: 0, - onsignal: None, - } - } -} - - impl Component for Barrier { type Message = Msg; type Properties = Props; @@ -44,10 +35,8 @@ impl Component for Barrier { Msg::ChildClicked => { self.counter += 1; if self.counter >= self.limit { - if let Some(ref mut callback) = self.onsignal { - callback.emit(()); - self.counter = 0; - } + self.onsignal.emit(()); + self.counter = 0; } } } diff --git a/examples/custom_components/src/button.rs b/examples/custom_components/src/button.rs index e7b93a259..4d5ab7de4 100644 --- a/examples/custom_components/src/button.rs +++ b/examples/custom_components/src/button.rs @@ -1,27 +1,19 @@ -use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::prelude::*; pub struct Button { title: String, - onsignal: Option>, + onsignal: Callback<()>, } pub enum Msg { Clicked, } -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Properties)] pub struct Props { pub title: String, - pub onsignal: Option>, -} - -impl Default for Props { - fn default() -> Self { - Props { - title: "Send Signal".into(), - onsignal: None, - } - } + #[props(required)] + pub onsignal: Callback<()>, } impl Component for Button { @@ -38,9 +30,7 @@ impl Component for Button { fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { Msg::Clicked => { - if let Some(ref mut callback) = self.onsignal { - callback.emit(()); - } + self.onsignal.emit(()); } } false diff --git a/examples/custom_components/src/counter.rs b/examples/custom_components/src/counter.rs index 52ec4b122..93cd445ff 100644 --- a/examples/custom_components/src/counter.rs +++ b/examples/custom_components/src/counter.rs @@ -1,4 +1,4 @@ -use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; +use yew::prelude::*; #[derive(PartialEq, Clone)] pub enum Color { @@ -7,31 +7,28 @@ pub enum Color { Blue, } +impl Default for Color { + fn default() -> Self { + Color::Green + } +} + pub struct Counter { value: u32, color: Color, - onclick: Option>, + onclick: Callback, } pub enum Msg { Increase, } -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Properties)] pub struct Props { pub initial: u32, pub color: Color, - pub onclick: Option>, -} - -impl Default for Props { - fn default() -> Self { - Props { - initial: 0, - color: Color::Green, - onclick: None, - } - } + #[props(required)] + pub onclick: Callback, } impl Component for Counter { @@ -50,9 +47,7 @@ impl Component for Counter { match msg { Msg::Increase => { self.value = self.value + 1; - if let Some(ref onclick) = self.onclick { - onclick.emit(self.value); - } + self.onclick.emit(self.value); } } true diff --git a/examples/custom_components/src/lib.rs b/examples/custom_components/src/lib.rs index d1cbdb638..7557d23a4 100644 --- a/examples/custom_components/src/lib.rs +++ b/examples/custom_components/src/lib.rs @@ -1,12 +1,12 @@ #![recursion_limit = "128"] -mod counter; -mod button; mod barrier; +mod button; +mod counter; -use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; -use counter::{Counter, Color}; use barrier::Barrier; +use counter::{Color, Counter}; +use yew::prelude::*; pub struct Model { with_barrier: bool, @@ -19,8 +19,7 @@ pub enum Msg { ChildClicked(u32), } -impl Component for Model -{ +impl Component for Model { type Message = Msg; type Properties = (); @@ -41,17 +40,17 @@ impl Component for Model self.with_barrier = !self.with_barrier; true } - Msg::ChildClicked(_value) => { - false - } + Msg::ChildClicked(_value) => false, } } } impl Renderable for Model { fn view(&self) -> Html { - let counter = |x| html! { - + let counter = |x| { + html! { + + } }; html! {

diff --git a/examples/js_callback/Cargo.toml b/examples/js_callback/Cargo.toml index 5a5137a9a..6abf6189b 100644 --- a/examples/js_callback/Cargo.toml +++ b/examples/js_callback/Cargo.toml @@ -6,4 +6,7 @@ edition = "2018" [dependencies] yew = { path = "../.." } -stdweb = "0.4.7" +stdweb = "^0.4.16" + +[target.'cfg(all(target_arch = "wasm32", not(cargo_web)))'.dependencies] +wasm-bindgen = "0.2" diff --git a/examples/js_callback/src/lib.rs b/examples/js_callback/src/lib.rs index b0c2e5401..8950f80d5 100644 --- a/examples/js_callback/src/lib.rs +++ b/examples/js_callback/src/lib.rs @@ -1,10 +1,8 @@ -#![recursion_limit="128"] +#![recursion_limit = "128"] #![deny(warnings)] -#[macro_use] -extern crate stdweb; - -use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; +use stdweb::{_js_impl, js}; +use yew::prelude::*; pub struct Model { payload: String, @@ -18,16 +16,12 @@ pub enum Msg { AsyncPayload, } -#[derive(Default, PartialEq, Eq, Clone)] -pub struct Props { - payload: String, -} - impl Component for Model { type Message = Msg; - type Properties = Props; + type Properties = (); - fn create(Props { payload }: Self::Properties, link: ComponentLink) -> Self { + fn create(_: Self::Properties, link: ComponentLink) -> Self { + let payload = String::default(); let debugged_payload = format!("{:?}", payload); Self { payload, @@ -39,7 +33,15 @@ impl Component for Model { fn update(&mut self, msg: Self::Message) -> ShouldRender { use Msg::*; match msg { - Payload(payload) => self.change(Self::Properties { payload }), + Payload(payload) => { + if payload != self.payload { + self.debugged_payload = format!("{:?}", payload); + self.payload = payload; + true + } else { + false + } + } AsyncPayload => { get_payload_later(self.link.send_back(Msg::Payload)); false @@ -47,14 +49,8 @@ impl Component for Model { } } - fn change(&mut self, Self::Properties { payload }: Self::Properties) -> ShouldRender { - if payload == self.payload { - false - } else { - self.debugged_payload = format!("{:?}", payload); - self.payload = payload; - true - } + fn change(&mut self, _: Self::Properties) -> ShouldRender { + false } } @@ -73,7 +69,7 @@ impl Renderable for Model { { "Get the payload later!" }

- { nbsp(self.debugged_payload.as_ref()) } + { nbsp(self.debugged_payload.as_str()) }

} diff --git a/src/callback.rs b/src/callback.rs index 05c8978d9..c9cf687ef 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -10,7 +10,6 @@ use std::rc::Rc; /// Callbacks should be used from JS callbacks or `setTimeout` calls. /// /// `Rc` wrapper used to make it clonable. -#[must_use] pub struct Callback(Rc); impl From for Callback { diff --git a/src/components/select.rs b/src/components/select.rs index 4ab7cd6b4..3b1dc6b76 100644 --- a/src/components/select.rs +++ b/src/components/select.rs @@ -17,7 +17,7 @@ use crate::callback::Callback; use crate::html::{ChangeData, Component, ComponentLink, Html, Renderable, ShouldRender}; -use crate::macros::html; +use crate::macros::{html, Properties}; /// `Select` component. pub struct Select { @@ -31,7 +31,7 @@ pub enum Msg { } /// Properties of `Select` component. -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Properties)] pub struct Props { /// Initially selected value. pub selected: Option, @@ -40,18 +40,8 @@ pub struct Props { /// Options are available to choose. pub options: Vec, /// Callback to handle changes. - pub onchange: Option>, -} - -impl Default for Props { - fn default() -> Self { - Props { - selected: None, - disabled: false, - options: Vec::new(), - onchange: None, - } - } + #[props(required)] + pub onchange: Callback, } impl Component for Select @@ -69,11 +59,9 @@ where match msg { Msg::Selected(value) => { if let Some(idx) = value { - if let Some(ref mut callback) = self.props.onchange { - let item = self.props.options.get(idx - 1).cloned(); - if let Some(value) = item { - callback.emit(value); - } + let item = self.props.options.get(idx - 1).cloned(); + if let Some(value) = item { + self.props.onchange.emit(value); } } } diff --git a/src/html.rs b/src/html.rs index f1bc5e0f9..e10706712 100644 --- a/src/html.rs +++ b/src/html.rs @@ -23,10 +23,7 @@ pub trait Component: Sized + 'static { /// Control message type which `update` loop get. type Message: 'static; /// Properties type of component implementation. - /// It sould be serializable because it's sent to dynamicaly created - /// component (layed under `VComp`) and must be restored for a component - /// with unknown type. - type Properties: Clone + Default; + type Properties: Properties; /// Initialization routine which could use a context. fn create(props: Self::Properties, link: ComponentLink) -> Self; /// Called everytime when a messages of `Msg` type received. It also takes a @@ -40,6 +37,30 @@ pub trait Component: Sized + 'static { fn destroy(&mut self) {} // TODO Replace with `Drop` } +/// Trait for building properties for a component +pub trait Properties { + /// Builder that will be used to construct properties + type Builder; + + /// Entrypoint for building properties + fn builder() -> Self::Builder; +} + +/// Builder for when a component has no properties +pub struct EmptyBuilder; + +impl Properties for () { + type Builder = EmptyBuilder; + + fn builder() -> Self::Builder { + EmptyBuilder + } +} +impl EmptyBuilder { + /// Build empty properties + pub fn build(self) {} +} + /// Should be rendered relative to context and component environment. pub trait Renderable { /// Called by rendering loop. diff --git a/src/lib.rs b/src/lib.rs index 16f921df1..c5becb33e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,7 @@ pub use yew_macro::html; /// This module contains macros which implements html! macro and JSX-like templates pub mod macros { pub use crate::html; + pub use yew_props_derive::Properties; } pub mod agent; diff --git a/src/virtual_dom/vcomp.rs b/src/virtual_dom/vcomp.rs index 170a24489..4d17749e3 100644 --- a/src/virtual_dom/vcomp.rs +++ b/src/virtual_dom/vcomp.rs @@ -150,12 +150,12 @@ where } } -impl<'a, COMP, F, IN> Transformer>> for VComp +impl<'a, COMP, F, IN> Transformer> for VComp where COMP: Component + Renderable, F: Fn(IN) -> COMP::Message + 'static, { - fn transform(scope: ScopeHolder, from: F) -> Option> { + fn transform(scope: ScopeHolder, from: F) -> Callback { let callback = move |arg| { let msg = from(arg); if let Some(ref mut sender) = *scope.borrow_mut() { @@ -164,7 +164,7 @@ where panic!("unactivated callback, parent component have to activate it"); } }; - Some(callback.into()) + callback.into() } } diff --git a/tests/macro/html-component-fail-unimplemented.rs b/tests/macro/html-component-fail-unimplemented.rs new file mode 100644 index 000000000..70a28c90f --- /dev/null +++ b/tests/macro/html-component-fail-unimplemented.rs @@ -0,0 +1,9 @@ +#![recursion_limit = "128"] + +use yew::prelude::*; + +fn compile_fail() { + html! { }; +} + +fn main() {} diff --git a/tests/macro/html-component-fail-unimplemented.stderr b/tests/macro/html-component-fail-unimplemented.stderr new file mode 100644 index 000000000..372530db5 --- /dev/null +++ b/tests/macro/html-component-fail-unimplemented.stderr @@ -0,0 +1,9 @@ +error[E0277]: the trait bound `std::string::String: yew::html::Component` is not satisfied + --> $DIR/html-component-fail-unimplemented.rs:6:14 + | +6 | html! { }; + | ^^^^^^ the trait `yew::html::Component` is not implemented for `std::string::String` + | + = help: see issue #48214 + +For more information about this error, try `rustc --explain E0277`. diff --git a/tests/macro/html-component-fail.rs b/tests/macro/html-component-fail.rs index 2bef92832..85c83aed6 100644 --- a/tests/macro/html-component-fail.rs +++ b/tests/macro/html-component-fail.rs @@ -2,9 +2,10 @@ use yew::prelude::*; -#[derive(Clone, Default, PartialEq)] +#[derive(Properties, PartialEq)] pub struct ChildProperties { pub string: String, + #[props(required)] pub int: i32, } @@ -40,14 +41,11 @@ fn compile_fail() { html! { }; html! { }; html! { }; - html! { }; - html! { }; - html! { }; + html! { }; + html! { }; + html! { }; html! { }; -} - -fn additional_fail() { - html! { }; + html! { }; } fn main() {} diff --git a/tests/macro/html-component-fail.stderr b/tests/macro/html-component-fail.stderr index 79d324230..f8bbb65fa 100644 --- a/tests/macro/html-component-fail.stderr +++ b/tests/macro/html-component-fail.stderr @@ -1,127 +1,131 @@ error: expected component tag be of form `< .. />` - --> $DIR/html-component-fail.rs:32:13 + --> $DIR/html-component-fail.rs:33:13 | -32 | html! { }; +33 | html! { }; | ^^^^^^^^^^^^^^^^ error: unexpected end of input, expected identifier - --> $DIR/html-component-fail.rs:33:31 + --> $DIR/html-component-fail.rs:34:31 | -33 | html! { }; +34 | html! { }; | ^ error: unexpected end of input, expected identifier - --> $DIR/html-component-fail.rs:34:34 + --> $DIR/html-component-fail.rs:35:34 | -34 | html! { }; +35 | html! { }; | ^ error: unexpected token - --> $DIR/html-component-fail.rs:35:29 + --> $DIR/html-component-fail.rs:36:29 | -35 | html! { }; +36 | html! { }; | ^^^^^ error: expected component tag be of form `< .. />` - --> $DIR/html-component-fail.rs:36:13 + --> $DIR/html-component-fail.rs:37:13 | -36 | html! { }; +37 | html! { }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unexpected token - --> $DIR/html-component-fail.rs:38:40 + --> $DIR/html-component-fail.rs:39:40 | -38 | html! { }; +39 | html! { }; | ^^ -error: expected identifier - --> $DIR/html-component-fail.rs:39:29 - | -39 | html! { }; - | ^^^^ - error: expected identifier --> $DIR/html-component-fail.rs:40:29 | -40 | html! { }; +40 | html! { }; + | ^^^^ + +error: expected identifier + --> $DIR/html-component-fail.rs:41:29 + | +41 | html! { }; | ^^^^^^^^^^^^^^^^^ error: unexpected end of input, expected expression - --> $DIR/html-component-fail.rs:42:37 + --> $DIR/html-component-fail.rs:43:37 | -42 | html! { }; +43 | html! { }; | ^ error[E0425]: cannot find value `blah` in this scope - --> $DIR/html-component-fail.rs:37:34 + --> $DIR/html-component-fail.rs:38:34 | -37 | html! { }; +38 | html! { }; | ^^^^ not found in this scope error[E0609]: no field `unknown` on type `ChildProperties` - --> $DIR/html-component-fail.rs:41:29 + --> $DIR/html-component-fail.rs:42:29 | -41 | html! { }; +42 | html! { }; | ^^^^^^^ unknown field | = note: available fields are: `string`, `int` -error[E0308]: mismatched types - --> $DIR/html-component-fail.rs:43:36 +error[E0599]: no method named `unknown` found for type `ChildPropertiesBuilder` in the current scope + --> $DIR/html-component-fail.rs:42:29 | -43 | html! { }; - | ^^ expected struct `std::string::String`, found () +5 | #[derive(Properties, PartialEq)] + | - method `unknown` not found for this +... +42 | html! { }; + | ^^^^^^^ + +error[E0308]: mismatched types + --> $DIR/html-component-fail.rs:44:42 + | +44 | html! { }; + | ^^ expected struct `std::string::String`, found () | = note: expected type `std::string::String` found type `()` error[E0308]: mismatched types - --> $DIR/html-component-fail.rs:44:36 + --> $DIR/html-component-fail.rs:45:42 | -44 | html! { }; - | ^ - | | - | expected struct `std::string::String`, found integer - | help: try using a conversion method: `3.to_string()` +45 | html! { }; + | ^ + | | + | expected struct `std::string::String`, found integer + | help: try using a conversion method: `3.to_string()` | = note: expected type `std::string::String` found type `{integer}` error[E0308]: mismatched types - --> $DIR/html-component-fail.rs:45:36 + --> $DIR/html-component-fail.rs:46:42 | -45 | html! { }; - | ^^^ - | | - | expected struct `std::string::String`, found integer - | help: try using a conversion method: `{3}.to_string()` +46 | html! { }; + | ^^^ + | | + | expected struct `std::string::String`, found integer + | help: try using a conversion method: `{3}.to_string()` | = note: expected type `std::string::String` found type `{integer}` error[E0308]: mismatched types - --> $DIR/html-component-fail.rs:46:33 + --> $DIR/html-component-fail.rs:47:33 | -46 | html! { }; +47 | html! { }; | ^^^^ expected i32, found u32 help: you can convert an `u32` to `i32` and panic if the converted value wouldn't fit | -46 | html! { }; +47 | html! { }; | ^^^^^^^^^^^^^^^^^^^^^^^^ -error[E0277]: the trait bound `std::string::String: yew::html::Component` is not satisfied - --> $DIR/html-component-fail.rs:50:14 +error[E0599]: no method named `string` found for type `ChildPropertiesBuilder` in the current scope + --> $DIR/html-component-fail.rs:48:29 | -50 | html! { }; - | ^^^^^^ the trait `yew::html::Component` is not implemented for `std::string::String` +5 | #[derive(Properties, PartialEq)] + | - method `string` not found for this +... +48 | html! { }; + | ^^^^^^ -error[E0277]: the trait bound `std::string::String: yew::html::Renderable` is not satisfied - --> $DIR/html-component-fail.rs:50:14 - | -50 | html! { }; - | ^^^^^^ the trait `yew::html::Renderable` is not implemented for `std::string::String` - | - = note: required by `yew::virtual_dom::vcomp::VComp::::new` - -Some errors have detailed explanations: E0277, E0308, E0425, E0609. -For more information about an error, try `rustc --explain E0277`. +Some errors have detailed explanations: E0308, E0425, E0599, E0609. +For more information about an error, try `rustc --explain E0308`. diff --git a/tests/macro/html-component-pass.rs b/tests/macro/html-component-pass.rs index 8a28528ff..9977f2ff5 100644 --- a/tests/macro/html-component-pass.rs +++ b/tests/macro/html-component-pass.rs @@ -3,9 +3,10 @@ #[macro_use] mod helpers; -#[derive(Clone, Default, PartialEq)] +#[derive(Properties, Default, PartialEq)] pub struct ChildProperties { pub string: String, + #[props(required)] pub int: i32, pub vec: Vec, } @@ -35,19 +36,19 @@ mod scoped { } pass_helper! { - html! { }; + html! { }; // backwards compat - html! { }; + html! { }; html! { <> - - + + // backwards compat - - + + }; @@ -64,10 +65,10 @@ pass_helper! { html! { <> - + - + // backwards compat @@ -77,7 +78,7 @@ pass_helper! { let name_expr = "child"; html! { - + }; } diff --git a/tests/macro/test_component.rs b/tests/macro/test_component.rs index d3b792269..13af06f06 100644 --- a/tests/macro/test_component.rs +++ b/tests/macro/test_component.rs @@ -1,6 +1,6 @@ use yew::prelude::*; -#[derive(Clone, Default, PartialEq)] +#[derive(Properties, PartialEq)] pub struct TestProperties { pub string: String, pub int: i32, diff --git a/tests/vcomp_test.rs b/tests/vcomp_test.rs index ebed55c25..d82fdd68a 100644 --- a/tests/vcomp_test.rs +++ b/tests/vcomp_test.rs @@ -1,5 +1,6 @@ #[cfg(feature = "wasm-bindgen-test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; +use yew::macros::Properties; use yew::virtual_dom::VNode; use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; @@ -8,21 +9,12 @@ wasm_bindgen_test_configure!(run_in_browser); struct Comp; -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Properties)] struct Props { field_1: u32, field_2: u32, } -impl Default for Props { - fn default() -> Self { - Props { - field_1: 0, - field_2: 0, - } - } -} - impl Component for Comp { type Message = (); type Properties = Props;