diff --git a/Cargo.toml b/Cargo.toml index 5b55f4628..0072a608e 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", 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/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index dbc336401..44f527337 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -49,6 +49,7 @@ impl Writer<'_> { attributes, children, brace, + extra_attributes, } = el; /* diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 62e142f60..b9e514a7c 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -73,8 +73,8 @@ 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, + fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeBox, AttributeValue, BorrowedAttributeValue, + CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, HasAttributesBox, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a108f3577..0e3a9708b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -832,3 +832,34 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option { } } } + +pub struct AttributeBox<'a> { + /// The name of the attribute. + pub name: &'a str, + + /// The value of the attribute + pub value: Box + 'static>, + + /// The namespace of the attribute. + /// + /// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups. + pub namespace: Option<&'static str>, + + /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated + pub volatile: bool, +} + +impl<'a> AttributeBox<'a> { + pub fn new(name: &'a str, value: impl IntoAttributeValue<'a> + 'static, namespace: Option<&'static str>, volatile: bool) -> Self { + Self { + name, + value: Box::new(value), + namespace, + volatile, + } + } +} + +pub trait HasAttributesBox<'a, T> { + fn push_attribute(self, attr: AttributeBox<'a>) -> T; +} diff --git a/packages/dioxus/tests/props_spread.rs b/packages/dioxus/tests/props_spread.rs new file mode 100644 index 000000000..a183e11da --- /dev/null +++ b/packages/dioxus/tests/props_spread.rs @@ -0,0 +1,121 @@ +use dioxus::core_macro::render; +use dioxus::prelude::rsx; +use dioxus_core::{AttributeBox, Element, HasAttributesBox, Scope}; +use dioxus_html::{ExtendedAudioMarker, ExtendedGlobalAttributesMarker}; + +#[test] +fn props_spread() { + pub struct FooProps<'a> { + pub open: Option<&'a str>, + attributes: Vec>, + } + + // ----- + impl<'a> FooProps<'a> { + #[doc = "\nCreate a builder for building `FooProps`.\nOn the builder, call `.open(...)`(optional) to set the values of the fields.\nFinally, call `.build()` to create the instance of `FooProps`.\n "] + #[allow(dead_code)] + pub fn builder() -> FooPropsBuilder<'a, ((), ), > { + FooPropsBuilder { fields: ((), ), attributes: Vec::new(), _phantom: core::default::Default::default() } + } + } + #[must_use] + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, non_snake_case)] + pub struct FooPropsBuilder<'a, TypedBuilderFields, > { + fields: TypedBuilderFields, + attributes: Vec>, + _phantom: ( core::marker::PhantomData<&'a ()> ), + } + //impl<'a, TypedBuilderFields, > Clone for FooPropsBuilder<'a, TypedBuilderFields, > where TypedBuilderFields: Clone { fn clone(&self) -> Self { Self { fields: self.fields.clone(), attributes: self.attributes, _phantom: Default::default() } } } + impl<'a> dioxus::prelude::Properties for FooProps<'a> { + type Builder = FooPropsBuilder<'a, ((), ), >; + const IS_STATIC: bool = false; + fn builder() -> Self::Builder { FooProps::builder() } + unsafe fn memoize(&self, other: &Self) -> bool { false } + } + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, non_snake_case)] + pub trait FooPropsBuilder_Optional { fn into_value T>(self, default: F) -> T; } + impl FooPropsBuilder_Optional for () { fn into_value T>(self, default: F) -> T { default() } } + impl FooPropsBuilder_Optional for (T, ) { fn into_value T>(self, _: F) -> T { self.0 } } + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl<'a> FooPropsBuilder<'a, ((), )> { + pub fn open(self, open: &'a str) -> FooPropsBuilder<'a, ((Option<&'a str>, + // pub attributes: Vec>, + ), )> { + let open = (Some(open), ); + let (_, ) = self.fields; + FooPropsBuilder { fields: (open, ), attributes: self.attributes, _phantom: self._phantom } + } + } + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, non_snake_case)] + pub enum FooPropsBuilder_Error_Repeated_field_open {} + #[doc(hidden)] + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl<'a> FooPropsBuilder<'a, ((Option<&'a str>, + // pub attributes: Vec>, + ), )> { + #[deprecated(note = "Repeated field open")] + pub fn open(self, _: FooPropsBuilder_Error_Repeated_field_open) -> FooPropsBuilder<'a, ((Option<&'a str>, + // pub attributes: Vec>, + ), )> { self } + } + #[allow(dead_code, non_camel_case_types, missing_docs)] + impl<'a, __open: FooPropsBuilder_Optional>> FooPropsBuilder<'a, (__open, ), > { + pub fn build(self) -> FooProps<'a> { + let (open, ) = self.fields; + let open = FooPropsBuilder_Optional::into_value(open, || Default::default()); + FooProps { open, attributes: self.attributes } + } + } + // ----- + + impl<'a, A> HasAttributesBox<'a, FooPropsBuilder<'a, (A, )>> for FooPropsBuilder<'a, (A, )> { + fn push_attribute(self, attr: AttributeBox<'a>) -> FooPropsBuilder<'a, (A, )> { + let mut attrs = Vec::from(self.attributes); + attrs.push(attr); + FooPropsBuilder { fields: self.fields, attributes: attrs, _phantom: self._phantom } + } + } + impl ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {} + impl ExtendedAudioMarker for FooPropsBuilder<'_, (A,)> {} + + use dioxus::prelude::*; + use dioxus_html::AudioExtension; + + #[allow(non_snake_case)] + pub fn Foo<'a>(cx: Scope<'a, FooProps<'a>>) -> Element<'a> { + let muted = false; + let attributes = cx.props.attributes; + render! { + // rsx! { + // audio { + // muted: muted, + // } + // } + ::dioxus::core::LazyNodes::new(move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { + static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] }; + let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)]; + for attr in attributes { + attrs.push(__cx.attr(attr.name, attr.value.into_value(__cx.bump()), attr.namespace, attr.volatile)); + }; + ::dioxus::core::VNode { + parent: None, + key: None, + template: std::cell::Cell::new(TEMPLATE), + root_ids: Default::default(), + dynamic_nodes: __cx.bump().alloc([]), + dynamic_attrs: __cx.bump().alloc(attrs), + } + }) + } + } + + rsx! { + Foo { + autoplay: true, + controls: true, + } + }; +} \ No newline at end of file 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..5794fd073 --- /dev/null +++ b/packages/html-internal-macro/src/lib.rs @@ -0,0 +1,83 @@ +use proc_macro::TokenStream; + +use convert_case::{Case, Casing}; +use quote::{quote, TokenStreamExt, ToTokens}; +use syn::{braced, Ident, parse_macro_input, Token}; +use syn::__private::TokenStream2; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; + +#[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.to_string() == "ELEMENT", + name, + attrs, + }) + } +} + +impl ToTokens for ImplExtensionAttributes { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + let camel_name = name.to_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()); + let marker_name = Ident::new(format!("Extended{}Marker", &camel_name).as_str(), name.span()); + + if !self.is_element { + tokens.append_all(quote! { + trait #impl_name {} + impl #name for #impl_name {} + }); + } + + let defs = self.attrs.iter().map(|ident| { + quote! { + fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self; + } + }); + let impls = self.attrs.iter().map(|ident| { + let d = if self.is_element { + quote! { #name::#ident } + } else { + quote! { #impl_name::#ident } + }; + quote! { + fn #ident(self, value: impl IntoAttributeValue<'a> + 'static) -> Self { + let d = #d; + self.push_attribute(AttributeBox::new(d.0, value, d.1, d.2)) + } + } + }); + tokens.append_all(quote! { + pub trait #marker_name {} + + pub trait #extension_name<'a> { + #(#defs)* + } + impl<'a, T> #extension_name<'a> for T where T: HasAttributesBox<'a, T> + #marker_name { + #(#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..e71fdf554 --- /dev/null +++ b/packages/html-internal-macro/tests/01-simple.rs @@ -0,0 +1 @@ +fn main() {} \ No newline at end of file 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 86ccb029d..cdf072a7c 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 7e5bed124..e7e682bf5 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1,9 +1,15 @@ #![allow(non_upper_case_globals)] + +use dioxus_core::AttributeBox; +use dioxus_core::HasAttributesBox; +use dioxus_core::prelude::IntoAttributeValue; +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); @@ -106,6 +112,8 @@ macro_rules! impl_element { } impl GlobalAttributes for $name {} + + impl_extension_attributes![ELEMENT $name { $($fil,)* }]; }; ( diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 3792397ad..2c7718686 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -1,5 +1,10 @@ #![allow(non_upper_case_globals)] +use dioxus_core::HasAttributesBox; +use dioxus_core::AttributeBox; +use dioxus_core::prelude::IntoAttributeValue; +use dioxus_html_internal_macro::impl_extension_attributes; + use crate::AttributeDiscription; #[cfg(feature = "hot-reload-context")] @@ -62,6 +67,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/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 2ceb5f038..97678cf3a 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -73,6 +73,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { attributes, key: None, brace: Default::default(), + extra_attributes: None, })) } diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 8f45d91ff..dd33c825f 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -21,6 +21,7 @@ pub struct Element { pub attributes: Vec, pub children: Vec, pub brace: syn::token::Brace, + pub extra_attributes: Option, } impl Parse for Element { @@ -35,6 +36,7 @@ impl Parse for Element { let mut children: Vec = vec![]; let mut key = None; let mut _el_ref = None; + let mut extra_attributes = None; // parse fields with commas // break when we don't get this pattern anymore @@ -42,6 +44,11 @@ impl Parse for Element { // "def": 456, // abc: 123, loop { + if content.peek(Token![...]) { + content.parse::()?; + extra_attributes = Some(content.parse::()?); + } + // Parse the raw literal fields if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { let name = content.parse::()?; @@ -160,6 +167,7 @@ impl Parse for Element { attributes, children, brace, + extra_attributes }) } }