diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 2a52d1c5c3..7cde3ee5af 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -33,7 +33,9 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { let fields = quote!(#(#fields)*).into_iter(); let required_fields = struct_info .included_fields() - .filter(|f| f.builder_attr.default.is_none()) + .filter(|f| { + f.builder_attr.default.is_none() && f.builder_attr.extends.is_empty() + }) .map(|f| struct_info.required_field_impl(f)) .collect::, _>>()?; let build_method = struct_info.build_method_impl(); @@ -172,7 +174,7 @@ mod util { mod field_info { use crate::props::type_from_inside_option; use proc_macro2::TokenStream; - use quote::quote; + use quote::{format_ident, quote}; use syn::spanned::Spanned; use syn::{parse::Error, punctuated::Punctuated}; use syn::{parse_quote, Expr, Path}; @@ -269,6 +271,13 @@ mod field_info { } .into() } + + pub fn extends_vec_ident(&self) -> Option { + (!self.builder_attr.extends.is_empty()).then(|| { + let ident = &self.name; + format_ident!("__{ident}_attributes") + }) + } } #[derive(Debug, Default, Clone)] @@ -566,9 +575,7 @@ mod struct_info { impl<'a> StructInfo<'a> { pub fn included_fields(&self) -> impl Iterator> { - self.fields - .iter() - .filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty()) + self.fields.iter().filter(|f| !f.builder_attr.skip) } pub fn extend_fields(&self) -> impl Iterator> { @@ -670,7 +677,6 @@ mod struct_info { let regular_fields: Vec<_> = self .included_fields() - .chain(self.extend_fields()) .filter(|f| !looks_like_signal_type(f.ty) && !looks_like_callback_type(f.ty)) .map(|f| { let name = f.name; @@ -840,7 +846,7 @@ Finally, call `.build()` to create the instance of `{name}`. let global_fields = self .extend_fields() .map(|f| { - let name = f.name; + let name = f.extends_vec_ident(); let ty = f.ty; quote!(#name: #ty) }) @@ -848,7 +854,7 @@ Finally, call `.build()` to create the instance of `{name}`. let global_fields_value = self .extend_fields() .map(|f| { - let name = f.name; + let name = f.extends_vec_ident(); quote!(#name: Vec::new()) }) .chain( @@ -922,15 +928,11 @@ Finally, call `.build()` to create the instance of `{name}`. ref builder_name, .. } = *self; - let field_name = field.name; + let field_name = field.extends_vec_ident().unwrap(); let descructuring = self.included_fields().map(|f| { - if f.ordinal == field.ordinal { - quote!(_) - } else { - let name = f.name; - quote!(#name) - } + let name = f.name; + quote!(#name) }); let reconstructing = self.included_fields().map(|f| f.name); @@ -962,7 +964,12 @@ Finally, call `.build()` to create the instance of `{name}`. .count(); for f in self.included_fields() { if f.ordinal == field.ordinal { - ty_generics_tuple.elems.push_value(empty_type()); + g.params.insert( + index_after_lifetime_in_generics, + syn::GenericParam::Type(self.generic_builder_param(f)), + ); + let generic_argument: syn::Type = f.type_ident(); + ty_generics_tuple.elems.push_value(generic_argument.clone()); target_generics_tuple .elems .push_value(f.tuplized_type_ty_param()); @@ -993,7 +1000,7 @@ Finally, call `.build()` to create the instance of `{name}`. let (impl_generics, _, where_clause) = generics.split_for_impl(); let forward_extended_fields = self.extend_fields().map(|f| { - let name = f.name; + let name = f.extends_vec_ident(); quote!(#name: self.#name) }); @@ -1176,7 +1183,7 @@ Finally, call `.build()` to create the instance of `{name}`. let forward_fields = self .extend_fields() .map(|f| { - let name = f.name; + let name = f.extends_vec_ident(); quote!(#name: self.#name) }) .chain( @@ -1323,6 +1330,31 @@ Finally, call `.build()` to create the instance of `{name}`. }) } + fn generic_builder_param(&self, field: &FieldInfo) -> syn::TypeParam { + let trait_ref = syn::TraitBound { + paren_token: None, + lifetimes: None, + modifier: syn::TraitBoundModifier::None, + path: syn::PathSegment { + ident: self.conversion_helper_trait_name.clone(), + arguments: syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Default::default(), + args: make_punctuated_single(syn::GenericArgument::Type( + field.ty.clone(), + )), + gt_token: Default::default(), + }, + ), + } + .into(), + }; + let mut generic_param: syn::TypeParam = field.generic_ident.clone().into(); + generic_param.bounds.push(trait_ref.into()); + generic_param + } + pub fn build_method_impl(&self) -> TokenStream { let StructInfo { ref name, @@ -1338,27 +1370,7 @@ Finally, call `.build()` to create the instance of `{name}`. .count(); for field in self.included_fields() { if field.builder_attr.default.is_some() { - let trait_ref = syn::TraitBound { - paren_token: None, - lifetimes: None, - modifier: syn::TraitBoundModifier::None, - path: syn::PathSegment { - ident: self.conversion_helper_trait_name.clone(), - arguments: syn::PathArguments::AngleBracketed( - syn::AngleBracketedGenericArguments { - colon2_token: None, - lt_token: Default::default(), - args: make_punctuated_single(syn::GenericArgument::Type( - field.ty.clone(), - )), - gt_token: Default::default(), - }, - ), - } - .into(), - }; - let mut generic_param: syn::TypeParam = field.generic_ident.clone().into(); - generic_param.bounds.push(trait_ref.into()); + let generic_param = self.generic_builder_param(field); g.params .insert(index_after_lifetime_in_generics, generic_param.into()); } @@ -1395,10 +1407,12 @@ Finally, call `.build()` to create the instance of `{name}`. // reordering based on that, but for now this much simpler thing is a reasonable approach. let assignments = self.fields.iter().map(|field| { let name = &field.name; - if !field.builder_attr.extends.is_empty() { - quote!(let #name = self.#name;) + if let Some(extends_vec) = field.extends_vec_ident() { + quote!{ + let mut #name = #helper_trait_name::into_value(#name, || ::core::default::Default::default()); + #name.extend(self.#extends_vec); + } } else if let Some(ref default) = field.builder_attr.default { - // If field has `into`, apply it to the default value. // Ignore any blank defaults as it causes type inference errors. let is_default = *default == parse_quote!(::core::default::Default::default()); diff --git a/packages/document/src/elements/link.rs b/packages/document/src/elements/link.rs index 215bd8a8a4..cbfa3dcfa7 100644 --- a/packages/document/src/elements/link.rs +++ b/packages/document/src/elements/link.rs @@ -2,6 +2,14 @@ use super::*; use crate::document; use dioxus_html as dioxus_elements; +#[non_exhaustive] +#[derive(Clone, Props, PartialEq)] +pub struct OtherLinkProps { + pub rel: String, + #[props(extends = link, extends = GlobalAttributes)] + pub additional_attributes: Vec, +} + #[non_exhaustive] #[derive(Clone, Props, PartialEq)] pub struct LinkProps { diff --git a/packages/ssr/tests/forward_spreads.rs b/packages/ssr/tests/forward_spreads.rs new file mode 100644 index 0000000000..0cf03845c1 --- /dev/null +++ b/packages/ssr/tests/forward_spreads.rs @@ -0,0 +1,55 @@ +use dioxus::prelude::*; + +// Regression test for https://github.com/DioxusLabs/dioxus/issues/3844 +#[test] +fn forward_spreads() { + #[derive(Props, Clone, PartialEq)] + struct Comp1Props { + #[props(extends = GlobalAttributes)] + attributes: Vec, + } + + #[component] + fn Comp1(props: Comp1Props) -> Element { + rsx! { + Comp2 { + attributes: props.attributes.clone(), + height: "100%", + } + Comp2 { + height: "100%", + attributes: props.attributes.clone(), + } + } + } + + #[derive(Props, Clone, PartialEq)] + struct CompProps2 { + #[props(extends = GlobalAttributes)] + attributes: Vec, + } + + #[component] + fn Comp2(props: CompProps2) -> Element { + let attributes = props.attributes; + rsx! { + div { + ..attributes + } + } + } + + let merged = || { + rsx! { + Comp1 { + width: "100%" + } + } + }; + let dom = VirtualDom::prebuilt(merged); + let html = dioxus_ssr::render(&dom); + assert_eq!( + html, + r#"
"# + ); +}