Răsfoiți Sursa

Merge pull request #1349 from atty303/issue-1179

feat: props spread
Jonathan Kelley 1 an în urmă
părinte
comite
4fc2802eab

+ 2 - 0
Cargo.toml

@@ -9,6 +9,7 @@ members = [
     "packages/extension",
     "packages/router",
     "packages/html",
+    "packages/html-internal-macro",
     "packages/hooks",
     "packages/web",
     "packages/ssr",
@@ -60,6 +61,7 @@ dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0"  }
 dioxus-router = { path = "packages/router", version = "0.4.1"  }
 dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" }
 dioxus-html = { path = "packages/html", default-features = false, version = "0.4.0"  }
+dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.4.0"  }
 dioxus-hooks = { path = "packages/hooks", version = "0.4.0" }
 dioxus-web = { path = "packages/web", version = "0.4.0"  }
 dioxus-ssr = { path = "packages/ssr", version = "0.4.0"  }

+ 0 - 1
examples/README.md

@@ -139,7 +139,6 @@ Missing Features
 Missing examples
 - Shared state
 - Root-less element groups
-- Spread props
 - Custom elements
 - Component Children: Pass children into child components
 - Render To string: Render a mounted virtualdom to a string

+ 36 - 0
examples/spread.rs

@@ -0,0 +1,36 @@
+use dioxus::prelude::*;
+
+fn main() {
+    let mut dom = VirtualDom::new(app);
+    let _ = dom.rebuild();
+    let html = dioxus_ssr::render(&dom);
+
+    println!("{}", html);
+}
+
+fn app(cx: Scope) -> Element {
+    render! {
+        Component {
+            width: "10px",
+            extra_data: "hello{1}",
+            extra_data2: "hello{2}",
+            height: "10px",
+            left: 1
+        }
+    }
+}
+
+#[component]
+fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
+    render! {
+        audio { ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" }
+    }
+}
+
+#[derive(Props)]
+struct Props<'a> {
+    #[props(extends = GlobalAttributes)]
+    attributes: Vec<Attribute<'a>>,
+    extra_data: &'a str,
+    extra_data2: &'a str,
+}

+ 17 - 3
packages/autofmt/src/element.rs

@@ -166,7 +166,7 @@ impl Writer<'_> {
 
     fn write_attributes(
         &mut self,
-        attributes: &[ElementAttrNamed],
+        attributes: &[AttributeType],
         key: &Option<IfmtInput>,
         sameline: bool,
     ) -> Result {
@@ -188,7 +188,7 @@ impl Writer<'_> {
         while let Some(attr) = attr_iter.next() {
             self.out.indent_level += 1;
             if !sameline {
-                self.write_comments(attr.attr.start())?;
+                self.write_comments(attr.start())?;
             }
             self.out.indent_level -= 1;
 
@@ -289,7 +289,14 @@ impl Writer<'_> {
         Ok(())
     }
 
-    fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
+    fn write_attribute(&mut self, attr: &AttributeType) -> Result {
+        match attr {
+            AttributeType::Named(attr) => self.write_named_attribute(attr),
+            AttributeType::Spread(attr) => self.write_spread_attribute(attr),
+        }
+    }
+
+    fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
         self.write_attribute_name(&attr.attr.name)?;
         write!(self.out, ": ")?;
         self.write_attribute_value(&attr.attr.value)?;
@@ -297,6 +304,13 @@ impl Writer<'_> {
         Ok(())
     }
 
+    fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
+        write!(self.out, "..")?;
+        write!(self.out, "{}", prettyplease::unparse_expr(attr))?;
+
+        Ok(())
+    }
+
     // make sure the comments are actually relevant to this element.
     // test by making sure this element is the primary element on this line
     pub fn current_span_is_primary(&self, location: Span) -> bool {

+ 19 - 11
packages/autofmt/src/writer.rs

@@ -1,4 +1,4 @@
-use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop};
+use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop};
 use proc_macro2::{LineColumn, Span};
 use quote::ToTokens;
 use std::{
@@ -165,12 +165,12 @@ impl<'a> Writer<'a> {
         }
     }
 
-    pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
+    pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize {
         let mut total = 0;
 
         for attr in attributes {
-            if self.current_span_is_primary(attr.attr.start()) {
-                'line: for line in self.src[..attr.attr.start().start().line - 1].iter().rev() {
+            if self.current_span_is_primary(attr.start()) {
+                'line: for line in self.src[..attr.start().start().line - 1].iter().rev() {
                     match (line.trim().starts_with("//"), line.is_empty()) {
                         (true, _) => return 100000,
                         (_, true) => continue 'line,
@@ -179,16 +179,24 @@ impl<'a> Writer<'a> {
                 }
             }
 
-            total += match &attr.attr.name {
-                dioxus_rsx::ElementAttrName::BuiltIn(name) => {
-                    let name = name.to_string();
-                    name.len()
+            match attr {
+                AttributeType::Named(attr) => {
+                    let name_len = match &attr.attr.name {
+                        dioxus_rsx::ElementAttrName::BuiltIn(name) => {
+                            let name = name.to_string();
+                            name.len()
+                        }
+                        dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
+                    };
+                    total += name_len;
+                    total += self.attr_value_len(&attr.attr.value);
+                }
+                AttributeType::Spread(expr) => {
+                    let expr_len = self.retrieve_formatted_expr(expr).len();
+                    total += expr_len + 3;
                 }
-                dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
             };
 
-            total += self.attr_value_len(&attr.attr.value);
-
             total += 6;
         }
 

+ 2 - 1
packages/core-macro/Cargo.toml

@@ -15,10 +15,11 @@ proc-macro = true
 [dependencies]
 proc-macro2 = { version = "1.0" }
 quote = "1.0"
-syn = { version = "2.0", features = ["full", "extra-traits"] }
+syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
 dioxus-rsx = { workspace = true }
 dioxus-core = { workspace = true }
 constcat = "0.3.0"
+convert_case = "^0.6.0"
 prettyplease = "0.2.15"
 
 # testing

+ 277 - 24
packages/core-macro/src/props/mod.rs

@@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
                     .included_fields()
                     .map(|f| struct_info.field_impl(f))
                     .collect::<Result<Vec<_>, _>>()?;
+                let extends = struct_info
+                    .extend_fields()
+                    .map(|f| struct_info.extends_impl(f))
+                    .collect::<Result<Vec<_>, _>>()?;
                 let fields = quote!(#(#fields)*).into_iter();
                 let required_fields = struct_info
                     .included_fields()
@@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
                     #builder_creation
                     #conversion_helper
                     #( #fields )*
+                    #( #extends )*
                     #( #required_fields )*
                     #build_method
                 }
@@ -167,8 +172,8 @@ mod field_info {
     use proc_macro2::TokenStream;
     use quote::quote;
     use syn::spanned::Spanned;
-    use syn::Expr;
     use syn::{parse::Error, punctuated::Punctuated};
+    use syn::{Expr, Path};
 
     use super::util::{
         expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
@@ -199,6 +204,13 @@ mod field_info {
                     );
                 }
 
+                // extended field is automatically empty
+                if !builder_attr.extends.is_empty() {
+                    builder_attr.default = Some(
+                        syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
+                    );
+                }
+
                 // auto detect optional
                 let strip_option_auto = builder_attr.strip_option
                     || !builder_attr.ignore_option
@@ -253,6 +265,7 @@ mod field_info {
         pub auto_into: bool,
         pub strip_option: bool,
         pub ignore_option: bool,
+        pub extends: Vec<Path>,
     }
 
     impl FieldBuilderAttr {
@@ -305,6 +318,17 @@ mod field_info {
                     let name = expr_to_single_string(&assign.left)
                         .ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
                     match name.as_str() {
+                        "extends" => {
+                            if let syn::Expr::Path(path) = *assign.right {
+                                self.extends.push(path.path);
+                                Ok(())
+                            } else {
+                                Err(Error::new_spanned(
+                                    assign.right,
+                                    "Expected simple identifier",
+                                ))
+                            }
+                        }
                         "default" => {
                             self.default = Some(*assign.right);
                             Ok(())
@@ -359,6 +383,11 @@ mod field_info {
                             Ok(())
                         }
 
+                        "extend" => {
+                            self.extends.push(path.path);
+                            Ok(())
+                        }
+
                         _ => {
                             macro_rules! handle_fields {
                                 ( $( $flag:expr, $field:ident, $already:expr; )* ) => {
@@ -462,11 +491,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s
 }
 
 mod struct_info {
+    use convert_case::{Case, Casing};
     use proc_macro2::TokenStream;
     use quote::quote;
     use syn::parse::Error;
     use syn::punctuated::Punctuated;
-    use syn::Expr;
+    use syn::spanned::Spanned;
+    use syn::visit::Visit;
+    use syn::{parse_quote, Expr, Ident};
 
     use super::field_info::{FieldBuilderAttr, FieldInfo};
     use super::util::{
@@ -489,7 +521,46 @@ mod struct_info {
 
     impl<'a> StructInfo<'a> {
         pub fn included_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
-            self.fields.iter().filter(|f| !f.builder_attr.skip)
+            self.fields
+                .iter()
+                .filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty())
+        }
+
+        pub fn extend_fields(&self) -> impl Iterator<Item = &FieldInfo<'a>> {
+            self.fields
+                .iter()
+                .filter(|f| !f.builder_attr.extends.is_empty())
+        }
+
+        fn extend_lifetime(&self) -> syn::Result<Option<syn::Lifetime>> {
+            let first_extend = self.extend_fields().next();
+
+            match first_extend {
+                Some(f) => {
+                    struct VisitFirstLifetime(Option<syn::Lifetime>);
+
+                    impl Visit<'_> for VisitFirstLifetime {
+                        fn visit_lifetime(&mut self, lifetime: &'_ syn::Lifetime) {
+                            if self.0.is_none() {
+                                self.0 = Some(lifetime.clone());
+                            }
+                        }
+                    }
+
+                    let name = f.name;
+                    let mut visitor = VisitFirstLifetime(None);
+
+                    visitor.visit_type(f.ty);
+
+                    visitor.0.ok_or_else(|| {
+                        syn::Error::new_spanned(
+                            name,
+                            "Unable to find lifetime for extended field. Please specify it manually",
+                        )
+                    }).map(Some)
+                }
+                None => Ok(None),
+            }
         }
 
         pub fn new(
@@ -536,7 +607,17 @@ mod struct_info {
             // Therefore, we will generate code that shortcircuits the "comparison" in memoization
             let are_there_generics = !self.generics.params.is_empty();
 
-            let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
+            let extend_lifetime = self.extend_lifetime()?;
+
+            let generics = self.generics.clone();
+            let (_, ty_generics, where_clause) = generics.split_for_impl();
+            let impl_generics = self.modify_generics(|g| {
+                if extend_lifetime.is_none() {
+                    g.params.insert(0, parse_quote!('__bump));
+                }
+            });
+            let (impl_generics, _, _) = impl_generics.split_for_impl();
+            let (_, b_initial_generics, _) = self.generics.split_for_impl();
             let all_fields_param = syn::GenericParam::Type(
                 syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(),
             );
@@ -544,7 +625,7 @@ mod struct_info {
                 g.params.insert(0, all_fields_param.clone());
             });
             let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
-            let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
+            let generics_with_empty = modify_types_generics_hack(&b_initial_generics, |args| {
                 args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
             });
             let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
@@ -603,8 +684,7 @@ Finally, call `.build()` to create the instance of `{name}`.
                 quote!(#[doc(hidden)])
             };
 
-            let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) =
-                b_generics.split_for_impl();
+            let (_, _, b_generics_where_extras_predicates) = b_generics.split_for_impl();
             let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
                 where TypedBuilderFields: Clone
             })?;
@@ -624,12 +704,39 @@ Finally, call `.build()` to create the instance of `{name}`.
                 false => quote! { true },
             };
 
+            let extend_fields = self.extend_fields().map(|f| {
+                let name = f.name;
+                let ty = f.ty;
+                quote!(#name: #ty)
+            });
+            let extend_fields_value = self.extend_fields().map(|f| {
+                let name = f.name;
+                quote!(#name: Vec::new())
+            });
+            let has_extend_fields = self.extend_fields().next().is_some();
+            let take_bump = if has_extend_fields {
+                quote!(bump: _cx.bump(),)
+            } else {
+                quote!()
+            };
+            let bump_field = if has_extend_fields {
+                quote!(bump: & #extend_lifetime ::dioxus::core::exports::bumpalo::Bump,)
+            } else {
+                quote!()
+            };
+            let extend_lifetime = extend_lifetime.unwrap_or(syn::Lifetime::new(
+                "'__bump",
+                proc_macro2::Span::call_site(),
+            ));
+
             Ok(quote! {
                 impl #impl_generics #name #ty_generics #where_clause {
                     #[doc = #builder_method_doc]
                     #[allow(dead_code, clippy::type_complexity)]
-                    #vis fn builder() -> #builder_name #generics_with_empty {
+                    #vis fn builder(_cx: & #extend_lifetime ::dioxus::prelude::ScopeState) -> #builder_name #generics_with_empty {
                         #builder_name {
+                            #(#extend_fields_value,)*
+                            #take_bump
                             fields: #empties_tuple,
                             _phantom: ::core::default::Default::default(),
                         }
@@ -640,26 +747,19 @@ Finally, call `.build()` to create the instance of `{name}`.
                 #builder_type_doc
                 #[allow(dead_code, non_camel_case_types, non_snake_case)]
                 #vis struct #builder_name #b_generics {
+                    #(#extend_fields,)*
+                    #bump_field
                     fields: #all_fields_param,
                     _phantom: (#( #phantom_generics ),*),
                 }
 
-                impl #b_generics_impl Clone for #builder_name #b_generics_ty #b_generics_where {
-                    fn clone(&self) -> Self {
-                        Self {
-                            fields: self.fields.clone(),
-                            _phantom: ::core::default::Default::default(),
-                        }
-                    }
-                }
-
-                impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
+                impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics
                 #b_generics_where_extras_predicates
                 {
                     type Builder = #builder_name #generics_with_empty;
                     const IS_STATIC: bool = #is_static;
-                    fn builder() -> Self::Builder {
-                        #name::builder()
+                    fn builder(_cx: &#extend_lifetime ::dioxus::prelude::ScopeState) -> Self::Builder {
+                        #name::builder(_cx)
                     }
                     unsafe fn memoize(&self, other: &Self) -> bool {
                         #can_memoize
@@ -694,11 +794,143 @@ Finally, call `.build()` to create the instance of `{name}`.
             })
         }
 
+        pub fn extends_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
+            let StructInfo {
+                ref builder_name, ..
+            } = *self;
+
+            let field_name = field.name;
+
+            let descructuring = self.included_fields().map(|f| {
+                if f.ordinal == field.ordinal {
+                    quote!(_)
+                } else {
+                    let name = f.name;
+                    quote!(#name)
+                }
+            });
+            let reconstructing = self.included_fields().map(|f| f.name);
+
+            // Add the bump lifetime to the generics
+            let mut ty_generics: Vec<syn::GenericArgument> = self
+                .generics
+                .params
+                .iter()
+                .map(|generic_param| match generic_param {
+                    syn::GenericParam::Type(type_param) => {
+                        let ident = type_param.ident.clone();
+                        syn::parse(quote!(#ident).into()).unwrap()
+                    }
+                    syn::GenericParam::Lifetime(lifetime_def) => {
+                        syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
+                    }
+                    syn::GenericParam::Const(const_param) => {
+                        let ident = const_param.ident.clone();
+                        syn::parse(quote!(#ident).into()).unwrap()
+                    }
+                })
+                .collect();
+            let mut target_generics_tuple = empty_type_tuple();
+            let mut ty_generics_tuple = empty_type_tuple();
+            let generics = self.modify_generics(|g| {
+                let index_after_lifetime_in_generics = g
+                    .params
+                    .iter()
+                    .filter(|arg| matches!(arg, syn::GenericParam::Lifetime(_)))
+                    .count();
+                for f in self.included_fields() {
+                    if f.ordinal == field.ordinal {
+                        ty_generics_tuple.elems.push_value(empty_type());
+                        target_generics_tuple
+                            .elems
+                            .push_value(f.tuplized_type_ty_param());
+                    } else {
+                        g.params
+                            .insert(index_after_lifetime_in_generics, f.generic_ty_param());
+                        let generic_argument: syn::Type = f.type_ident();
+                        ty_generics_tuple.elems.push_value(generic_argument.clone());
+                        target_generics_tuple.elems.push_value(generic_argument);
+                    }
+                    ty_generics_tuple.elems.push_punct(Default::default());
+                    target_generics_tuple.elems.push_punct(Default::default());
+                }
+            });
+            let mut target_generics = ty_generics.clone();
+            let index_after_lifetime_in_generics = target_generics
+                .iter()
+                .filter(|arg| matches!(arg, syn::GenericArgument::Lifetime(_)))
+                .count();
+            target_generics.insert(
+                index_after_lifetime_in_generics,
+                syn::GenericArgument::Type(target_generics_tuple.into()),
+            );
+            ty_generics.insert(
+                index_after_lifetime_in_generics,
+                syn::GenericArgument::Type(ty_generics_tuple.into()),
+            );
+            let (impl_generics, _, where_clause) = generics.split_for_impl();
+
+            let forward_extended_fields = self.extend_fields().map(|f| {
+                let name = f.name;
+                quote!(#name: self.#name)
+            });
+
+            let extend_lifetime = self.extend_lifetime()?.ok_or(Error::new_spanned(
+                field_name,
+                "Unable to find lifetime for extended field. Please specify it manually",
+            ))?;
+
+            let extends_impl = field.builder_attr.extends.iter().map(|path| {
+                let name_str = path_to_single_string(path).unwrap();
+                let camel_name = name_str.to_case(Case::UpperCamel);
+                let marker_name = Ident::new(
+                    format!("{}Extension", &camel_name).as_str(),
+                    path.span(),
+                );
+                quote! {
+                    #[allow(dead_code, non_camel_case_types, missing_docs)]
+                    impl #impl_generics dioxus_elements::extensions::#marker_name < #extend_lifetime > for #builder_name < #( #ty_generics ),* > #where_clause {}
+                }
+            });
+
+            Ok(quote! {
+                #[allow(dead_code, non_camel_case_types, missing_docs)]
+                impl #impl_generics ::dioxus::prelude::HasAttributes<#extend_lifetime> for #builder_name < #( #ty_generics ),* > #where_clause {
+                    fn push_attribute(
+                        mut self,
+                        name: &#extend_lifetime str,
+                        ns: Option<&'static str>,
+                        attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>,
+                        volatile: bool
+                    ) -> Self {
+                        let ( #(#descructuring,)* ) = self.fields;
+                        self.#field_name.push(
+                            ::dioxus::core::Attribute::new(
+                                name,
+                                {
+                                    use ::dioxus::prelude::IntoAttributeValue;
+                                    attr.into_value(self.bump)
+                                },
+                                ns,
+                                volatile,
+                            )
+                        );
+                        #builder_name {
+                            #(#forward_extended_fields,)*
+                            bump: self.bump,
+                            fields: ( #(#reconstructing,)* ),
+                            _phantom: self._phantom,
+                        }
+                    }
+                }
+
+                #(#extends_impl)*
+            })
+        }
+
         pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
             let FieldInfo {
-                name: field_name,
-                ty: field_type,
-                ..
+                name: field_name, ..
             } = field;
             if *field_name == "key" {
                 return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
@@ -717,6 +949,12 @@ Finally, call `.build()` to create the instance of `{name}`.
             });
             let reconstructing = self.included_fields().map(|f| f.name);
 
+            let FieldInfo {
+                name: field_name,
+                ty: field_type,
+                ..
+            } = field;
+            // Add the bump lifetime to the generics
             let mut ty_generics: Vec<syn::GenericArgument> = self
                 .generics
                 .params
@@ -800,6 +1038,16 @@ Finally, call `.build()` to create the instance of `{name}`.
             );
             let repeated_fields_error_message = format!("Repeated field {field_name}");
 
+            let forward_extended_fields = self.extend_fields().map(|f| {
+                let name = f.name;
+                quote!(#name: self.#name)
+            });
+            let forward_bump = if self.extend_fields().next().is_some() {
+                quote!(bump: self.bump,)
+            } else {
+                quote!()
+            };
+
             Ok(quote! {
                 #[allow(dead_code, non_camel_case_types, missing_docs)]
                 impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
@@ -809,6 +1057,8 @@ Finally, call `.build()` to create the instance of `{name}`.
                         let #field_name = (#arg_expr,);
                         let ( #(#descructuring,)* ) = self.fields;
                         #builder_name {
+                            #(#forward_extended_fields,)*
+                            #forward_bump
                             fields: ( #(#reconstructing,)* ),
                             _phantom: self._phantom,
                         }
@@ -842,6 +1092,7 @@ Finally, call `.build()` to create the instance of `{name}`.
                 name: ref field_name,
                 ..
             } = field;
+            // Add a bump lifetime to the generics
             let mut builder_generics: Vec<syn::GenericArgument> = self
                 .generics
                 .params
@@ -1009,7 +1260,9 @@ 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 let Some(ref default) = field.builder_attr.default {
+                if !field.builder_attr.extends.is_empty() {
+                    quote!(let #name = self.#name;)
+                } else if let Some(ref default) = field.builder_attr.default {
                     if field.builder_attr.skip {
                         quote!(let #name = #default;)
                     } else {

+ 24 - 7
packages/core/src/create.rs

@@ -1,6 +1,7 @@
 use crate::any_props::AnyProps;
 use crate::innerlude::{
-    BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText,
+    AttributeType, BorrowedAttributeValue, ElementPath, ElementRef, MountedAttribute, VComponent,
+    VPlaceholder, VText,
 };
 use crate::mutations::Mutation;
 use crate::mutations::Mutation::*;
@@ -314,7 +315,7 @@ impl<'b> VirtualDom {
             let id = self.assign_static_node_as_dynamic(path, root);
 
             loop {
-                self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id);
+                self.write_attribute_type(node, &node.dynamic_attrs[attr_id], attr_id, id);
 
                 // Only push the dynamic attributes forward if they match the current path (same element)
                 match attrs.next_if(|(_, p)| *p == path) {
@@ -325,25 +326,41 @@ impl<'b> VirtualDom {
         }
     }
 
-    fn write_attribute(
+    fn write_attribute_type(
         &mut self,
-        template: &'b VNode<'b>,
+        vnode: &'b VNode<'b>,
+        attribute: &'b MountedAttribute<'b>,
         idx: usize,
-        attribute: &'b crate::Attribute<'b>,
         id: ElementId,
     ) {
         // Make sure we set the attribute's associated id
         attribute.mounted_element.set(id);
+        match &attribute.ty {
+            AttributeType::Single(attribute) => self.write_attribute(vnode, attribute, idx, id),
+            AttributeType::Many(attribute) => {
+                for attribute in *attribute {
+                    self.write_attribute(vnode, attribute, idx, id);
+                }
+            }
+        }
+    }
 
+    pub(crate) fn write_attribute(
+        &mut self,
+        vnode: &'b VNode<'b>,
+        attribute: &'b crate::Attribute<'b>,
+        idx: usize,
+        id: ElementId,
+    ) {
         // Safety: we promise not to re-alias this text later on after committing it to the mutation
         let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };
 
         match &attribute.value {
             AttributeValue::Listener(_) => {
-                let path = &template.template.get().attr_paths[idx];
+                let path = &vnode.template.get().attr_paths[idx];
                 let element_ref = ElementRef {
                     path: ElementPath { path },
-                    template: template.stable_id().unwrap(),
+                    template: vnode.stable_id().unwrap(),
                     scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
                 };
                 self.elements[id.0] = Some(element_ref);

+ 79 - 10
packages/core/src/diff.rs

@@ -2,8 +2,8 @@ use crate::{
     any_props::AnyProps,
     arena::ElementId,
     innerlude::{
-        BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder,
-        VText,
+        AttributeType, BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent,
+        VPlaceholder, VText,
     },
     mutations::Mutation,
     nodes::RenderReturn,
@@ -126,14 +126,54 @@ impl<'b> VirtualDom {
             .dynamic_attrs
             .iter()
             .zip(right_template.dynamic_attrs.iter())
-            .for_each(|(left_attr, right_attr)| {
+            .enumerate()
+            .for_each(|(idx, (left_attr, right_attr))| {
                 // Move over the ID from the old to the new
-                let mounted_element = left_attr.mounted_element.get();
-                right_attr.mounted_element.set(mounted_element);
+                let mounted_id = left_attr.mounted_element.get();
+                right_attr.mounted_element.set(mounted_id);
 
-                // If the attributes are different (or volatile), we need to update them
-                if left_attr.value != right_attr.value || left_attr.volatile {
-                    self.update_attribute(right_attr, left_attr);
+                match (&left_attr.ty, &right_attr.ty) {
+                    (AttributeType::Single(left), AttributeType::Single(right)) => {
+                        self.diff_attribute(left, right, mounted_id)
+                    }
+                    (AttributeType::Many(left), AttributeType::Many(right)) => {
+                        let mut left_iter = left.iter().peekable();
+                        let mut right_iter = right.iter().peekable();
+
+                        loop {
+                            match (left_iter.peek(), right_iter.peek()) {
+                                (Some(left), Some(right)) => {
+                                    // check which name is greater
+                                    match left.name.cmp(right.name) {
+                                        std::cmp::Ordering::Less => self.remove_attribute(
+                                            left.name,
+                                            left.namespace,
+                                            mounted_id,
+                                        ),
+                                        std::cmp::Ordering::Greater => self.write_attribute(
+                                            right_template,
+                                            right,
+                                            idx,
+                                            mounted_id,
+                                        ),
+                                        std::cmp::Ordering::Equal => {
+                                            self.diff_attribute(left, right, mounted_id)
+                                        }
+                                    }
+                                }
+                                (Some(_), None) => {
+                                    let left = left_iter.next().unwrap();
+                                    self.remove_attribute(left.name, left.namespace, mounted_id)
+                                }
+                                (None, Some(_)) => {
+                                    let right = right_iter.next().unwrap();
+                                    self.write_attribute(right_template, right, idx, mounted_id)
+                                }
+                                (None, None) => break,
+                            }
+                        }
+                    }
+                    _ => unreachable!("The macro should never generate this case"),
                 }
             });
 
@@ -164,6 +204,18 @@ impl<'b> VirtualDom {
         }
     }
 
+    fn diff_attribute(
+        &mut self,
+        left_attr: &'b Attribute<'b>,
+        right_attr: &'b Attribute<'b>,
+        id: ElementId,
+    ) {
+        // If the attributes are different (or volatile), we need to update them
+        if left_attr.value != right_attr.value || left_attr.volatile {
+            self.update_attribute(right_attr, left_attr, id);
+        }
+    }
+
     fn diff_dynamic_node(
         &mut self,
         left_node: &'b DynamicNode<'b>,
@@ -184,12 +236,29 @@ impl<'b> VirtualDom {
         };
     }
 
-    fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) {
+    fn remove_attribute(&mut self, name: &'b str, ns: Option<&'static str>, id: ElementId) {
+        let name = unsafe { std::mem::transmute(name) };
+        let value: BorrowedAttributeValue<'b> = BorrowedAttributeValue::None;
+        let value = unsafe { std::mem::transmute(value) };
+        self.mutations.push(Mutation::SetAttribute {
+            id,
+            ns,
+            name,
+            value,
+        });
+    }
+
+    fn update_attribute(
+        &mut self,
+        right_attr: &'b Attribute<'b>,
+        left_attr: &'b Attribute<'b>,
+        id: ElementId,
+    ) {
         let name = unsafe { std::mem::transmute(left_attr.name) };
         let value: BorrowedAttributeValue<'b> = (&right_attr.value).into();
         let value = unsafe { std::mem::transmute(value) };
         self.mutations.push(Mutation::SetAttribute {
-            id: left_attr.mounted_element.get(),
+            id,
             ns: right_attr.namespace,
             name,
             value,

+ 2 - 2
packages/core/src/error_boundary.rs

@@ -334,10 +334,10 @@ where
         }
     }
 }
-impl<'a> Properties for ErrorBoundaryProps<'a> {
+impl<'a> Properties<'a> for ErrorBoundaryProps<'a> {
     type Builder = ErrorBoundaryPropsBuilder<'a, ((), ())>;
     const IS_STATIC: bool = false;
-    fn builder() -> Self::Builder {
+    fn builder(_: &'a ScopeState) -> Self::Builder {
         ErrorBoundaryProps::builder()
     }
     unsafe fn memoize(&self, _: &Self) -> bool {

+ 2 - 2
packages/core/src/fragment.rs

@@ -92,10 +92,10 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> {
 ///     })
 /// }
 /// ```
-impl<'a> Properties for FragmentProps<'a> {
+impl<'a> Properties<'_> for FragmentProps<'a> {
     type Builder = FragmentBuilder<'a, false>;
     const IS_STATIC: bool = false;
-    fn builder() -> Self::Builder {
+    fn builder(_cx: &ScopeState) -> Self::Builder {
         FragmentBuilder(None)
     }
     unsafe fn memoize(&self, _other: &Self) -> bool {

+ 9 - 9
packages/core/src/lib.rs

@@ -76,11 +76,11 @@ pub(crate) mod innerlude {
 }
 
 pub use crate::innerlude::{
-    fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
-    CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode,
-    LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
-    TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
-    VirtualDom,
+    fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue,
+    BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event,
+    Fragment, HasAttributes, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations,
+    Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
+    TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
 };
 
 /// The purpose of this module is to alleviate imports of many common types
@@ -91,10 +91,10 @@ pub mod prelude {
         consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
         provide_context, provide_context_to_scope, provide_root_context, push_future,
         remove_future, schedule_update_any, spawn, spawn_forever, suspend, use_error_boundary,
-        AnyValue, Component, Element, ErrorBoundary, Event, EventHandler, Fragment,
-        IntoAttributeValue, IntoDynNode, LazyNodes, Properties, Runtime, RuntimeGuard, Scope,
-        ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw,
-        VNode, VirtualDom,
+        AnyValue, Attribute, AttributeType, Component, Element, ErrorBoundary, Event, EventHandler,
+        Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, LazyNodes, MountedAttribute,
+        Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
+        TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
     };
 }
 

+ 90 - 9
packages/core/src/nodes.rs

@@ -64,7 +64,7 @@ pub struct VNode<'a> {
     pub dynamic_nodes: &'a [DynamicNode<'a>],
 
     /// The dynamic parts of the template
-    pub dynamic_attrs: &'a [Attribute<'a>],
+    pub dynamic_attrs: &'a [MountedAttribute<'a>],
 }
 
 impl<'a> VNode<'a> {
@@ -92,7 +92,7 @@ impl<'a> VNode<'a> {
         template: Template<'static>,
         root_ids: bumpalo::collections::Vec<'a, ElementId>,
         dynamic_nodes: &'a [DynamicNode<'a>],
-        dynamic_attrs: &'a [Attribute<'a>],
+        dynamic_attrs: &'a [MountedAttribute<'a>],
     ) -> Self {
         Self {
             key,
@@ -445,6 +445,51 @@ pub enum TemplateAttribute<'a> {
     },
 }
 
+/// An attribute with information about its position in the DOM and the element it was mounted to
+#[derive(Debug)]
+pub struct MountedAttribute<'a> {
+    pub(crate) ty: AttributeType<'a>,
+
+    /// The element in the DOM that this attribute belongs to
+    pub(crate) mounted_element: Cell<ElementId>,
+}
+
+impl<'a> From<Attribute<'a>> for MountedAttribute<'a> {
+    fn from(attr: Attribute<'a>) -> Self {
+        Self {
+            ty: AttributeType::Single(attr),
+            mounted_element: Default::default(),
+        }
+    }
+}
+
+impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> {
+    fn from(attr: &'a [Attribute<'a>]) -> Self {
+        Self {
+            ty: AttributeType::Many(attr),
+            mounted_element: Default::default(),
+        }
+    }
+}
+
+impl<'a> From<&'a Vec<Attribute<'a>>> for MountedAttribute<'a> {
+    fn from(attr: &'a Vec<Attribute<'a>>) -> Self {
+        attr.as_slice().into()
+    }
+}
+
+impl<'a> MountedAttribute<'a> {
+    /// Get the type of this attribute
+    pub fn attribute_type(&self) -> &AttributeType<'a> {
+        &self.ty
+    }
+
+    /// Get the element that this attribute is mounted to
+    pub fn mounted_element(&self) -> ElementId {
+        self.mounted_element.get()
+    }
+}
+
 /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
 #[derive(Debug)]
 pub struct Attribute<'a> {
@@ -461,9 +506,6 @@ pub struct Attribute<'a> {
 
     /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
     pub volatile: bool,
-
-    /// The element in the DOM that this attribute belongs to
-    pub(crate) mounted_element: Cell<ElementId>,
 }
 
 impl<'a> Attribute<'a> {
@@ -479,13 +521,40 @@ impl<'a> Attribute<'a> {
             value,
             namespace,
             volatile,
-            mounted_element: Cell::new(ElementId::default()),
         }
     }
+}
 
-    /// Get the element that this attribute is mounted to
-    pub fn mounted_element(&self) -> ElementId {
-        self.mounted_element.get()
+/// The type of an attribute
+#[derive(Debug)]
+pub enum AttributeType<'a> {
+    /// A single attribute
+    Single(Attribute<'a>),
+    /// Many different attributes sorted by name
+    Many(&'a [Attribute<'a>]),
+}
+
+impl<'a> AttributeType<'a> {
+    /// Call the given function on each attribute
+    pub fn for_each<'b, F>(&'b self, mut f: F)
+    where
+        F: FnMut(&'b Attribute<'a>),
+    {
+        match self {
+            Self::Single(attr) => f(attr),
+            Self::Many(attrs) => attrs.iter().for_each(f),
+        }
+    }
+
+    /// Try to call the given function on each attribute
+    pub fn try_for_each<'b, F, E>(&'b self, mut f: F) -> Result<(), E>
+    where
+        F: FnMut(&'b Attribute<'a>) -> Result<(), E>,
+    {
+        match self {
+            Self::Single(attr) => f(attr),
+            Self::Many(attrs) => attrs.iter().try_for_each(f),
+        }
     }
 }
 
@@ -870,3 +939,15 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option<T> {
         }
     }
 }
+
+/// A trait for anything that has a dynamic list of attributes
+pub trait HasAttributes<'a> {
+    /// Push an attribute onto the list of attributes
+    fn push_attribute(
+        self,
+        name: &'a str,
+        ns: Option<&'static str>,
+        attr: impl IntoAttributeValue<'a>,
+        volatile: bool,
+    ) -> Self;
+}

+ 9 - 6
packages/core/src/properties.rs

@@ -32,7 +32,7 @@ use crate::innerlude::*;
 ///     data: &'a str
 /// }
 /// ```
-pub trait Properties: Sized {
+pub trait Properties<'a>: Sized {
     /// The type of the builder for this component.
     /// Used to create "in-progress" versions of the props.
     type Builder;
@@ -41,7 +41,7 @@ pub trait Properties: Sized {
     const IS_STATIC: bool;
 
     /// Create a builder for this component.
-    fn builder() -> Self::Builder;
+    fn builder(cx: &'a ScopeState) -> Self::Builder;
 
     /// Memoization can only happen if the props are valid for the 'static lifetime
     ///
@@ -51,10 +51,10 @@ pub trait Properties: Sized {
     unsafe fn memoize(&self, other: &Self) -> bool;
 }
 
-impl Properties for () {
+impl Properties<'_> for () {
     type Builder = EmptyBuilder;
     const IS_STATIC: bool = true;
-    fn builder() -> Self::Builder {
+    fn builder(_cx: &ScopeState) -> Self::Builder {
         EmptyBuilder {}
     }
     unsafe fn memoize(&self, _other: &Self) -> bool {
@@ -70,8 +70,11 @@ impl EmptyBuilder {
 
 /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
 /// to initialize a component's props.
-pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
-    T::builder()
+pub fn fc_to_builder<'a, T: Properties<'a> + 'a>(
+    cx: &'a ScopeState,
+    _: fn(Scope<'a, T>) -> Element<'a>,
+) -> T::Builder {
+    T::builder(cx)
 }
 
 #[cfg(not(miri))]

+ 26 - 21
packages/core/src/scopes.rs

@@ -7,7 +7,8 @@ use crate::{
     nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
     runtime::Runtime,
     scope_context::ScopeContext,
-    AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
+    AnyValue, Attribute, AttributeType, AttributeValue, Element, Event, MountedAttribute,
+    Properties, TaskId,
 };
 use bumpalo::{boxed::Box as BumpBox, Bump};
 use std::{
@@ -350,20 +351,22 @@ impl<'src> ScopeState {
 
         let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
         for attr in element.dynamic_attrs {
-            match attr.value {
-                // We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
-                AttributeValue::Listener(_) => {
-                    let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
-                    listeners.push(unbounded);
+            attr.ty.for_each(|attr| {
+                match attr.value {
+                    // We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
+                    AttributeValue::Listener(_) => {
+                        let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
+                        listeners.push(unbounded);
+                    }
+                    // We need to drop any values manually to make sure that their drop implementation is called before the next render
+                    AttributeValue::Any(_) => {
+                        let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
+                        self.previous_frame().add_attribute_to_drop(unbounded);
+                    }
+
+                    _ => (),
                 }
-                // We need to drop any values manually to make sure that their drop implementation is called before the next render
-                AttributeValue::Any(_) => {
-                    let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
-                    self.previous_frame().add_attribute_to_drop(unbounded);
-                }
-
-                _ => (),
-            }
+            })
         }
 
         let mut props = self.borrowed_props.borrow_mut();
@@ -419,13 +422,15 @@ impl<'src> ScopeState {
         value: impl IntoAttributeValue<'src>,
         namespace: Option<&'static str>,
         volatile: bool,
-    ) -> Attribute<'src> {
-        Attribute {
-            name,
-            namespace,
-            volatile,
+    ) -> MountedAttribute<'src> {
+        MountedAttribute {
+            ty: AttributeType::Single(Attribute {
+                name,
+                namespace,
+                volatile,
+                value: value.into_value(self.bump()),
+            }),
             mounted_element: Default::default(),
-            value: value.into_value(self.bump()),
         }
     }
 
@@ -451,7 +456,7 @@ impl<'src> ScopeState {
     ) -> DynamicNode<'src>
     where
         // The properties must be valid until the next bump frame
-        P: Properties + 'src,
+        P: Properties<'src> + 'src,
         // The current bump allocator frame must outlive the child's borrowed props
         'src: 'child,
     {

+ 42 - 31
packages/core/src/virtual_dom.rs

@@ -4,8 +4,8 @@
 
 use crate::{
     any_props::VProps,
-    arena::ElementId,
-    innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
+    arena::{ElementId, ElementRef},
+    innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
     mutations::Mutation,
     nodes::RenderReturn,
     nodes::{Template, TemplateId},
@@ -371,7 +371,6 @@ impl VirtualDom {
             .get(parent_path.template.0)
             .cloned()
             .map(|el| (*parent_path, el));
-        let mut listeners = vec![];
 
         // We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
         let uievent = Event {
@@ -383,6 +382,8 @@ impl VirtualDom {
         if bubbles {
             // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
             while let Some((path, el_ref)) = parent_node {
+                let mut listeners = vec![];
+
                 // safety: we maintain references of all vnodes in the element slab
                 let template = unsafe { el_ref.unwrap().as_ref() };
                 let node_template = template.template.get();
@@ -392,10 +393,14 @@ impl VirtualDom {
                     let this_path = node_template.attr_paths[idx];
 
                     // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
-                    if attr.name.trim_start_matches("on") == name
-                        && target_path.is_decendant(&this_path)
-                    {
-                        listeners.push(&attr.value);
+                    if target_path.is_decendant(&this_path) {
+                        attr.ty.for_each(|attribute| {
+                            if attribute.name.trim_start_matches("on") == name {
+                                if let AttributeValue::Listener(listener) = &attribute.value {
+                                    listeners.push(listener);
+                                }
+                            }
+                        });
 
                         // Break if this is the exact target element.
                         // This means we won't call two listeners with the same name on the same element. This should be
@@ -408,20 +413,18 @@ impl VirtualDom {
 
                 // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
                 // We check the bubble state between each call to see if the event has been stopped from bubbling
-                for listener in listeners.drain(..).rev() {
-                    if let AttributeValue::Listener(listener) = listener {
-                        let origin = path.scope;
-                        self.runtime.scope_stack.borrow_mut().push(origin);
-                        self.runtime.rendering.set(false);
-                        if let Some(cb) = listener.borrow_mut().as_deref_mut() {
-                            cb(uievent.clone());
-                        }
-                        self.runtime.scope_stack.borrow_mut().pop();
-                        self.runtime.rendering.set(true);
+                for listener in listeners.into_iter().rev() {
+                    let origin = path.scope;
+                    self.runtime.scope_stack.borrow_mut().push(origin);
+                    self.runtime.rendering.set(false);
+                    if let Some(cb) = listener.borrow_mut().as_deref_mut() {
+                        cb(uievent.clone());
+                    }
+                    self.runtime.scope_stack.borrow_mut().pop();
+                    self.runtime.rendering.set(true);
 
-                        if !uievent.propagates.get() {
-                            return;
-                        }
+                    if !uievent.propagates.get() {
+                        return;
                     }
                 }
 
@@ -445,18 +448,26 @@ impl VirtualDom {
 
                     // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
                     // Only call the listener if this is the exact target element.
-                    if attr.name.trim_start_matches("on") == name && target_path == this_path {
-                        if let AttributeValue::Listener(listener) = &attr.value {
-                            let origin = path.scope;
-                            self.runtime.scope_stack.borrow_mut().push(origin);
-                            self.runtime.rendering.set(false);
-                            if let Some(cb) = listener.borrow_mut().as_deref_mut() {
-                                cb(uievent.clone());
+                    if target_path == this_path {
+                        let mut should_stop = false;
+                        attr.ty.for_each(|attribute| {
+                            if attribute.name.trim_start_matches("on") == name {
+                                if let AttributeValue::Listener(listener) = &attribute.value {
+                                    let origin = path.scope;
+                                    self.runtime.scope_stack.borrow_mut().push(origin);
+                                    self.runtime.rendering.set(false);
+                                    if let Some(cb) = listener.borrow_mut().as_deref_mut() {
+                                        cb(uievent.clone());
+                                    }
+                                    self.runtime.scope_stack.borrow_mut().pop();
+                                    self.runtime.rendering.set(true);
+
+                                    should_stop = true;
+                                }
                             }
-                            self.runtime.scope_stack.borrow_mut().pop();
-                            self.runtime.rendering.set(true);
-
-                            break;
+                        });
+                        if should_stop {
+                            return;
                         }
                     }
                 }

+ 4 - 3
packages/core/tests/fuzzing.rs

@@ -1,7 +1,7 @@
 #![cfg(not(miri))]
 
 use dioxus::prelude::Props;
-use dioxus_core::*;
+use dioxus_core::{MountedAttribute, *};
 use std::{cfg, collections::HashSet};
 
 fn random_ns() -> Option<&'static str> {
@@ -206,7 +206,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
     }
 }
 
-fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
+fn create_random_dynamic_attr(cx: &ScopeState) -> MountedAttribute {
     let value = match rand::random::<u8>() % 7 {
         0 => AttributeValue::Text(Box::leak(
             format!("{}", rand::random::<usize>()).into_boxed_str(),
@@ -218,7 +218,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
         5 => AttributeValue::None,
         6 => {
             let value = cx.listener(|e: Event<String>| println!("{:?}", e));
-            return Attribute::new("ondata", value, None, false);
+            return Attribute::new("ondata", value, None, false).into();
         }
         _ => unreachable!(),
     };
@@ -228,6 +228,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
         random_ns(),
         rand::random(),
     )
+    .into()
 }
 
 static mut TEMPLATE_COUNT: usize = 0;

+ 28 - 0
packages/html-internal-macro/Cargo.toml

@@ -0,0 +1,28 @@
+[package]
+name = "dioxus-html-internal-macro"
+version = { workspace = true }
+edition = "2021"
+repository = "https://github.com/DioxusLabs/dioxus/"
+homepage = "https://dioxuslabs.com"
+keywords = ["dom", "ui", "gui", "react", "liveview"]
+license = "MIT OR Apache-2.0"
+description = "HTML function macros for Dioxus"
+
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+proc-macro2 = "1.0.66"
+syn = { version = "2", features = ["full"] }
+quote = "^1.0.26"
+convert_case = "^0.6.0"
+
+[lib]
+proc-macro = true
+
+[[test]]
+name = "tests"
+path = "tests/progress.rs"
+
+[dev-dependencies]
+trybuild = { version = "1.0.82", features = ["diff"] }

+ 76 - 0
packages/html-internal-macro/src/lib.rs

@@ -0,0 +1,76 @@
+use proc_macro::TokenStream;
+
+use convert_case::{Case, Casing};
+use quote::{quote, ToTokens, TokenStreamExt};
+use syn::__private::TokenStream2;
+use syn::parse::{Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::{braced, parse_macro_input, Ident, Token};
+
+#[proc_macro]
+pub fn impl_extension_attributes(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as ImplExtensionAttributes);
+    input.to_token_stream().into()
+}
+
+struct ImplExtensionAttributes {
+    is_element: bool,
+    name: Ident,
+    attrs: Punctuated<Ident, Token![,]>,
+}
+
+impl Parse for ImplExtensionAttributes {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let content;
+
+        let element: Ident = input.parse()?;
+        let name = input.parse()?;
+        braced!(content in input);
+        let attrs = content.parse_terminated(Ident::parse, Token![,])?;
+
+        Ok(ImplExtensionAttributes {
+            is_element: element == "ELEMENT",
+            name,
+            attrs,
+        })
+    }
+}
+
+impl ToTokens for ImplExtensionAttributes {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let name = &self.name;
+        let name_string = name.to_string();
+        let camel_name = name_string
+            .strip_prefix("r#")
+            .unwrap_or(&name_string)
+            .to_case(Case::UpperCamel);
+        let impl_name = Ident::new(format!("{}Impl", &camel_name).as_str(), name.span());
+        let extension_name = Ident::new(format!("{}Extension", &camel_name).as_str(), name.span());
+
+        if !self.is_element {
+            tokens.append_all(quote! {
+                struct #impl_name;
+                impl #name for #impl_name {}
+            });
+        }
+
+        let impls = self.attrs.iter().map(|ident| {
+            let d = if self.is_element {
+                quote! { #name::#ident }
+            } else {
+                quote! { <#impl_name as #name>::#ident }
+            };
+            quote! {
+                fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self {
+                    let d = #d;
+                    self.push_attribute(d.0, d.1, value, d.2)
+                }
+            }
+        });
+        tokens.append_all(quote! {
+            pub trait #extension_name<'a>: HasAttributes<'a> + Sized {
+                #(#impls)*
+            }
+        });
+    }
+}

+ 1 - 0
packages/html-internal-macro/tests/01-simple.rs

@@ -0,0 +1 @@
+fn main() {}

+ 5 - 0
packages/html-internal-macro/tests/progress.rs

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

+ 1 - 0
packages/html/Cargo.toml

@@ -12,6 +12,7 @@ keywords = ["dom", "ui", "gui", "react"]
 [dependencies]
 dioxus-core = { workspace = true }
 dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true }
+dioxus-html-internal-macro = { workspace = true }
 serde = { version = "1", features = ["derive"], optional = true }
 serde_repr = { version = "0.1", optional = true }
 wasm-bindgen = { workspace = true, optional = true }

+ 14 - 2
packages/html/src/elements.rs

@@ -1,9 +1,14 @@
 #![allow(non_upper_case_globals)]
+
+use dioxus_core::prelude::IntoAttributeValue;
+use dioxus_core::HasAttributes;
+use dioxus_html_internal_macro::impl_extension_attributes;
+#[cfg(feature = "hot-reload-context")]
+use dioxus_rsx::HotReloadingContext;
+
 #[cfg(feature = "hot-reload-context")]
 use crate::{map_global_attributes, map_svg_attributes};
 use crate::{GlobalAttributes, SvgAttributes};
-#[cfg(feature = "hot-reload-context")]
-use dioxus_rsx::HotReloadingContext;
 
 pub type AttributeDiscription = (&'static str, Option<&'static str>, bool);
 
@@ -378,6 +383,13 @@ macro_rules! builder_constructors {
                 }
             );
         )*
+
+        pub(crate) mod extensions {
+            use super::*;
+            $(
+                impl_extension_attributes![ELEMENT $name { $($fil,)* }];
+            )*
+        }
     };
 }
 

+ 2 - 2
packages/html/src/events.rs

@@ -12,7 +12,7 @@ macro_rules! impl_event {
         $(
             $( #[$attr] )*
             #[inline]
-            pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::Attribute<'a> {
+            pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::MountedAttribute<'a> {
                 ::dioxus_core::Attribute::new(
                     impl_event!(@name $name $($js_name)?),
                     _cx.listener(move |e: ::dioxus_core::Event<crate::PlatformEventData>| {
@@ -20,7 +20,7 @@ macro_rules! impl_event {
                     }),
                     None,
                     false,
-                )
+                ).into()
             }
         )*
     };

+ 17 - 1
packages/html/src/events/mouse.rs

@@ -78,7 +78,7 @@ impl_event! {
     #[deprecated(since = "0.5.0", note = "use ondoubleclick instead")]
     ondblclick
 
-    ondoubleclick: "ondblclick"
+//     ondoubleclick: "ondblclick"
 
     /// onmousedown
     onmousedown
@@ -104,6 +104,22 @@ impl_event! {
     onmouseup
 }
 
+#[inline]
+pub fn ondoubleclick<'a, E: crate::EventReturn<T>, T>(
+    _cx: &'a ::dioxus_core::ScopeState,
+    mut _f: impl FnMut(::dioxus_core::Event<MouseData>) -> E + 'a,
+) -> ::dioxus_core::MountedAttribute<'a> {
+    ::dioxus_core::Attribute::new(
+        "ondblclick",
+        _cx.listener(move |e: ::dioxus_core::Event<MouseData>| {
+            _f(e).spawn(_cx);
+        }),
+        None,
+        false,
+    )
+    .into()
+}
+
 impl MouseData {
     /// Create a new instance of MouseData
     pub fn new(inner: impl HasMouseData + 'static) -> Self {

+ 6 - 0
packages/html/src/global_attributes.rs

@@ -1,5 +1,9 @@
 #![allow(non_upper_case_globals)]
 
+use dioxus_core::prelude::IntoAttributeValue;
+use dioxus_core::HasAttributes;
+use dioxus_html_internal_macro::impl_extension_attributes;
+
 use crate::AttributeDiscription;
 
 #[cfg(feature = "hot-reload-context")]
@@ -106,6 +110,8 @@ macro_rules! trait_methods {
             )*
             None
         }
+
+        impl_extension_attributes![GLOBAL $trait { $($name,)* }];
     };
 
     // Rename the incoming ident and apply a custom namespace

+ 7 - 0
packages/html/src/lib.rs

@@ -46,10 +46,17 @@ pub use render_template::*;
 #[cfg(feature = "eval")]
 pub mod eval;
 
+pub mod extensions {
+    pub use crate::elements::extensions::*;
+    pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension};
+}
+
 pub mod prelude {
+    pub use crate::elements::extensions::*;
     #[cfg(feature = "eval")]
     pub use crate::eval::*;
     pub use crate::events::*;
+    pub use crate::global_attributes::{GlobalAttributesExtension, SvgAttributesExtension};
     pub use crate::point_interaction::*;
     pub use keyboard_types::{self, Code, Key, Location, Modifiers};
 }

+ 21 - 6
packages/native-core/tests/fuzzing.rs

@@ -204,6 +204,20 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
     }
 }
 
+fn create_random_dynamic_mounted_attr(cx: &ScopeState) -> MountedAttribute {
+    match rand::random::<u8>() % 2 {
+        0 => MountedAttribute::from(
+            &*cx.bump().alloc(
+                (0..(rand::random::<u8>() % 3) as usize)
+                    .map(|_| create_random_dynamic_attr(cx))
+                    .collect::<Vec<_>>(),
+            ),
+        ),
+        1 => MountedAttribute::from(create_random_dynamic_attr(cx)),
+        _ => unreachable!(),
+    }
+}
+
 fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
     let value = match rand::random::<u8>() % 6 {
         0 => AttributeValue::Text(Box::leak(
@@ -214,7 +228,6 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
         3 => AttributeValue::Bool(rand::random()),
         4 => cx.any_value(rand::random::<usize>()),
         5 => AttributeValue::None,
-        // Listener(RefCell<Option<ListenerCb<'a>>>),
         _ => unreachable!(),
     };
     Attribute::new(
@@ -271,11 +284,13 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
                         .collect();
                     cx.bump().alloc(dynamic_nodes)
                 },
-                cx.bump().alloc(
-                    (0..template.attr_paths.len())
-                        .map(|_| create_random_dynamic_attr(cx))
-                        .collect::<Vec<_>>(),
-                ),
+                cx.bump()
+                    .alloc(
+                        (0..template.attr_paths.len())
+                            .map(|_| create_random_dynamic_mounted_attr(cx))
+                            .collect::<Vec<_>>(),
+                    )
+                    .as_slice(),
             );
             Some(node)
         }

+ 8 - 7
packages/rsx-rosetta/src/lib.rs

@@ -5,7 +5,8 @@
 use convert_case::{Case, Casing};
 use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx};
 use dioxus_rsx::{
-    BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
+    AttributeType, BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed,
+    ElementName, IfmtInput,
 };
 pub use html_parser::{Dom, Node};
 use proc_macro2::{Ident, Span};
@@ -63,16 +64,16 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                         }
                     };
 
-                    ElementAttrNamed {
+                    AttributeType::Named(ElementAttrNamed {
                         el_name: el_name.clone(),
                         attr,
-                    }
+                    })
                 })
                 .collect();
 
             let class = el.classes.join(" ");
             if !class.is_empty() {
-                attributes.push(ElementAttrNamed {
+                attributes.push(AttributeType::Named(ElementAttrNamed {
                     el_name: el_name.clone(),
                     attr: ElementAttr {
                         name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
@@ -81,11 +82,11 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                         )),
                         value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)),
                     },
-                });
+                }));
             }
 
             if let Some(id) = &el.id {
-                attributes.push(ElementAttrNamed {
+                attributes.push(AttributeType::Named(ElementAttrNamed {
                     el_name: el_name.clone(),
                     attr: ElementAttr {
                         name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
@@ -94,7 +95,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                         )),
                         value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)),
                     },
-                });
+                }));
             }
 
             let children = el.children.iter().filter_map(rsx_node_from_html).collect();

+ 46 - 1
packages/rsx/src/attribute.rs

@@ -4,14 +4,59 @@ use super::*;
 
 use proc_macro2::{Span, TokenStream as TokenStream2};
 use quote::{quote, ToTokens, TokenStreamExt};
-use syn::{parse_quote, Expr, ExprIf, Ident, LitStr};
+use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr};
 
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
+pub enum AttributeType {
+    Named(ElementAttrNamed),
+    Spread(Expr),
+}
+
+impl AttributeType {
+    pub fn start(&self) -> Span {
+        match self {
+            AttributeType::Named(n) => n.attr.start(),
+            AttributeType::Spread(e) => e.span(),
+        }
+    }
+
+    pub(crate) fn try_combine(&self, other: &Self) -> Option<Self> {
+        match (self, other) {
+            (Self::Named(a), Self::Named(b)) => a.try_combine(b).map(Self::Named),
+            _ => None,
+        }
+    }
+}
+
+impl ToTokens for AttributeType {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match self {
+            AttributeType::Named(n) => tokens.append_all(quote! { #n }),
+            AttributeType::Spread(e) => tokens.append_all(quote! { (&#e).into() }),
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
 pub struct ElementAttrNamed {
     pub el_name: ElementName,
     pub attr: ElementAttr,
 }
 
+impl Hash for ElementAttrNamed {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.attr.name.hash(state);
+    }
+}
+
+impl PartialEq for ElementAttrNamed {
+    fn eq(&self, other: &Self) -> bool {
+        self.attr == other.attr
+    }
+}
+
+impl Eq for ElementAttrNamed {}
+
 impl ElementAttrNamed {
     pub(crate) fn try_combine(&self, other: &Self) -> Option<Self> {
         if self.el_name == other.el_name && self.attr.name == other.attr.name {

+ 2 - 2
packages/rsx/src/component.rs

@@ -151,8 +151,8 @@ impl ToTokens for Component {
             }
             None => {
                 let mut toks = match prop_gen_args {
-                    Some(gen_args) => quote! { fc_to_builder(#name #gen_args) },
-                    None => quote! { fc_to_builder(#name) },
+                    Some(gen_args) => quote! { fc_to_builder(__cx, #name #gen_args) },
+                    None => quote! { fc_to_builder(__cx, #name) },
                 };
                 for field in &self.fields {
                     match field.name.to_string().as_str() {

+ 69 - 24
packages/rsx/src/element.rs

@@ -8,7 +8,7 @@ use syn::{
     parse::{Parse, ParseBuffer, ParseStream},
     punctuated::Punctuated,
     spanned::Spanned,
-    Ident, LitStr, Result, Token,
+    Expr, Ident, LitStr, Result, Token,
 };
 
 // =======================================
@@ -18,8 +18,8 @@ use syn::{
 pub struct Element {
     pub name: ElementName,
     pub key: Option<IfmtInput>,
-    pub attributes: Vec<ElementAttrNamed>,
-    pub merged_attributes: Vec<ElementAttrNamed>,
+    pub attributes: Vec<AttributeType>,
+    pub merged_attributes: Vec<AttributeType>,
     pub children: Vec<BodyNode>,
     pub brace: syn::token::Brace,
 }
@@ -32,7 +32,7 @@ impl Parse for Element {
         let content: ParseBuffer;
         let brace = syn::braced!(content in stream);
 
-        let mut attributes: Vec<ElementAttrNamed> = vec![];
+        let mut attributes: Vec<AttributeType> = vec![];
         let mut children: Vec<BodyNode> = vec![];
         let mut key = None;
 
@@ -42,6 +42,22 @@ impl Parse for Element {
         // "def": 456,
         // abc: 123,
         loop {
+            if content.peek(Token![..]) {
+                content.parse::<Token![..]>()?;
+                let expr = content.parse::<Expr>()?;
+                let span = expr.span();
+                attributes.push(attribute::AttributeType::Spread(expr));
+
+                if content.is_empty() {
+                    break;
+                }
+
+                if content.parse::<Token![,]>().is_err() {
+                    missing_trailing_comma!(span);
+                }
+                continue;
+            }
+
             // Parse the raw literal fields
             if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
                 let name = content.parse::<LitStr>()?;
@@ -50,13 +66,13 @@ impl Parse for Element {
                 content.parse::<Token![:]>()?;
 
                 let value = content.parse::<ElementAttrValue>()?;
-                attributes.push(ElementAttrNamed {
+                attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
                     el_name: el_name.clone(),
                     attr: ElementAttr {
                         name: ElementAttrName::Custom(name),
                         value,
                     },
-                });
+                }));
 
                 if content.is_empty() {
                     break;
@@ -79,13 +95,13 @@ impl Parse for Element {
                 let span = content.span();
 
                 if name_str.starts_with("on") {
-                    attributes.push(ElementAttrNamed {
+                    attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
                         el_name: el_name.clone(),
                         attr: ElementAttr {
                             name: ElementAttrName::BuiltIn(name),
                             value: ElementAttrValue::EventTokens(content.parse()?),
                         },
-                    });
+                    }));
                 } else {
                     match name_str.as_str() {
                         "key" => {
@@ -93,13 +109,13 @@ impl Parse for Element {
                         }
                         _ => {
                             let value = content.parse::<ElementAttrValue>()?;
-                            attributes.push(ElementAttrNamed {
+                            attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
                                 el_name: el_name.clone(),
                                 attr: ElementAttr {
                                     name: ElementAttrName::BuiltIn(name),
                                     value,
                                 },
-                            });
+                            }));
                         }
                     }
                 }
@@ -108,7 +124,6 @@ impl Parse for Element {
                     break;
                 }
 
-                // todo: add a message saying you need to include commas between fields
                 if content.parse::<Token![,]>().is_err() {
                     missing_trailing_comma!(span);
                 }
@@ -120,12 +135,26 @@ impl Parse for Element {
 
         // Deduplicate any attributes that can be combined
         // For example, if there are two `class` attributes, combine them into one
-        let mut merged_attributes: Vec<ElementAttrNamed> = Vec::new();
+        let mut merged_attributes: Vec<AttributeType> = Vec::new();
         for attr in &attributes {
-            if let Some(old_attr_index) = merged_attributes
-                .iter()
-                .position(|a| a.attr.name == attr.attr.name)
-            {
+            if let Some(old_attr_index) = merged_attributes.iter().position(|a| {
+                matches!((a, attr), (
+                                AttributeType::Named(ElementAttrNamed {
+                                    attr: ElementAttr {
+                                        name: ElementAttrName::BuiltIn(old_name),
+                                        ..
+                                    },
+                                    ..
+                                }),
+                                AttributeType::Named(ElementAttrNamed {
+                                    attr: ElementAttr {
+                                        name: ElementAttrName::BuiltIn(new_name),
+                                        ..
+                                    },
+                                    ..
+                                }),
+                            ) if old_name == new_name)
+            }) {
                 let old_attr = &mut merged_attributes[old_attr_index];
                 if let Some(combined) = old_attr.try_combine(attr) {
                     *old_attr = combined;
@@ -173,15 +202,31 @@ impl ToTokens for Element {
             None => quote! { None },
         };
 
-        let listeners = self
-            .merged_attributes
-            .iter()
-            .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
+        let listeners = self.merged_attributes.iter().filter(|f| {
+            matches!(
+                f,
+                AttributeType::Named(ElementAttrNamed {
+                    attr: ElementAttr {
+                        value: ElementAttrValue::EventTokens { .. },
+                        ..
+                    },
+                    ..
+                })
+            )
+        });
 
-        let attr = self
-            .merged_attributes
-            .iter()
-            .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
+        let attr = self.merged_attributes.iter().filter(|f| {
+            !matches!(
+                f,
+                AttributeType::Named(ElementAttrNamed {
+                    attr: ElementAttr {
+                        value: ElementAttrValue::EventTokens { .. },
+                        ..
+                    },
+                    ..
+                })
+            )
+        });
 
         tokens.append_all(quote! {
             __cx.element(

+ 65 - 49
packages/rsx/src/lib.rs

@@ -268,7 +268,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
 #[cfg(feature = "hot_reload")]
 #[derive(Default, Debug)]
 struct DynamicMapping {
-    attribute_to_idx: std::collections::HashMap<ElementAttr, Vec<usize>>,
+    attribute_to_idx: std::collections::HashMap<AttributeType, Vec<usize>>,
     last_attribute_idx: usize,
     node_to_idx: std::collections::HashMap<BodyNode, Vec<usize>>,
     last_element_idx: usize,
@@ -284,7 +284,7 @@ impl DynamicMapping {
         new
     }
 
-    fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option<usize> {
+    fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option<usize> {
         self.attribute_to_idx
             .get_mut(attr)
             .and_then(|idxs| idxs.pop())
@@ -294,7 +294,7 @@ impl DynamicMapping {
         self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop())
     }
 
-    fn insert_attribute(&mut self, attr: ElementAttr) -> usize {
+    fn insert_attribute(&mut self, attr: AttributeType) -> usize {
         let idx = self.last_attribute_idx;
         self.last_attribute_idx += 1;
 
@@ -316,10 +316,17 @@ impl DynamicMapping {
         match node {
             BodyNode::Element(el) => {
                 for attr in el.merged_attributes {
-                    match &attr.attr.value {
-                        ElementAttrValue::AttrLiteral(input) if input.is_static() => {}
+                    match &attr {
+                        AttributeType::Named(ElementAttrNamed {
+                            attr:
+                                ElementAttr {
+                                    value: ElementAttrValue::AttrLiteral(input),
+                                    ..
+                                },
+                            ..
+                        }) if input.is_static() => {}
                         _ => {
-                            self.insert_attribute(attr.attr);
+                            self.insert_attribute(attr);
                         }
                     }
                 }
@@ -347,7 +354,7 @@ impl DynamicMapping {
 #[derive(Default, Debug)]
 pub struct DynamicContext<'a> {
     dynamic_nodes: Vec<&'a BodyNode>,
-    dynamic_attributes: Vec<&'a ElementAttrNamed>,
+    dynamic_attributes: Vec<&'a AttributeType>,
     current_path: Vec<u8>,
 
     node_paths: Vec<Vec<u8>>,
@@ -367,10 +374,16 @@ impl<'a> DynamicContext<'a> {
 
                 let mut static_attrs = Vec::new();
                 for attr in &el.merged_attributes {
-                    match &attr.attr.value {
-                        ElementAttrValue::AttrLiteral(value) if value.is_static() => {
+                    match &attr {
+                        AttributeType::Named(ElementAttrNamed {
+                            attr:
+                                ElementAttr {
+                                    value: ElementAttrValue::AttrLiteral(value),
+                                    name,
+                                },
+                            ..
+                        }) if value.is_static() => {
                             let value = value.source.as_ref().unwrap();
-                            let name = &attr.attr.name;
                             let attribute_name_rust = name.to_string();
                             let (name, namespace) =
                                 Ctx::map_attribute(&element_name_rust, &attribute_name_rust)
@@ -384,7 +397,7 @@ impl<'a> DynamicContext<'a> {
 
                         _ => {
                             let idx = match mapping {
-                                Some(mapping) => mapping.get_attribute_idx(&attr.attr)?,
+                                Some(mapping) => mapping.get_attribute_idx(attr)?,
                                 None => self.dynamic_attributes.len(),
                             };
                             self.dynamic_attributes.push(attr);
@@ -454,47 +467,50 @@ impl<'a> DynamicContext<'a> {
                     ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
                     ElementName::Custom(_) => quote! { None },
                 };
-                let static_attrs = el
-                    .merged_attributes
-                    .iter()
-                    .map(|attr| match &attr.attr.value {
-                        ElementAttrValue::AttrLiteral(value) if value.is_static() => {
-                            let value = value.to_static().unwrap();
-                            let ns = {
-                                match &attr.attr.name {
-                                    ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
-                                    ElementAttrName::Custom(_) => quote!(None),
-                                }
-                            };
-                            let name = &attr.attr.name;
-                            let name = match (el_name, name) {
-                                (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
-                                    quote! { #el_name::#name.0 }
-                                }
-                                _ => {
-                                    let as_string = name.to_string();
-                                    quote! { #as_string }
-                                }
-                            };
-                            quote! {
-                                ::dioxus::core::TemplateAttribute::Static {
-                                    name: #name,
-                                    namespace: #ns,
-                                    value: #value,
-
-                                    // todo: we don't diff these so we never apply the volatile flag
-                                    // volatile: dioxus_elements::#el_name::#name.2,
-                                }
+                let static_attrs = el.merged_attributes.iter().map(|attr| match attr {
+                    AttributeType::Named(ElementAttrNamed {
+                        attr:
+                            ElementAttr {
+                                value: ElementAttrValue::AttrLiteral(value),
+                                name,
+                            },
+                        ..
+                    }) if value.is_static() => {
+                        let value = value.to_static().unwrap();
+                        let ns = {
+                            match name {
+                                ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
+                                ElementAttrName::Custom(_) => quote!(None),
+                            }
+                        };
+                        let name = match (el_name, name) {
+                            (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
+                                quote! { #el_name::#name.0 }
+                            }
+                            _ => {
+                                let as_string = name.to_string();
+                                quote! { #as_string }
+                            }
+                        };
+                        quote! {
+                            ::dioxus::core::TemplateAttribute::Static {
+                                name: #name,
+                                namespace: #ns,
+                                value: #value,
+
+                                // todo: we don't diff these so we never apply the volatile flag
+                                // volatile: dioxus_elements::#el_name::#name.2,
                             }
                         }
+                    }
 
-                        _ => {
-                            let ct = self.dynamic_attributes.len();
-                            self.dynamic_attributes.push(attr);
-                            self.attr_paths.push(self.current_path.clone());
-                            quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
-                        }
-                    });
+                    _ => {
+                        let ct = self.dynamic_attributes.len();
+                        self.dynamic_attributes.push(attr);
+                        self.attr_paths.push(self.current_path.clone());
+                        quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
+                    }
+                });
 
                 let attrs = quote! { #(#static_attrs),*};
 

+ 19 - 27
packages/ssr/src/renderer.rs

@@ -91,27 +91,29 @@ impl Renderer {
             match segment {
                 Segment::Attr(idx) => {
                     let attr = &template.dynamic_attrs[*idx];
-                    if attr.name == "dangerous_inner_html" {
-                        inner_html = Some(attr);
-                    } else if attr.namespace == Some("style") {
-                        accumulated_dynamic_styles.push(attr);
-                    } else if BOOL_ATTRS.contains(&attr.name) {
-                        if truthy(&attr.value) {
-                            write!(buf, " {}=", attr.name)?;
-                            write_value(buf, &attr.value)?;
+                    attr.attribute_type().try_for_each(|attr| {
+                        if attr.name == "dangerous_inner_html" {
+                            inner_html = Some(attr);
+                        } else if attr.namespace == Some("style") {
+                            accumulated_dynamic_styles.push(attr);
+                        } else if BOOL_ATTRS.contains(&attr.name) {
+                            if truthy(&attr.value) {
+                                write_attribute(buf, attr)?;
+                            }
+                        } else {
+                            write_attribute(buf, attr)?;
                         }
-                    } else {
-                        write_attribute(buf, attr)?;
-                    }
 
-                    if self.pre_render {
-                        if let AttributeValue::Listener(_) = &attr.value {
-                            // The onmounted event doesn't need a DOM listener
-                            if attr.name != "onmounted" {
-                                accumulated_listeners.push(attr.name);
+                        if self.pre_render {
+                            if let AttributeValue::Listener(_) = &attr.value {
+                                // The onmounted event doesn't need a DOM listener
+                                if attr.name != "onmounted" {
+                                    accumulated_listeners.push(attr.name);
+                                }
                             }
                         }
-                    }
+                        Ok(())
+                    })?;
                 }
                 Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
                     DynamicNode::Component(node) => {
@@ -424,16 +426,6 @@ pub(crate) fn write_attribute(buf: &mut impl Write, attr: &Attribute) -> std::fm
     }
 }
 
-pub(crate) fn write_value(buf: &mut impl Write, value: &AttributeValue) -> std::fmt::Result {
-    match value {
-        AttributeValue::Text(value) => write!(buf, "\"{}\"", value),
-        AttributeValue::Bool(value) => write!(buf, "{}", value),
-        AttributeValue::Int(value) => write!(buf, "{}", value),
-        AttributeValue::Float(value) => write!(buf, "{}", value),
-        _ => Ok(()),
-    }
-}
-
 pub(crate) fn write_value_unquoted(
     buf: &mut impl Write,
     value: &AttributeValue,

+ 9 - 5
packages/web/src/rehydrate.rs

@@ -1,4 +1,5 @@
 use crate::dom::WebsysDom;
+use dioxus_core::AttributeValue;
 use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom};
 
 #[derive(Debug)]
@@ -81,12 +82,15 @@ impl WebsysDom {
                     if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
                         let attribute = &vnode.dynamic_attrs[*id];
                         let id = attribute.mounted_element();
-                        mounted_id = Some(id);
-                        if let dioxus_core::AttributeValue::Listener(_) = attribute.value {
-                            if attribute.name == "onmounted" {
-                                to_mount.push(id);
+                        attribute.attribute_type().for_each(|attribute| {
+                            let value = &attribute.value;
+                            mounted_id = Some(id);
+                            if let AttributeValue::Listener(_) = value {
+                                if attribute.name == "onmounted" {
+                                    to_mount.push(id);
+                                }
                             }
-                        }
+                        });
                     }
                 }
                 if let Some(id) = mounted_id {