Add support for required props

This commit is contained in:
Justin Starry 2019-07-08 23:12:56 -04:00
parent df47f20daa
commit 3fed651b42
30 changed files with 818 additions and 225 deletions

View File

@ -32,6 +32,7 @@ slab = "0.4"
stdweb = "^0.4.16" stdweb = "^0.4.16"
toml = { version = "0.4", optional = true } toml = { version = "0.4", optional = true }
yew-macro = { version = "0.8.0", path = "crates/macro" } 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] [target.'cfg(all(target_arch = "wasm32", not(cargo_web)))'.dependencies]
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
@ -54,6 +55,7 @@ cbor = ["serde_cbor"]
[workspace] [workspace]
members = [ members = [
"crates/macro", "crates/macro",
"crates/props-derive",
"examples/counter", "examples/counter",
"examples/crm", "examples/crm",
"examples/custom_components", "examples/custom_components",

View File

@ -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. Properties are also pure Rust types with strict type-checking during the compilation.
```rust ```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! { html! {
<nav class="menu"> <div class="confirm-dialog">
<MyButton color=Color::Red /> <MyButton onclick=|_| DialogMsg::Cancel color=Color::Red hidden=false />
<MyButton onclick=|_| ParentMsg::DoIt /> <MyButton onclick=|_| DialogMsg::Submit color=Color::Blue />
</nav> </div>
} }
``` ```

View File

@ -29,6 +29,9 @@ cargo test --target=wasm32-unknown-unknown
echo "Testing macro..." echo "Testing macro..."
cargo test --test macro_test cargo test --test macro_test
echo "Testing props derive macro..."
(cd crates/props-derive && cargo test)
check_example() { check_example() {
echo "Checking example [$2]" echo "Checking example [$2]"
pushd $2 > /dev/null pushd $2 > /dev/null

View File

@ -25,5 +25,5 @@ proc-macro2 = "0.4"
quote = "0.6" quote = "0.6"
syn = { version = "^0.15.34", features = ["full"] } syn = { version = "^0.15.34", features = ["full"] }
[dev-dependencies] [build-dependencies]
yew = { path = "../.." } autocfg = "0.1.3"

7
crates/macro/build.rs Normal file
View File

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

View File

@ -48,35 +48,72 @@ impl Parse for HtmlComponent {
impl ToTokens for HtmlComponent { impl ToTokens for HtmlComponent {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let HtmlComponentInner { ty, props } = &self.0; 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 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 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, .. }| { let check_props = vec_props.iter().map(|HtmlProp { label, .. }| {
quote_spanned! { label.span()=> #vcomp_props.#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 }| { let set_props = vec_props.iter().map(|HtmlProp { label, value }| {
quote_spanned! { value.span()=> quote_spanned! { value.span()=>
#vcomp_props.#label = <::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value); .#label(<::yew::virtual_dom::vcomp::VComp<_> as ::yew::virtual_dom::vcomp::Transformer<_, _, _>>::transform(#vcomp_scope.clone(), #value))
} }
}); });
quote! { quote! {
#(#check_props#set_props)* <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder()
#(#set_props)*
.build()
} }
} }
Props::With(WithProps(props)) => { Props::With(WithProps(props)) => quote! { #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 #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::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)) Ok(ListProps(props))
} }
} }

View File

@ -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"

View File

@ -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<Ident>,
}
#[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<PropField> = {
let res: Result<Vec<PropField>, 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<Ident> = 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<P: #step_name, #generic_params> #generic_where {
wrapped: ::std::boxed::Box<#wrapped_name<#generic_idents>>,
_marker: ::std::marker::PhantomData<P>,
}
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<Item = &'a PropField>,
) -> impl Iterator<Item = impl ToTokens + 'a> {
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<Item = &'a PropField>,
) -> impl Iterator<Item = impl ToTokens + 'a> {
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<Item = &'a PropField>,
) -> impl Iterator<Item = impl ToTokens + 'a> {
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<MetaList> {
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<Option<Ident>, 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<Item = &'a Ident>,
) -> impl Iterator<Item = impl ToTokens + 'a> {
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<WhereClause>,
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
}

View File

@ -0,0 +1,6 @@
#[test]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/pass.rs");
t.compile_fail("tests/fail.rs");
}

View File

@ -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() {}

View File

@ -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<t3::Props_value_is_required>` 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<t4::Props_a_is_required>` 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`.

View File

@ -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<T: Default> {
value: T,
}
fn optional_prop_generics_should_work() {
Props::<bool>::builder().build();
Props::<bool>::builder().value(true).build();
}
}
mod t2 {
use super::*;
struct Value;
#[derive(Properties)]
pub struct Props<T> {
#[props(required)]
value: T,
}
fn required_prop_generics_should_work() {
Props::<Value>::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<T>
where
T: Default,
{
value: T,
}
fn optional_prop_generics_should_work() {
Props::<bool>::builder().build();
Props::<bool>::builder().value(true).build();
}
}
fn main() {}

View File

@ -1,32 +1,23 @@
use crate::button::Button; use crate::button::Button;
use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; use yew::prelude::*;
pub struct Barrier { pub struct Barrier {
limit: u32, limit: u32,
counter: u32, counter: u32,
onsignal: Option<Callback<()>>, onsignal: Callback<()>,
} }
pub enum Msg { pub enum Msg {
ChildClicked, ChildClicked,
} }
#[derive(PartialEq, Clone)] #[derive(PartialEq, Properties)]
pub struct Props { pub struct Props {
pub limit: u32, pub limit: u32,
pub onsignal: Option<Callback<()>>, #[props(required)]
pub onsignal: Callback<()>,
} }
impl Default for Props {
fn default() -> Self {
Props {
limit: 0,
onsignal: None,
}
}
}
impl Component for Barrier { impl Component for Barrier {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
@ -44,13 +35,11 @@ impl Component for Barrier {
Msg::ChildClicked => { Msg::ChildClicked => {
self.counter += 1; self.counter += 1;
if self.counter >= self.limit { if self.counter >= self.limit {
if let Some(ref mut callback) = self.onsignal { self.onsignal.emit(());
callback.emit(());
self.counter = 0; self.counter = 0;
} }
} }
} }
}
true true
} }

View File

@ -1,27 +1,19 @@
use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; use yew::prelude::*;
pub struct Button { pub struct Button {
title: String, title: String,
onsignal: Option<Callback<()>>, onsignal: Callback<()>,
} }
pub enum Msg { pub enum Msg {
Clicked, Clicked,
} }
#[derive(PartialEq, Clone)] #[derive(PartialEq, Properties)]
pub struct Props { pub struct Props {
pub title: String, pub title: String,
pub onsignal: Option<Callback<()>>, #[props(required)]
} pub onsignal: Callback<()>,
impl Default for Props {
fn default() -> Self {
Props {
title: "Send Signal".into(),
onsignal: None,
}
}
} }
impl Component for Button { impl Component for Button {
@ -38,9 +30,7 @@ impl Component for Button {
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg { match msg {
Msg::Clicked => { Msg::Clicked => {
if let Some(ref mut callback) = self.onsignal { self.onsignal.emit(());
callback.emit(());
}
} }
} }
false false

View File

@ -1,4 +1,4 @@
use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender}; use yew::prelude::*;
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
pub enum Color { pub enum Color {
@ -7,31 +7,28 @@ pub enum Color {
Blue, Blue,
} }
impl Default for Color {
fn default() -> Self {
Color::Green
}
}
pub struct Counter { pub struct Counter {
value: u32, value: u32,
color: Color, color: Color,
onclick: Option<Callback<u32>>, onclick: Callback<u32>,
} }
pub enum Msg { pub enum Msg {
Increase, Increase,
} }
#[derive(PartialEq, Clone)] #[derive(PartialEq, Properties)]
pub struct Props { pub struct Props {
pub initial: u32, pub initial: u32,
pub color: Color, pub color: Color,
pub onclick: Option<Callback<u32>>, #[props(required)]
} pub onclick: Callback<u32>,
impl Default for Props {
fn default() -> Self {
Props {
initial: 0,
color: Color::Green,
onclick: None,
}
}
} }
impl Component for Counter { impl Component for Counter {
@ -50,9 +47,7 @@ impl Component for Counter {
match msg { match msg {
Msg::Increase => { Msg::Increase => {
self.value = self.value + 1; self.value = self.value + 1;
if let Some(ref onclick) = self.onclick { self.onclick.emit(self.value);
onclick.emit(self.value);
}
} }
} }
true true

View File

@ -1,12 +1,12 @@
#![recursion_limit = "128"] #![recursion_limit = "128"]
mod counter;
mod button;
mod barrier; mod barrier;
mod button;
mod counter;
use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender};
use counter::{Counter, Color};
use barrier::Barrier; use barrier::Barrier;
use counter::{Color, Counter};
use yew::prelude::*;
pub struct Model { pub struct Model {
with_barrier: bool, with_barrier: bool,
@ -19,8 +19,7 @@ pub enum Msg {
ChildClicked(u32), ChildClicked(u32),
} }
impl Component for Model impl Component for Model {
{
type Message = Msg; type Message = Msg;
type Properties = (); type Properties = ();
@ -41,17 +40,17 @@ impl Component for Model
self.with_barrier = !self.with_barrier; self.with_barrier = !self.with_barrier;
true true
} }
Msg::ChildClicked(_value) => { Msg::ChildClicked(_value) => false,
false
}
} }
} }
} }
impl Renderable<Model> for Model { impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> { fn view(&self) -> Html<Self> {
let counter = |x| html! { let counter = |x| {
<Counter initial=x color=&self.color onclick=Msg::ChildClicked/> html! {
<Counter initial=x color=&self.color onclick=Msg::ChildClicked />
}
}; };
html! { html! {
<div class="custom-components-example"> <div class="custom-components-example">

View File

@ -6,4 +6,7 @@ edition = "2018"
[dependencies] [dependencies]
yew = { path = "../.." } yew = { path = "../.." }
stdweb = "0.4.7" stdweb = "^0.4.16"
[target.'cfg(all(target_arch = "wasm32", not(cargo_web)))'.dependencies]
wasm-bindgen = "0.2"

View File

@ -1,10 +1,8 @@
#![recursion_limit="128"] #![recursion_limit = "128"]
#![deny(warnings)] #![deny(warnings)]
#[macro_use] use stdweb::{_js_impl, js};
extern crate stdweb; use yew::prelude::*;
use yew::{html, Callback, Component, ComponentLink, Html, Renderable, ShouldRender};
pub struct Model { pub struct Model {
payload: String, payload: String,
@ -18,16 +16,12 @@ pub enum Msg {
AsyncPayload, AsyncPayload,
} }
#[derive(Default, PartialEq, Eq, Clone)]
pub struct Props {
payload: String,
}
impl Component for Model { impl Component for Model {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = ();
fn create(Props { payload }: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let payload = String::default();
let debugged_payload = format!("{:?}", payload); let debugged_payload = format!("{:?}", payload);
Self { Self {
payload, payload,
@ -39,7 +33,15 @@ impl Component for Model {
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, msg: Self::Message) -> ShouldRender {
use Msg::*; use Msg::*;
match 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 => { AsyncPayload => {
get_payload_later(self.link.send_back(Msg::Payload)); get_payload_later(self.link.send_back(Msg::Payload));
false false
@ -47,14 +49,8 @@ impl Component for Model {
} }
} }
fn change(&mut self, Self::Properties { payload }: Self::Properties) -> ShouldRender { fn change(&mut self, _: Self::Properties) -> ShouldRender {
if payload == self.payload {
false false
} else {
self.debugged_payload = format!("{:?}", payload);
self.payload = payload;
true
}
} }
} }
@ -73,7 +69,7 @@ impl Renderable<Model> for Model {
{ "Get the payload later!" } { "Get the payload later!" }
</button> </button>
<p style="font-family: 'Monaco', monospace;"> <p style="font-family: 'Monaco', monospace;">
{ nbsp(self.debugged_payload.as_ref()) } { nbsp(self.debugged_payload.as_str()) }
</p> </p>
</div> </div>
} }

View File

@ -10,7 +10,6 @@ use std::rc::Rc;
/// Callbacks should be used from JS callbacks or `setTimeout` calls. /// Callbacks should be used from JS callbacks or `setTimeout` calls.
/// </aside> /// </aside>
/// `Rc` wrapper used to make it clonable. /// `Rc` wrapper used to make it clonable.
#[must_use]
pub struct Callback<IN>(Rc<dyn Fn(IN)>); pub struct Callback<IN>(Rc<dyn Fn(IN)>);
impl<IN, F: Fn(IN) + 'static> From<F> for Callback<IN> { impl<IN, F: Fn(IN) + 'static> From<F> for Callback<IN> {

View File

@ -17,7 +17,7 @@
use crate::callback::Callback; use crate::callback::Callback;
use crate::html::{ChangeData, Component, ComponentLink, Html, Renderable, ShouldRender}; use crate::html::{ChangeData, Component, ComponentLink, Html, Renderable, ShouldRender};
use crate::macros::html; use crate::macros::{html, Properties};
/// `Select` component. /// `Select` component.
pub struct Select<T> { pub struct Select<T> {
@ -31,7 +31,7 @@ pub enum Msg {
} }
/// Properties of `Select` component. /// Properties of `Select` component.
#[derive(PartialEq, Clone)] #[derive(PartialEq, Properties)]
pub struct Props<T> { pub struct Props<T> {
/// Initially selected value. /// Initially selected value.
pub selected: Option<T>, pub selected: Option<T>,
@ -40,18 +40,8 @@ pub struct Props<T> {
/// Options are available to choose. /// Options are available to choose.
pub options: Vec<T>, pub options: Vec<T>,
/// Callback to handle changes. /// Callback to handle changes.
pub onchange: Option<Callback<T>>, #[props(required)]
} pub onchange: Callback<T>,
impl<T> Default for Props<T> {
fn default() -> Self {
Props {
selected: None,
disabled: false,
options: Vec::new(),
onchange: None,
}
}
} }
impl<T> Component for Select<T> impl<T> Component for Select<T>
@ -69,11 +59,9 @@ where
match msg { match msg {
Msg::Selected(value) => { Msg::Selected(value) => {
if let Some(idx) = value { if let Some(idx) = value {
if let Some(ref mut callback) = self.props.onchange {
let item = self.props.options.get(idx - 1).cloned(); let item = self.props.options.get(idx - 1).cloned();
if let Some(value) = item { if let Some(value) = item {
callback.emit(value); self.props.onchange.emit(value);
}
} }
} }
} }

View File

@ -23,10 +23,7 @@ pub trait Component: Sized + 'static {
/// Control message type which `update` loop get. /// Control message type which `update` loop get.
type Message: 'static; type Message: 'static;
/// Properties type of component implementation. /// Properties type of component implementation.
/// It sould be serializable because it's sent to dynamicaly created type Properties: Properties;
/// component (layed under `VComp`) and must be restored for a component
/// with unknown type.
type Properties: Clone + Default;
/// Initialization routine which could use a context. /// Initialization routine which could use a context.
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self; fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self;
/// Called everytime when a messages of `Msg` type received. It also takes a /// 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` 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. /// Should be rendered relative to context and component environment.
pub trait Renderable<COMP: Component> { pub trait Renderable<COMP: Component> {
/// Called by rendering loop. /// Called by rendering loop.

View File

@ -74,6 +74,7 @@ pub use yew_macro::html;
/// This module contains macros which implements html! macro and JSX-like templates /// This module contains macros which implements html! macro and JSX-like templates
pub mod macros { pub mod macros {
pub use crate::html; pub use crate::html;
pub use yew_props_derive::Properties;
} }
pub mod agent; pub mod agent;

View File

@ -150,12 +150,12 @@ where
} }
} }
impl<'a, COMP, F, IN> Transformer<COMP, F, Option<Callback<IN>>> for VComp<COMP> impl<'a, COMP, F, IN> Transformer<COMP, F, Callback<IN>> for VComp<COMP>
where where
COMP: Component + Renderable<COMP>, COMP: Component + Renderable<COMP>,
F: Fn(IN) -> COMP::Message + 'static, F: Fn(IN) -> COMP::Message + 'static,
{ {
fn transform(scope: ScopeHolder<COMP>, from: F) -> Option<Callback<IN>> { fn transform(scope: ScopeHolder<COMP>, from: F) -> Callback<IN> {
let callback = move |arg| { let callback = move |arg| {
let msg = from(arg); let msg = from(arg);
if let Some(ref mut sender) = *scope.borrow_mut() { if let Some(ref mut sender) = *scope.borrow_mut() {
@ -164,7 +164,7 @@ where
panic!("unactivated callback, parent component have to activate it"); panic!("unactivated callback, parent component have to activate it");
} }
}; };
Some(callback.into()) callback.into()
} }
} }

View File

@ -0,0 +1,9 @@
#![recursion_limit = "128"]
use yew::prelude::*;
fn compile_fail() {
html! { <String /> };
}
fn main() {}

View File

@ -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! { <String /> };
| ^^^^^^ 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`.

View File

@ -2,9 +2,10 @@
use yew::prelude::*; use yew::prelude::*;
#[derive(Clone, Default, PartialEq)] #[derive(Properties, PartialEq)]
pub struct ChildProperties { pub struct ChildProperties {
pub string: String, pub string: String,
#[props(required)]
pub int: i32, pub int: i32,
} }
@ -40,14 +41,11 @@ fn compile_fail() {
html! { <ChildComponent invalid-prop-name=0 /> }; html! { <ChildComponent invalid-prop-name=0 /> };
html! { <ChildComponent unknown="unknown" /> }; html! { <ChildComponent unknown="unknown" /> };
html! { <ChildComponent string= /> }; html! { <ChildComponent string= /> };
html! { <ChildComponent string={} /> }; html! { <ChildComponent int=1 string={} /> };
html! { <ChildComponent string=3 /> }; html! { <ChildComponent int=1 string=3 /> };
html! { <ChildComponent string={3} /> }; html! { <ChildComponent int=1 string={3} /> };
html! { <ChildComponent int=0u32 /> }; html! { <ChildComponent int=0u32 /> };
} html! { <ChildComponent string="abc" /> };
fn additional_fail() {
html! { <String /> };
} }
fn main() {} fn main() {}

View File

@ -1,84 +1,93 @@
error: expected component tag be of form `< .. />` error: expected component tag be of form `< .. />`
--> $DIR/html-component-fail.rs:32:13 --> $DIR/html-component-fail.rs:33:13
| |
32 | html! { <ChildComponent> }; 33 | html! { <ChildComponent> };
| ^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^
error: unexpected end of input, expected identifier error: unexpected end of input, expected identifier
--> $DIR/html-component-fail.rs:33:31 --> $DIR/html-component-fail.rs:34:31
| |
33 | html! { <ChildComponent:: /> }; 34 | html! { <ChildComponent:: /> };
| ^ | ^
error: unexpected end of input, expected identifier error: unexpected end of input, expected identifier
--> $DIR/html-component-fail.rs:34:34 --> $DIR/html-component-fail.rs:35:34
| |
34 | html! { <ChildComponent with /> }; 35 | html! { <ChildComponent with /> };
| ^ | ^
error: unexpected token error: unexpected token
--> $DIR/html-component-fail.rs:35:29 --> $DIR/html-component-fail.rs:36:29
| |
35 | html! { <ChildComponent props /> }; 36 | html! { <ChildComponent props /> };
| ^^^^^ | ^^^^^
error: expected component tag be of form `< .. />` error: expected component tag be of form `< .. />`
--> $DIR/html-component-fail.rs:36:13 --> $DIR/html-component-fail.rs:37:13
| |
36 | html! { <ChildComponent with props > }; 37 | html! { <ChildComponent with props > };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: unexpected token error: unexpected token
--> $DIR/html-component-fail.rs:38:40 --> $DIR/html-component-fail.rs:39:40
| |
38 | html! { <ChildComponent with props () /> }; 39 | html! { <ChildComponent with props () /> };
| ^^ | ^^
error: expected identifier
--> $DIR/html-component-fail.rs:39:29
|
39 | html! { <ChildComponent type=0 /> };
| ^^^^
error: expected identifier error: expected identifier
--> $DIR/html-component-fail.rs:40:29 --> $DIR/html-component-fail.rs:40:29
| |
40 | html! { <ChildComponent invalid-prop-name=0 /> }; 40 | html! { <ChildComponent type=0 /> };
| ^^^^
error: expected identifier
--> $DIR/html-component-fail.rs:41:29
|
41 | html! { <ChildComponent invalid-prop-name=0 /> };
| ^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^
error: unexpected end of input, expected expression error: unexpected end of input, expected expression
--> $DIR/html-component-fail.rs:42:37 --> $DIR/html-component-fail.rs:43:37
| |
42 | html! { <ChildComponent string= /> }; 43 | html! { <ChildComponent string= /> };
| ^ | ^
error[E0425]: cannot find value `blah` in this scope 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! { <ChildComponent with blah /> }; 38 | html! { <ChildComponent with blah /> };
| ^^^^ not found in this scope | ^^^^ not found in this scope
error[E0609]: no field `unknown` on type `ChildProperties` error[E0609]: no field `unknown` on type `ChildProperties`
--> $DIR/html-component-fail.rs:41:29 --> $DIR/html-component-fail.rs:42:29
| |
41 | html! { <ChildComponent unknown="unknown" /> }; 42 | html! { <ChildComponent unknown="unknown" /> };
| ^^^^^^^ unknown field | ^^^^^^^ unknown field
| |
= note: available fields are: `string`, `int` = note: available fields are: `string`, `int`
error[E0308]: mismatched types error[E0599]: no method named `unknown` found for type `ChildPropertiesBuilder<ChildProperties_int_is_required>` in the current scope
--> $DIR/html-component-fail.rs:43:36 --> $DIR/html-component-fail.rs:42:29
| |
43 | html! { <ChildComponent string={} /> }; 5 | #[derive(Properties, PartialEq)]
| - method `unknown` not found for this
...
42 | html! { <ChildComponent unknown="unknown" /> };
| ^^^^^^^
error[E0308]: mismatched types
--> $DIR/html-component-fail.rs:44:42
|
44 | html! { <ChildComponent int=1 string={} /> };
| ^^ expected struct `std::string::String`, found () | ^^ expected struct `std::string::String`, found ()
| |
= note: expected type `std::string::String` = note: expected type `std::string::String`
found type `()` found type `()`
error[E0308]: mismatched types error[E0308]: mismatched types
--> $DIR/html-component-fail.rs:44:36 --> $DIR/html-component-fail.rs:45:42
| |
44 | html! { <ChildComponent string=3 /> }; 45 | html! { <ChildComponent int=1 string=3 /> };
| ^ | ^
| | | |
| expected struct `std::string::String`, found integer | expected struct `std::string::String`, found integer
@ -88,9 +97,9 @@ error[E0308]: mismatched types
found type `{integer}` found type `{integer}`
error[E0308]: mismatched types error[E0308]: mismatched types
--> $DIR/html-component-fail.rs:45:36 --> $DIR/html-component-fail.rs:46:42
| |
45 | html! { <ChildComponent string={3} /> }; 46 | html! { <ChildComponent int=1 string={3} /> };
| ^^^ | ^^^
| | | |
| expected struct `std::string::String`, found integer | expected struct `std::string::String`, found integer
@ -100,28 +109,23 @@ error[E0308]: mismatched types
found type `{integer}` found type `{integer}`
error[E0308]: mismatched types error[E0308]: mismatched types
--> $DIR/html-component-fail.rs:46:33 --> $DIR/html-component-fail.rs:47:33
| |
46 | html! { <ChildComponent int=0u32 /> }; 47 | html! { <ChildComponent int=0u32 /> };
| ^^^^ expected i32, found u32 | ^^^^ expected i32, found u32
help: you can convert an `u32` to `i32` and panic if the converted value wouldn't fit help: you can convert an `u32` to `i32` and panic if the converted value wouldn't fit
| |
46 | html! { <ChildComponent int=0u32.try_into().unwrap() /> }; 47 | html! { <ChildComponent int=0u32.try_into().unwrap() /> };
| ^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `std::string::String: yew::html::Component` is not satisfied error[E0599]: no method named `string` found for type `ChildPropertiesBuilder<ChildProperties_int_is_required>` in the current scope
--> $DIR/html-component-fail.rs:50:14 --> $DIR/html-component-fail.rs:48:29
| |
50 | html! { <String /> }; 5 | #[derive(Properties, PartialEq)]
| ^^^^^^ the trait `yew::html::Component` is not implemented for `std::string::String` | - method `string` not found for this
...
48 | html! { <ChildComponent string="abc" /> };
| ^^^^^^
error[E0277]: the trait bound `std::string::String: yew::html::Renderable<std::string::String>` is not satisfied Some errors have detailed explanations: E0308, E0425, E0599, E0609.
--> $DIR/html-component-fail.rs:50:14 For more information about an error, try `rustc --explain E0308`.
|
50 | html! { <String /> };
| ^^^^^^ the trait `yew::html::Renderable<std::string::String>` is not implemented for `std::string::String`
|
= note: required by `yew::virtual_dom::vcomp::VComp::<COMP>::new`
Some errors have detailed explanations: E0277, E0308, E0425, E0609.
For more information about an error, try `rustc --explain E0277`.

View File

@ -3,9 +3,10 @@
#[macro_use] #[macro_use]
mod helpers; mod helpers;
#[derive(Clone, Default, PartialEq)] #[derive(Properties, Default, PartialEq)]
pub struct ChildProperties { pub struct ChildProperties {
pub string: String, pub string: String,
#[props(required)]
pub int: i32, pub int: i32,
pub vec: Vec<i32>, pub vec: Vec<i32>,
} }
@ -35,19 +36,19 @@ mod scoped {
} }
pass_helper! { pass_helper! {
html! { <ChildComponent /> }; html! { <ChildComponent int=1 /> };
// backwards compat // backwards compat
html! { <ChildComponent: /> }; html! { <ChildComponent: int=1 /> };
html! { html! {
<> <>
<ChildComponent /> <ChildComponent int=1 />
<scoped::ChildComponent /> <scoped::ChildComponent int=1 />
// backwards compat // backwards compat
<ChildComponent: /> <ChildComponent: int=1 />
<scoped::ChildComponent: /> <scoped::ChildComponent: int=1 />
</> </>
}; };
@ -64,10 +65,10 @@ pass_helper! {
html! { html! {
<> <>
<ChildComponent string="child" /> <ChildComponent int=1 string="child" />
<ChildComponent int=1 /> <ChildComponent int=1 />
<ChildComponent int={1+1} /> <ChildComponent int={1+1} />
<ChildComponent vec={vec![1]} /> <ChildComponent int=1 vec={vec![1]} />
<ChildComponent string={String::from("child")} int=1 /> <ChildComponent string={String::from("child")} int=1 />
// backwards compat // backwards compat
@ -77,7 +78,7 @@ pass_helper! {
let name_expr = "child"; let name_expr = "child";
html! { html! {
<ChildComponent string=name_expr /> <ChildComponent int=1 string=name_expr />
}; };
} }

View File

@ -1,6 +1,6 @@
use yew::prelude::*; use yew::prelude::*;
#[derive(Clone, Default, PartialEq)] #[derive(Properties, PartialEq)]
pub struct TestProperties { pub struct TestProperties {
pub string: String, pub string: String,
pub int: i32, pub int: i32,

View File

@ -1,5 +1,6 @@
#[cfg(feature = "wasm-bindgen-test")] #[cfg(feature = "wasm-bindgen-test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
use yew::macros::Properties;
use yew::virtual_dom::VNode; use yew::virtual_dom::VNode;
use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender}; use yew::{html, Component, ComponentLink, Html, Renderable, ShouldRender};
@ -8,21 +9,12 @@ wasm_bindgen_test_configure!(run_in_browser);
struct Comp; struct Comp;
#[derive(PartialEq, Clone)] #[derive(PartialEq, Properties)]
struct Props { struct Props {
field_1: u32, field_1: u32,
field_2: u32, field_2: u32,
} }
impl Default for Props {
fn default() -> Self {
Props {
field_1: 0,
field_2: 0,
}
}
}
impl Component for Comp { impl Component for Comp {
type Message = (); type Message = ();
type Properties = Props; type Properties = Props;