diff --git a/Cargo.toml b/Cargo.toml index 9fc38df5f..7ef10d612 100644 --- a/Cargo.toml +++ b/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" } diff --git a/examples/README.md b/examples/README.md index 93530ba30..a20ffe60e 100644 --- a/examples/README.md +++ b/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 diff --git a/examples/spread.rs b/examples/spread.rs new file mode 100644 index 000000000..45bdc90af --- /dev/null +++ b/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>, + extra_data: &'a str, + extra_data2: &'a str, +} diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index ebbce7538..48077fa16 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -166,7 +166,7 @@ impl Writer<'_> { fn write_attributes( &mut self, - attributes: &[ElementAttrNamed], + attributes: &[AttributeType], key: &Option, 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 { diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index ede35d768..d569cdb9f 100644 --- a/packages/autofmt/src/writer.rs +++ b/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; } diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 1898d8bd1..ae3dc9cdd 100644 --- a/packages/core-macro/Cargo.toml +++ b/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 diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index d7a580705..fff8bcbf1 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { .included_fields() .map(|f| struct_info.field_impl(f)) .collect::, _>>()?; + let extends = struct_info + .extend_fields() + .map(|f| struct_info.extends_impl(f)) + .collect::, _>>()?; 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 { #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, } 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> { - 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> { + self.fields + .iter() + .filter(|f| !f.builder_attr.extends.is_empty()) + } + + fn extend_lifetime(&self) -> syn::Result> { + let first_extend = self.extend_fields().next(); + + match first_extend { + Some(f) => { + struct VisitFirstLifetime(Option); + + 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 { + 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 = 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 { 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 = 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 = 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 { diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index f3b89ae9e..8602b93bd 100644 --- a/packages/core/src/create.rs +++ b/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); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 43b27f2e7..a116aee4d 100644 --- a/packages/core/src/diff.rs +++ b/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, diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index d4fe7c1ba..26155704b 100644 --- a/packages/core/src/error_boundary.rs +++ b/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 { diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index dbc8e8131..4a775a57c 100644 --- a/packages/core/src/fragment.rs +++ b/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 { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 13d55826b..c478ec657 100644 --- a/packages/core/src/lib.rs +++ b/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, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d3ad1875f..6396504a4 100644 --- a/packages/core/src/nodes.rs +++ b/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, +} + +impl<'a> From> 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>> for MountedAttribute<'a> { + fn from(attr: &'a Vec>) -> 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, } impl<'a> Attribute<'a> { @@ -479,13 +521,40 @@ impl<'a> Attribute<'a> { value, namespace, volatile, - mounted_element: Cell::new(ElementId::default()), + } + } +} + +/// 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), } } - /// Get the element that this attribute is mounted to - pub fn mounted_element(&self) -> ElementId { - self.mounted_element.get() + /// 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 { } } } + +/// 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; +} diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 3f0d9ef97..b2263e039 100644 --- a/packages/core/src/properties.rs +++ b/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))] diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ef4a5771f..64a60b112 100644 --- a/packages/core/src/scopes.rs +++ b/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); - } - // 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); - } + 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); + } - _ => (), - } + _ => (), + } + }) } 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, { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 9327e8f29..28ccbc76a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/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()); - } - self.runtime.scope_stack.borrow_mut().pop(); - self.runtime.rendering.set(true); + 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); - break; + should_stop = true; + } + } + }); + if should_stop { + return; } } } diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 65d131eb8..af5e9d05c 100644 --- a/packages/core/tests/fuzzing.rs +++ b/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::() % 7 { 0 => AttributeValue::Text(Box::leak( format!("{}", rand::random::()).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| 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; diff --git a/packages/html-internal-macro/Cargo.toml b/packages/html-internal-macro/Cargo.toml new file mode 100644 index 000000000..ce8ad54b1 --- /dev/null +++ b/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"] } \ No newline at end of file diff --git a/packages/html-internal-macro/src/lib.rs b/packages/html-internal-macro/src/lib.rs new file mode 100644 index 000000000..a404a9b16 --- /dev/null +++ b/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, +} + +impl Parse for ImplExtensionAttributes { + fn parse(input: ParseStream) -> syn::Result { + 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)* + } + }); + } +} diff --git a/packages/html-internal-macro/tests/01-simple.rs b/packages/html-internal-macro/tests/01-simple.rs new file mode 100644 index 000000000..f328e4d9d --- /dev/null +++ b/packages/html-internal-macro/tests/01-simple.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/packages/html-internal-macro/tests/progress.rs b/packages/html-internal-macro/tests/progress.rs new file mode 100644 index 000000000..563805397 --- /dev/null +++ b/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"); +} diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 68c365579..8d86a9672 100644 --- a/packages/html/Cargo.toml +++ b/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 } diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index ca3f4aee7..37e6bb682 100644 --- a/packages/html/src/elements.rs +++ b/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,)* }]; + )* + } }; } diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 1078f1db0..2e5cabf75 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -12,7 +12,7 @@ macro_rules! impl_event { $( $( #[$attr] )* #[inline] - pub fn $name<'a, E: crate::EventReturn, 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>(_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| { @@ -20,7 +20,7 @@ macro_rules! impl_event { }), None, false, - ) + ).into() } )* }; diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index f6b3d14a9..c5ec92720 100644 --- a/packages/html/src/events/mouse.rs +++ b/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>( + _cx: &'a ::dioxus_core::ScopeState, + mut _f: impl FnMut(::dioxus_core::Event) -> E + 'a, +) -> ::dioxus_core::MountedAttribute<'a> { + ::dioxus_core::Attribute::new( + "ondblclick", + _cx.listener(move |e: ::dioxus_core::Event| { + _f(e).spawn(_cx); + }), + None, + false, + ) + .into() +} + impl MouseData { /// Create a new instance of MouseData pub fn new(inner: impl HasMouseData + 'static) -> Self { diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 639026b33..b9f06a754 100644 --- a/packages/html/src/global_attributes.rs +++ b/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 diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index e7cce2657..379b5f581 100644 --- a/packages/html/src/lib.rs +++ b/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}; } diff --git a/packages/native-core/tests/fuzzing.rs b/packages/native-core/tests/fuzzing.rs index 29c4fef9e..4abcc2351 100644 --- a/packages/native-core/tests/fuzzing.rs +++ b/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::() % 2 { + 0 => MountedAttribute::from( + &*cx.bump().alloc( + (0..(rand::random::() % 3) as usize) + .map(|_| create_random_dynamic_attr(cx)) + .collect::>(), + ), + ), + 1 => MountedAttribute::from(create_random_dynamic_attr(cx)), + _ => unreachable!(), + } +} + fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute { let value = match rand::random::() % 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::()), 5 => AttributeValue::None, - // Listener(RefCell>>), _ => unreachable!(), }; Attribute::new( @@ -271,11 +284,13 @@ fn create_random_element(cx: Scope) -> Element { .collect(); cx.bump().alloc(dynamic_nodes) }, - cx.bump().alloc( - (0..template.attr_paths.len()) - .map(|_| create_random_dynamic_attr(cx)) - .collect::>(), - ), + cx.bump() + .alloc( + (0..template.attr_paths.len()) + .map(|_| create_random_dynamic_mounted_attr(cx)) + .collect::>(), + ) + .as_slice(), ); Some(node) } diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 882cb333e..fe68f78ad 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/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 { } }; - 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 { )), 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 { )), value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)), }, - }); + })); } let children = el.children.iter().filter_map(rsx_node_from_html).collect(); diff --git a/packages/rsx/src/attribute.rs b/packages/rsx/src/attribute.rs index 38cb7c853..4c0db5a33 100644 --- a/packages/rsx/src/attribute.rs +++ b/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 { + 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(&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 { if self.el_name == other.el_name && self.attr.name == other.attr.name { diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 6487e5ab7..bccef2638 100644 --- a/packages/rsx/src/component.rs +++ b/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() { diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 86d530d33..e62fc6d6e 100644 --- a/packages/rsx/src/element.rs +++ b/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, - pub attributes: Vec, - pub merged_attributes: Vec, + pub attributes: Vec, + pub merged_attributes: Vec, pub children: Vec, 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 = vec![]; + let mut attributes: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; @@ -42,6 +42,22 @@ impl Parse for Element { // "def": 456, // abc: 123, loop { + if content.peek(Token![..]) { + content.parse::()?; + let expr = content.parse::()?; + let span = expr.span(); + attributes.push(attribute::AttributeType::Spread(expr)); + + if content.is_empty() { + break; + } + + if content.parse::().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::()?; @@ -50,13 +66,13 @@ impl Parse for Element { content.parse::()?; let value = content.parse::()?; - 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::()?; - 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::().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 = Vec::new(); + let mut merged_attributes: Vec = 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( diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index f1464f0f2..aee1bb431 100644 --- a/packages/rsx/src/lib.rs +++ b/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>, + attribute_to_idx: std::collections::HashMap>, last_attribute_idx: usize, node_to_idx: std::collections::HashMap>, last_element_idx: usize, @@ -284,7 +284,7 @@ impl DynamicMapping { new } - fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option { + fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option { 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, node_paths: Vec>, @@ -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, + 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, - } + // 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),*}; diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 2db5fdc66..65ab4d0ed 100644 --- a/packages/ssr/src/renderer.rs +++ b/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, diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 4905d1e24..e50da9e2b 100644 --- a/packages/web/src/rehydrate.rs +++ b/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 {