Parcourir la source

partially restore spreading attributes

Evan Almloff il y a 1 an
Parent
commit
36e9eb1160

+ 8 - 8
examples/spread.rs

@@ -1,8 +1,8 @@
-use dioxus::prelude::*;
+use dioxus::{core::NoOpMutations, prelude::*};
 
 fn main() {
     let mut dom = VirtualDom::new(app);
-    let _ = dom.rebuild();
+    let _ = dom.rebuild(&mut NoOpMutations);
     let html = dioxus_ssr::render(&dom);
 
     println!("{}", html);
@@ -23,14 +23,14 @@ fn app() -> Element {
 #[component]
 fn Component(props: Props) -> Element {
     render! {
-        audio { ..cx.props.attributes, "1: {cx.props.extra_data}\n2: {cx.props.extra_data2}" }
+        audio { ..props.attributes, "1: {props.extra_data}\n2: {props.extra_data2}" }
     }
 }
 
-#[derive(Props)]
-struct Props<'a> {
+#[derive(Props, PartialEq, Clone)]
+struct Props {
     #[props(extends = GlobalAttributes)]
-    attributes: Vec<Attribute<'a>>,
-    extra_data: &'a str,
-    extra_data2: &'a str,
+    attributes: Vec<Attribute>,
+    extra_data: String,
+    extra_data2: String,
 }

+ 21 - 62
packages/core-macro/src/props/mod.rs

@@ -40,7 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
                     #builder_creation
                     #conversion_helper
                     #( #fields )*
-                    // #( #extends )*
+                    #( #extends )*
                     #( #required_fields )*
                     #build_method
                 }
@@ -497,8 +497,7 @@ mod struct_info {
     use syn::parse::Error;
     use syn::punctuated::Punctuated;
     use syn::spanned::Spanned;
-    use syn::visit::Visit;
-    use syn::{parse_quote, Expr, Ident};
+    use syn::{Expr, Ident};
 
     use super::field_info::{FieldBuilderAttr, FieldInfo};
     use super::util::{
@@ -532,37 +531,6 @@ mod struct_info {
                 .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(
             ast: &'a syn::DeriveInput,
             fields: impl Iterator<Item = &'a syn::Field>,
@@ -607,16 +575,8 @@ mod struct_info {
             // Therefore, we will generate code that shortcircuits the "comparison" in memoization
             let are_there_generics = !self.generics.params.is_empty();
 
-            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 (impl_generics, ty_generics, where_clause) = 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(),
@@ -699,12 +659,23 @@ Finally, call `.build()` to create the instance of `{name}`.
                 false => quote! { self == other },
             };
 
+            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())
+            });
+
             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 {
                         #builder_name {
+                            #(#extend_fields_value,)*
                             fields: #empties_tuple,
                             _phantom: ::core::default::Default::default(),
                         }
@@ -715,11 +686,12 @@ 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,)*
                     fields: #all_fields_param,
                     _phantom: (#( #phantom_generics ),*),
                 }
 
-                impl #impl_generics ::dioxus::prelude::Properties<#extend_lifetime> for #name #ty_generics
+                impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
                 #b_generics_where_extras_predicates
                 {
                     type Builder = #builder_name #generics_with_empty;
@@ -776,7 +748,6 @@ Finally, call `.build()` to create the instance of `{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
@@ -840,11 +811,6 @@ Finally, call `.build()` to create the instance of `{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);
@@ -854,18 +820,18 @@ Finally, call `.build()` to create the instance of `{name}`.
                 );
                 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 {}
+                    impl #impl_generics dioxus_elements::extensions::#marker_name 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 {
+                impl #impl_generics ::dioxus::prelude::HasAttributes for #builder_name < #( #ty_generics ),* > #where_clause {
                     fn push_attribute(
                         mut self,
-                        name: &#extend_lifetime str,
+                        name: &'static str,
                         ns: Option<&'static str>,
-                        attr: impl ::dioxus::prelude::IntoAttributeValue<#extend_lifetime>,
+                        attr: impl ::dioxus::prelude::IntoAttributeValue,
                         volatile: bool
                     ) -> Self {
                         let ( #(#descructuring,)* ) = self.fields;
@@ -874,7 +840,7 @@ Finally, call `.build()` to create the instance of `{name}`.
                                 name,
                                 {
                                     use ::dioxus::prelude::IntoAttributeValue;
-                                    attr.into_value(self.bump)
+                                    attr.into_value()
                                 },
                                 ns,
                                 volatile,
@@ -882,7 +848,6 @@ Finally, call `.build()` to create the instance of `{name}`.
                         );
                         #builder_name {
                             #(#forward_extended_fields,)*
-                            bump: self.bump,
                             fields: ( #(#reconstructing,)* ),
                             _phantom: self._phantom,
                         }
@@ -1007,11 +972,6 @@ Finally, call `.build()` to create the instance of `{name}`.
                 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)]
@@ -1023,7 +983,6 @@ Finally, call `.build()` to create the instance of `{name}`.
                         let ( #(#descructuring,)* ) = self.fields;
                         #builder_name {
                             #(#forward_extended_fields,)*
-                            #forward_bump
                             fields: ( #(#reconstructing,)* ),
                             _phantom: self._phantom,
                         }

+ 103 - 54
packages/core/src/diff/node.rs

@@ -7,7 +7,6 @@ use crate::{
     arena::ElementId,
     innerlude::{ElementPath, ElementRef, VComponent, VNodeMount, VText},
     nodes::DynamicNode,
-    nodes::RenderReturn,
     scopes::ScopeId,
     TemplateNode,
     TemplateNode::*,
@@ -61,16 +60,7 @@ impl VNode {
 
         // If the templates are the same, we can diff the attributes and children
         // Start with the attributes
-        self.dynamic_attrs
-            .iter()
-            .zip(new.dynamic_attrs.iter())
-            .enumerate()
-            .for_each(|(idx, (old_attr, new_attr))| {
-                // If the attributes are different (or volatile), we need to update them
-                if old_attr.value != new_attr.value || new_attr.volatile {
-                    self.update_attribute(mount, idx, new_attr, to);
-                }
-            });
+        self.diff_attributes(new, dom, to);
 
         // Now diff the dynamic nodes
         self.dynamic_nodes
@@ -317,17 +307,108 @@ impl VNode {
         }
     }
 
-    pub(super) fn update_attribute(
+    pub(super) fn diff_attributes(
         &self,
-        mount: &VNodeMount,
-        idx: usize,
-        new_attr: &Attribute,
+        new: &VNode,
+        dom: &mut VirtualDom,
         to: &mut impl WriteMutations,
     ) {
-        let name = &new_attr.name;
-        let value = &new_attr.value;
-        let id = mount.mounted_attributes[idx];
-        to.set_attribute(name, new_attr.namespace, value, id);
+        let mount_id = self.mount.get();
+        for (idx, (old_attrs, new_attrs)) in self
+            .dynamic_attrs
+            .iter()
+            .zip(new.dynamic_attrs.iter())
+            .enumerate()
+        {
+            let mut old_attributes_iter = old_attrs.iter().peekable();
+            let mut new_attributes_iter = new_attrs.iter().peekable();
+            let attribute_id = dom.mounts[mount_id.0].mounted_attributes[idx];
+            let path = self.template.get().attr_paths[idx];
+
+            loop {
+                match (old_attributes_iter.peek(), new_attributes_iter.peek()) {
+                    (Some(old_attribute), Some(new_attribute)) => {
+                        // check which name is greater
+                        match old_attribute.name.cmp(new_attribute.name) {
+                            // The two attributes are the same, so diff them
+                            std::cmp::Ordering::Equal => {
+                                let old = old_attributes_iter.next().unwrap();
+                                let new = new_attributes_iter.next().unwrap();
+                                if old.value != new.value {
+                                    self.write_attribute(
+                                        path,
+                                        new,
+                                        attribute_id,
+                                        mount_id,
+                                        dom,
+                                        to,
+                                    );
+                                }
+                            }
+                            // In a sorted list, if the old attribute name is first, then the new attribute is missing
+                            std::cmp::Ordering::Less => {
+                                let old = old_attributes_iter.next().unwrap();
+                                self.remove_attribute(old, attribute_id, to)
+                            }
+                            // In a sorted list, if the new attribute name is first, then the old attribute is missing
+                            std::cmp::Ordering::Greater => {
+                                let new = new_attributes_iter.next().unwrap();
+                                self.write_attribute(path, new, attribute_id, mount_id, dom, to);
+                            }
+                        }
+                    }
+                    (Some(_), None) => {
+                        let left = old_attributes_iter.next().unwrap();
+                        self.remove_attribute(left, attribute_id, to)
+                    }
+                    (None, Some(_)) => {
+                        let right = new_attributes_iter.next().unwrap();
+                        self.write_attribute(path, right, attribute_id, mount_id, dom, to)
+                    }
+                    (None, None) => break,
+                }
+            }
+        }
+    }
+
+    fn remove_attribute(&self, attribute: &Attribute, id: ElementId, to: &mut impl WriteMutations) {
+        match &attribute.value {
+            AttributeValue::Listener(_) => {
+                to.remove_event_listener(&attribute.name[2..], id);
+            }
+            _ => {
+                to.set_attribute(
+                    attribute.name,
+                    attribute.namespace,
+                    &AttributeValue::None,
+                    id,
+                );
+            }
+        }
+    }
+
+    fn write_attribute(
+        &self,
+        path: &'static [u8],
+        attribute: &Attribute,
+        id: ElementId,
+        mount: MountId,
+        dom: &mut VirtualDom,
+        to: &mut impl WriteMutations,
+    ) {
+        match &attribute.value {
+            AttributeValue::Listener(_) => {
+                let element_ref = ElementRef {
+                    path: ElementPath { path },
+                    mount,
+                };
+                dom.elements[id.0] = Some(element_ref);
+                to.create_event_listener(&attribute.name[2..], id);
+            }
+            _ => {
+                to.set_attribute(attribute.name, attribute.namespace, &attribute.value, id);
+            }
+        }
     }
 
     /// Lightly diff the two templates, checking only their roots.
@@ -650,7 +731,9 @@ impl VNode {
             let id = self.assign_static_node_as_dynamic(path, root, dom, to);
 
             loop {
-                self.write_attribute(mount, attr_id, id, dom, to);
+                for attr in &*self.dynamic_attrs[attr_id] {
+                    self.write_attribute(path, attr, id, mount, dom, to);
+                }
 
                 // Only push the dynamic attributes forward if they match the current path (same element)
                 match attrs.next_if(|(_, p)| *p == path) {
@@ -661,40 +744,6 @@ impl VNode {
         }
     }
 
-    fn write_attribute(
-        &self,
-        mount: MountId,
-        idx: usize,
-        id: ElementId,
-        dom: &mut VirtualDom,
-        to: &mut impl WriteMutations,
-    ) {
-        // Make sure we set the attribute's associated id
-        dom.mounts[mount.0].mounted_attributes[idx] = id;
-
-        let attribute = &self.dynamic_attrs[idx];
-
-        match &attribute.value {
-            AttributeValue::Listener(_) => {
-                // If this is a listener, we need to create an element reference for it so that when we receive an event, we can find the element
-                let path = &self.template.get().attr_paths[idx];
-
-                // The mount information should always be in the VDOM at this point
-                debug_assert!(dom.mounts.get(mount.0).is_some());
-
-                let element_ref = ElementRef {
-                    path: ElementPath { path },
-                    mount,
-                };
-                dom.elements[id.0] = Some(element_ref);
-                to.create_event_listener(&attribute.name[2..], id);
-            }
-            _ => {
-                to.set_attribute(attribute.name, attribute.namespace, &attribute.value, id);
-            }
-        }
-    }
-
     fn load_template_root(
         &self,
         mount: MountId,

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

@@ -7,7 +7,7 @@ use crate::{
 use std::{
     any::{Any, TypeId},
     backtrace::Backtrace,
-    cell::{Cell, RefCell},
+    cell::RefCell,
     error::Error,
     fmt::{Debug, Display},
     rc::Rc,

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

@@ -74,9 +74,9 @@ pub(crate) mod innerlude {
 pub use crate::innerlude::{
     fc_to_builder, generation, once, schedule_update, schedule_update_any, vdom_is_rendering,
     AnyValue, Attribute, AttributeValue, CapturedError, Component, DynamicNode, Element, ElementId,
-    Event, Fragment, IntoDynNode, Mutation, Mutations, NoOpMutations, Properties, RenderReturn,
-    ScopeId, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner,
-    VPlaceholder, VText, VirtualDom, WriteMutations,
+    Event, Fragment, HasAttributes, IntoDynNode, Mutation, Mutations, NoOpMutations, Properties,
+    RenderReturn, ScopeId, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode,
+    VNodeInner, VPlaceholder, VText, VirtualDom, WriteMutations,
 };
 
 /// The purpose of this module is to alleviate imports of many common types
@@ -87,9 +87,9 @@ pub mod prelude {
         consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, generation,
         has_context, needs_update, once, parent_scope, provide_context, provide_root_context,
         push_future, remove_future, schedule_update, schedule_update_any, spawn, spawn_forever,
-        suspend, use_error_boundary, use_hook, AnyValue, Component, Element, ErrorBoundary, Event,
-        EventHandler, Fragment, IntoAttributeValue, IntoDynNode, Properties, Runtime, RuntimeGuard,
-        ScopeId, Task, Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner,
-        VirtualDom,
+        suspend, use_error_boundary, use_hook, AnyValue, Attribute, Component, Element,
+        ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
+        IntoDynNode, Properties, Runtime, RuntimeGuard, ScopeId, Task, Template, TemplateAttribute,
+        TemplateNode, Throw, VNode, VNodeInner, VirtualDom,
     };
 }

+ 44 - 7
packages/core/src/nodes.rs

@@ -6,6 +6,7 @@ use crate::{
 };
 use crate::{arena::ElementId, Element, Event};
 use crate::{Properties, VirtualDom};
+use core::panic;
 use std::ops::Deref;
 use std::rc::Rc;
 use std::vec;
@@ -94,11 +95,34 @@ pub struct VNodeInner {
     /// The static nodes and static descriptor of the template
     pub template: Cell<Template>,
 
-    /// The dynamic parts of the template
+    /// The dynamic nodes in the template
     pub dynamic_nodes: Box<[DynamicNode]>,
 
-    /// The dynamic parts of the template
-    pub dynamic_attrs: Box<[Attribute]>,
+    /// The dynamic attribute slots in the template
+    ///
+    /// This is a list of positions in the template where dynamic attributes can be inserted.
+    ///
+    /// The inner list *must* be in the format [static named attributes, remaining dynamically named attributes].
+    ///
+    /// For example:
+    /// ```rust
+    /// div {
+    ///     class: "{class}",
+    ///     ..attrs,
+    ///     p {
+    ///         color: "{color}",
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// Would be represented as:
+    /// ```rust
+    /// [
+    ///     [class, every attribute in attrs sorted by name], // Slot 0 in the template
+    ///     [color], // Slot 1 in the template
+    /// ]
+    /// ```
+    pub dynamic_attrs: Box<[Box<[Attribute]>]>,
 }
 
 /// A reference to a template along with any context needed to hydrate it
@@ -186,7 +210,7 @@ impl VNode {
         key: Option<String>,
         template: Template,
         dynamic_nodes: Box<[DynamicNode]>,
-        dynamic_attrs: Box<[Attribute]>,
+        dynamic_attrs: Box<[Box<[Attribute]>]>,
     ) -> Self {
         Self {
             vnode: Rc::new(VNodeInner {
@@ -572,7 +596,7 @@ pub enum TemplateAttribute {
 }
 
 /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
-#[derive(Debug)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct Attribute {
     /// The name of the attribute.
     pub name: &'static str,
@@ -687,6 +711,19 @@ impl PartialEq for AttributeValue {
     }
 }
 
+impl Clone for AttributeValue {
+    fn clone(&self) -> Self {
+        match self {
+            Self::Text(arg0) => Self::Text(arg0.clone()),
+            Self::Float(arg0) => Self::Float(*arg0),
+            Self::Int(arg0) => Self::Int(*arg0),
+            Self::Bool(arg0) => Self::Bool(*arg0),
+            Self::Listener(_) | Self::Any(_) => panic!("Cannot clone listener or any value"),
+            Self::None => Self::None,
+        }
+    }
+}
+
 #[doc(hidden)]
 pub trait AnyValue: 'static {
     fn any_cmp(&self, other: &dyn AnyValue) -> bool;
@@ -880,11 +917,11 @@ impl<T: IntoAttributeValue> IntoAttributeValue for Option<T> {
 }
 
 /// A trait for anything that has a dynamic list of attributes
-pub trait HasAttributes<'a> {
+pub trait HasAttributes {
     /// Push an attribute onto the list of attributes
     fn push_attribute(
         self,
-        name: &'a str,
+        name: &'static str,
         ns: Option<&'static str>,
         attr: impl IntoAttributeValue,
         volatile: bool,

+ 24 - 20
packages/core/src/virtual_dom.rs

@@ -391,20 +391,22 @@ impl VirtualDom {
                 let node_template = el_ref.template.get();
                 let target_path = path.path;
 
-                for (idx, attr) in el_ref.dynamic_attrs.iter().enumerate() {
+                for (idx, attrs) in el_ref.dynamic_attrs.iter().enumerate() {
                     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);
-
-                        // 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
-                        // documented, or be rejected from the rsx! macro outright
-                        if target_path == this_path {
-                            break;
+                    for attr in attrs.iter() {
+                        // 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);
+
+                            // 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
+                            // documented, or be rejected from the rsx! macro outright
+                            if target_path == this_path {
+                                break;
+                            }
                         }
                     }
                 }
@@ -436,15 +438,17 @@ impl VirtualDom {
                 for (idx, attr) in el_ref.dynamic_attrs.iter().enumerate() {
                     let this_path = node_template.attr_paths[idx];
 
-                    // 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 {
-                            self.runtime.rendering.set(false);
-                            listener.call(uievent.clone());
-                            self.runtime.rendering.set(true);
+                    for attr in attr.iter() {
+                        // 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 {
+                                self.runtime.rendering.set(false);
+                                listener.call(uievent.clone());
+                                self.runtime.rendering.set(true);
 
-                            break;
+                                break;
+                            }
                         }
                     }
                 }

+ 4 - 4
packages/core/tests/create_dom.rs

@@ -75,7 +75,7 @@ fn create() {
 
 #[test]
 fn create_list() {
-    let mut dom = VirtualDom::new(|| render! {(0..3).map(|f| render!( div { "hello" } ))});
+    let mut dom = VirtualDom::new(|| render! {{(0..3).map(|f| render!( div { "hello" } ))}});
 
     let _edits = dom.rebuild_to_vec().santize();
 
@@ -137,7 +137,7 @@ fn create_components() {
     fn Child(cx: ChildProps) -> Element {
         render! {
             h1 {}
-            div { &cx.children }
+            div { {cx.children} }
             p {}
         }
     }
@@ -152,10 +152,10 @@ fn anchors() {
     let mut dom = VirtualDom::new(|| {
         render! {
             if true {
-                render!( div { "hello" } )
+                 div { "hello" }
             }
             if false {
-                render!( div { "goodbye" } )
+                div { "goodbye" }
             }
         }
     });

+ 3 - 6
packages/core/tests/create_fragments.rs

@@ -7,7 +7,7 @@ use dioxus_core::ElementId;
 #[test]
 fn empty_fragment_creates_nothing() {
     fn app() -> Element {
-        render!(())
+        render!({ () })
     }
 
     let mut vdom = VirtualDom::new(app);
@@ -55,7 +55,7 @@ fn fragments_nested() {
                     }}
                 }}
             }}
-        ))
+        )
     });
 
     assert_eq!(
@@ -77,10 +77,7 @@ fn fragments_across_components() {
 
     fn demo_child() -> Element {
         let world = "world";
-        render! {
-            "hellO!"
-            {world}
-        }
+        render! { "hellO!", {world} }
     }
 
     assert_eq!(

+ 1 - 1
packages/core/tests/diff_unkeyed_list.rs

@@ -153,7 +153,7 @@ fn list_shrink_multiroot() {
     let mut dom = VirtualDom::new(|| {
         render! {
             div {
-                for i in 0..cx.generation() {
+                for i in 0..generation() {
                     div { "{i}" }
                     div { "{i}" }
                 }

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

@@ -1,7 +1,7 @@
 #![cfg(not(miri))]
 
 use dioxus::prelude::*;
-use dioxus_core::{prelude::EventHandler, AttributeValue, DynamicNode, VComponent, *};
+use dioxus_core::{prelude::EventHandler, AttributeValue, DynamicNode, VComponent, VNode, *};
 use std::{cfg, collections::HashSet, default::Default};
 
 fn random_ns() -> Option<&'static str> {
@@ -250,7 +250,7 @@ fn create_random_element(cx: DepthProps) -> Element {
                 )
                 .into_boxed_str(),
             ));
-            let node = cx.vnode(
+            let node = VNode::new(
                 None,
                 template,
                 dynamic_node_types
@@ -263,7 +263,7 @@ fn create_random_element(cx: DepthProps) -> Element {
                     })
                     .collect(),
                 (0..template.attr_paths.len())
-                    .map(|_| create_random_dynamic_attr())
+                    .map(|_| Box::new([create_random_dynamic_attr()]) as Box<[Attribute]>)
                     .collect(),
             );
             Some(node)

+ 1 - 1
packages/core/tests/miri_full_app.rs

@@ -38,7 +38,7 @@ fn app() -> Element {
             }
             button { onclick: move |_| idx -= 1, "-" }
             ul {
-                {(0..**idx).map(|i| rsx! {
+                {(0..*idx()).map(|i| rsx! {
                     ChildExample { i: i, onhover: onhover }
                 })}
             }

+ 1 - 1
packages/core/tests/task.rs

@@ -25,7 +25,7 @@ async fn it_works() {
             });
         });
 
-        render!(())
+        render!({ () })
     }
 
     let mut dom = VirtualDom::new(app);

+ 2 - 2
packages/desktop/src/launch.rs

@@ -23,8 +23,8 @@ use tao::event::{Event, StartCause, WindowEvent};
 ///     })
 /// }
 /// ```
-pub fn launch(root: Component) {
-    launch_with_props(root, (), Config::default())
+pub fn launch(root: fn() -> Element) {
+    launch_with_props(|root| root(), root, Config::default())
 }
 
 /// Launch the WebView and run the event loop, with configuration.

+ 2 - 2
packages/html-internal-macro/src/lib.rs

@@ -61,14 +61,14 @@ impl ToTokens for ImplExtensionAttributes {
                 quote! { <#impl_name as #name>::#ident }
             };
             quote! {
-                fn #ident(self, value: impl IntoAttributeValue<'a>) -> Self {
+                fn #ident(self, value: impl IntoAttributeValue) -> 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 {
+            pub trait #extension_name: HasAttributes + Sized {
                 #(#impls)*
             }
         });

+ 4 - 3
packages/html/src/elements.rs

@@ -1,6 +1,7 @@
 #![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;
@@ -385,9 +386,9 @@ macro_rules! builder_constructors {
 
         pub(crate) mod extensions {
             use super::*;
-            // $(
-                // impl_extension_attributes![ELEMENT $name { $($fil,)* }];
-            // )*
+            $(
+                impl_extension_attributes![ELEMENT $name { $($fil,)* }];
+            )*
         }
     };
 }

+ 2 - 1
packages/html/src/global_attributes.rs

@@ -1,6 +1,7 @@
 #![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;
@@ -110,7 +111,7 @@ macro_rules! trait_methods {
             None
         }
 
-        // impl_extension_attributes![GLOBAL $trait { $($name,)* }];
+        impl_extension_attributes![GLOBAL $trait { $($name,)* }];
     };
 
     // Rename the incoming ident and apply a custom namespace

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

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

+ 20 - 10
packages/rsx/src/lib.rs

@@ -256,8 +256,8 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
             ::dioxus::core::VNode::new(
                 #key_tokens,
                 TEMPLATE,
-                Box::new([ #( #node_printer ),* ]),
-                Box::new([ #( #dyn_attr_printer ),* ]),
+                Box::new([ #( #node_printer),* ]),
+                Box::new([ #( Box::new([ #( #dyn_attr_printer),* ]) ),* ]),
             )
         });
     }
@@ -352,7 +352,7 @@ impl DynamicMapping {
 #[derive(Default, Debug)]
 pub struct DynamicContext<'a> {
     dynamic_nodes: Vec<&'a BodyNode>,
-    dynamic_attributes: Vec<&'a AttributeType>,
+    dynamic_attributes: Vec<Vec<&'a AttributeType>>,
     current_path: Vec<u8>,
 
     node_paths: Vec<Vec<u8>>,
@@ -398,7 +398,7 @@ impl<'a> DynamicContext<'a> {
                                 Some(mapping) => mapping.get_attribute_idx(attr)?,
                                 None => self.dynamic_attributes.len(),
                             };
-                            self.dynamic_attributes.push(attr);
+                            self.dynamic_attributes.push(vec![attr]);
 
                             if self.attr_paths.len() <= idx {
                                 self.attr_paths.resize_with(idx + 1, Vec::new);
@@ -498,19 +498,29 @@ impl<'a> DynamicContext<'a> {
 
                                 // 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 } }
+                        // If this attribute is dynamic, but it already exists in the template, we can reuse the index
+                        if let Some(attribute_index) = self
+                            .attr_paths
+                            .iter()
+                            .position(|path| path == &self.current_path)
+                        {
+                            self.dynamic_attributes[attribute_index].push(attr);
+                            quote! {}
+                        } else {
+                            let ct = self.dynamic_attributes.len();
+                            self.dynamic_attributes.push(vec![attr]);
+                            self.attr_paths.push(self.current_path.clone());
+                            quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct }, }
+                        }
                     }
                 });
 
-                let attrs = quote! { #(#static_attrs),*};
+                let attrs = quote! { #(#static_attrs)* };
 
                 let children = el.children.iter().enumerate().map(|(idx, root)| {
                     self.current_path.push(idx as u8);