mirror of https://github.com/yewstack/yew
Add support for required props
This commit is contained in:
parent
df47f20daa
commit
3fed651b42
|
@ -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",
|
||||||
|
|
24
README.md
24
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.
|
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>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
#[test]
|
||||||
|
fn tests() {
|
||||||
|
let t = trybuild::TestCases::new();
|
||||||
|
t.pass("tests/pass.rs");
|
||||||
|
t.compile_fail("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() {}
|
|
@ -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`.
|
|
@ -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() {}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
src/html.rs
29
src/html.rs
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
#![recursion_limit = "128"]
|
||||||
|
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
fn compile_fail() {
|
||||||
|
html! { <String /> };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -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`.
|
|
@ -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() {}
|
||||||
|
|
|
@ -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`.
|
|
||||||
|
|
|
@ -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 />
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue