瀏覽代碼

feat: props spread

Koji AGAWA 1 年之前
父節點
當前提交
d573f5dfd5

+ 2 - 0
Cargo.toml

@@ -9,6 +9,7 @@ members = [
     "packages/extension",
     "packages/router",
     "packages/html",
+    "packages/html-internal-macro",
     "packages/hooks",
     "packages/web",
     "packages/ssr",
@@ -60,6 +61,7 @@ dioxus-core-macro = { path = "packages/core-macro", version = "0.4.0"  }
 dioxus-router = { path = "packages/router", version = "0.4.1"  }
 dioxus-router-macro = { path = "packages/router-macro", version = "0.4.1" }
 dioxus-html = { path = "packages/html", 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"  }

+ 1 - 0
packages/autofmt/src/element.rs

@@ -49,6 +49,7 @@ impl Writer<'_> {
             attributes,
             children,
             brace,
+            extra_attributes,
         } = el;
 
         /*

+ 2 - 2
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,

+ 31 - 0
packages/core/src/nodes.rs

@@ -832,3 +832,34 @@ impl<'a, T: IntoAttributeValue<'a>> IntoAttributeValue<'a> for Option<T> {
         }
     }
 }
+
+pub struct AttributeBox<'a> {
+    /// The name of the attribute.
+    pub name: &'a str,
+
+    /// The value of the attribute
+    pub value: Box<dyn IntoAttributeValue<'a> + '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;
+}

+ 121 - 0
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<AttributeBox<'a>>,
+    }
+
+    // -----
+    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<AttributeBox<'a>>,
+        _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<T> { fn into_value<F: FnOnce() -> T>(self, default: F) -> T; }
+    impl<T> FooPropsBuilder_Optional<T> for () { fn into_value<F: FnOnce() -> T>(self, default: F) -> T { default() } }
+    impl<T> FooPropsBuilder_Optional<T> for (T, ) { fn into_value<F: FnOnce() -> 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<Attribute<'a>>,
+                                                                 ), )> {
+            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<Attribute<'a>>,
+                                  ), )> {
+        #[deprecated(note = "Repeated field open")]
+        pub fn open(self, _: FooPropsBuilder_Error_Repeated_field_open) -> FooPropsBuilder<'a, ((Option<&'a str>,
+                                                                                                 // pub attributes: Vec<Attribute<'a>>,
+                                                                                                ), )> { self }
+    }
+    #[allow(dead_code, non_camel_case_types, missing_docs)]
+    impl<'a, __open: FooPropsBuilder_Optional<Option<&'a str>>> 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<A,> ExtendedGlobalAttributesMarker for FooPropsBuilder<'_, (A,)> {}
+    impl<A,> 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,
+        }
+    };
+}

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

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

+ 83 - 0
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<Ident, Token![,]>,
+}
+
+impl Parse for ImplExtensionAttributes {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let content;
+
+        let element: Ident = input.parse()?;
+        let name = input.parse()?;
+        braced!(content in input);
+        let attrs = content.parse_terminated(Ident::parse, Token![,])?;
+
+        Ok(ImplExtensionAttributes {
+            is_element: element.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)*
+            }
+        });
+    }
+}

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

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

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

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

+ 1 - 0
packages/html/Cargo.toml

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

+ 10 - 2
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,)* }];
     };
 
     (

+ 7 - 0
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

+ 1 - 0
packages/rsx-rosetta/src/lib.rs

@@ -73,6 +73,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                 attributes,
                 key: None,
                 brace: Default::default(),
+                extra_attributes: None,
             }))
         }
 

+ 8 - 0
packages/rsx/src/element.rs

@@ -21,6 +21,7 @@ pub struct Element {
     pub attributes: Vec<ElementAttrNamed>,
     pub children: Vec<BodyNode>,
     pub brace: syn::token::Brace,
+    pub extra_attributes: Option<Expr>,
 }
 
 impl Parse for Element {
@@ -35,6 +36,7 @@ impl Parse for Element {
         let mut children: Vec<BodyNode> = 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::<Token![...]>()?;
+                extra_attributes = Some(content.parse::<Expr>()?);
+            }
+
             // Parse the raw literal fields
             if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
                 let name = content.parse::<LitStr>()?;
@@ -160,6 +167,7 @@ impl Parse for Element {
             attributes,
             children,
             brace,
+            extra_attributes
         })
     }
 }