Browse Source

implement spreading props in the rsx macro

Evan Almloff 1 năm trước cách đây
mục cha
commit
5b65c4cfb4

+ 1 - 1
examples/spread.rs

@@ -73,7 +73,7 @@ impl<'a> HasAttributesBox<'a, Props<'a>> for Props<'a> {
 impl ExtendedGlobalAttributesMarker for Props<'_> {}
 
 fn Component<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> {
-    let attributes = &cx.props.attributes;
+    let attributes = &*cx.props.attributes;
     render! {
         audio {
             ..attributes,

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

@@ -49,7 +49,6 @@ impl Writer<'_> {
             attributes,
             children,
             brace,
-            extra_attributes,
             ..
         } = el;
 
@@ -167,7 +166,7 @@ impl Writer<'_> {
 
     fn write_attributes(
         &mut self,
-        attributes: &[ElementAttrNamed],
+        attributes: &[AttributeType],
         key: &Option<IfmtInput>,
         sameline: bool,
     ) -> Result {
@@ -189,7 +188,7 @@ impl Writer<'_> {
         while let Some(attr) = attr_iter.next() {
             self.out.indent += 1;
             if !sameline {
-                self.write_comments(attr.attr.start())?;
+                self.write_comments(attr.start())?;
             }
             self.out.indent -= 1;
 
@@ -290,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)?;
@@ -298,6 +304,13 @@ impl Writer<'_> {
         Ok(())
     }
 
+    fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
+        write!(self.out, "..")?;
+        write!(self.out, "{}", prettyplease::unparse_expr(attr))?;
+
+        Ok(())
+    }
+
     // make sure the comments are actually relevant to this element.
     // test by making sure this element is the primary element on this line
     pub fn current_span_is_primary(&self, location: Span) -> bool {

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

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

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

@@ -1,6 +1,7 @@
 use convert_case::{Case, Casing};
 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};
@@ -34,7 +35,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                         Ident::new(new_name.as_str(), Span::call_site())
                     };
 
-                    ElementAttrNamed {
+                    AttributeType::Named(ElementAttrNamed {
                         el_name: el_name.clone(),
                         attr: ElementAttr {
                             value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(
@@ -42,13 +43,13 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                             )),
                             name: dioxus_rsx::ElementAttrName::BuiltIn(ident),
                         },
-                    }
+                    })
                 })
                 .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(
@@ -57,11 +58,11 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                         )),
                         value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)),
                     },
-                });
+                }));
             }
 
             if let Some(id) = &el.id {
-                attributes.push(ElementAttrNamed {
+                attributes.push(AttributeType::Named(ElementAttrNamed {
                     el_name: el_name.clone(),
                     attr: ElementAttr {
                         name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
@@ -70,7 +71,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                         )),
                         value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)),
                     },
-                });
+                }));
             }
 
             let children = el.children.iter().filter_map(rsx_node_from_html).collect();
@@ -82,7 +83,6 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                 merged_attributes: Default::default(),
                 key: None,
                 brace: Default::default(),
-                extra_attributes: None,
             }))
         }
 

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

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

+ 67 - 29
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,11 +18,10 @@ use syn::{
 pub struct Element {
     pub name: ElementName,
     pub key: Option<IfmtInput>,
-    pub attributes: Vec<ElementAttrNamed>,
-    pub merged_attributes: Vec<ElementAttrNamed>,
+    pub attributes: Vec<AttributeType>,
+    pub merged_attributes: Vec<AttributeType>,
     pub children: Vec<BodyNode>,
     pub brace: syn::token::Brace,
-    pub extra_attributes: Option<Expr>,
 }
 
 impl Parse for Element {
@@ -33,7 +32,7 @@ impl Parse for Element {
         let content: ParseBuffer;
         let brace = syn::braced!(content in stream);
 
-        let mut attributes: Vec<ElementAttrNamed> = vec![];
+        let mut attributes: Vec<AttributeType> = vec![];
         let mut children: Vec<BodyNode> = vec![];
         let mut key = None;
 
@@ -43,9 +42,20 @@ impl Parse for Element {
         // "def": 456,
         // abc: 123,
         loop {
-            if content.peek(Token![...]) {
-                content.parse::<Token![...]>()?;
-                extra_attributes = Some(content.parse::<Expr>()?);
+            if content.peek(Token![..]) {
+                content.parse::<Token![..]>()?;
+                let expr = content.parse::<Expr>()?;
+                let span = expr.span();
+                attributes.push(attribute::AttributeType::Spread(expr));
+
+                if content.is_empty() {
+                    break;
+                }
+
+                if content.parse::<Token![,]>().is_err() {
+                    missing_trailing_comma!(span);
+                }
+                continue;
             }
 
             // Parse the raw literal fields
@@ -56,13 +66,13 @@ impl Parse for Element {
                 content.parse::<Token![:]>()?;
 
                 let value = content.parse::<ElementAttrValue>()?;
-                attributes.push(ElementAttrNamed {
+                attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
                     el_name: el_name.clone(),
                     attr: ElementAttr {
                         name: ElementAttrName::Custom(name),
                         value,
                     },
-                });
+                }));
 
                 if content.is_empty() {
                     break;
@@ -85,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" => {
@@ -99,13 +109,13 @@ impl Parse for Element {
                         }
                         _ => {
                             let value = content.parse::<ElementAttrValue>()?;
-                            attributes.push(ElementAttrNamed {
+                            attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
                                 el_name: el_name.clone(),
                                 attr: ElementAttr {
                                     name: ElementAttrName::BuiltIn(name),
                                     value,
                                 },
-                            });
+                            }));
                         }
                     }
                 }
@@ -114,7 +124,6 @@ impl Parse for Element {
                     break;
                 }
 
-                // todo: add a message saying you need to include commas between fields
                 if content.parse::<Token![,]>().is_err() {
                     missing_trailing_comma!(span);
                 }
@@ -126,12 +135,26 @@ impl Parse for Element {
 
         // Deduplicate any attributes that can be combined
         // For example, if there are two `class` attributes, combine them into one
-        let mut merged_attributes: Vec<ElementAttrNamed> = Vec::new();
+        let mut merged_attributes: Vec<AttributeType> = Vec::new();
         for attr in &attributes {
-            if let Some(old_attr_index) = merged_attributes
-                .iter()
-                .position(|a| a.attr.name == attr.attr.name)
-            {
+            if let Some(old_attr_index) = merged_attributes.iter().position(|a| {
+                matches!((a, attr), (
+                                AttributeType::Named(ElementAttrNamed {
+                                    attr: ElementAttr {
+                                        name: ElementAttrName::BuiltIn(old_name),
+                                        ..
+                                    },
+                                    ..
+                                }),
+                                AttributeType::Named(ElementAttrNamed {
+                                    attr: ElementAttr {
+                                        name: ElementAttrName::BuiltIn(new_name),
+                                        ..
+                                    },
+                                    ..
+                                }),
+                            ) if old_name == new_name)
+            }) {
                 let old_attr = &mut merged_attributes[old_attr_index];
                 if let Some(combined) = old_attr.try_combine(attr) {
                     *old_attr = combined;
@@ -165,7 +188,6 @@ impl Parse for Element {
             merged_attributes,
             children,
             brace,
-            extra_attributes,
         })
     }
 }
@@ -180,15 +202,31 @@ impl ToTokens for Element {
             None => quote! { None },
         };
 
-        let listeners = self
-            .merged_attributes
-            .iter()
-            .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
+        let listeners = self.merged_attributes.iter().filter(|f| {
+            matches!(
+                f,
+                AttributeType::Named(ElementAttrNamed {
+                    attr: ElementAttr {
+                        value: ElementAttrValue::EventTokens { .. },
+                        ..
+                    },
+                    ..
+                })
+            )
+        });
 
-        let attr = self
-            .merged_attributes
-            .iter()
-            .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
+        let attr = self.merged_attributes.iter().filter(|f| {
+            !matches!(
+                f,
+                AttributeType::Named(ElementAttrNamed {
+                    attr: ElementAttr {
+                        value: ElementAttrValue::EventTokens { .. },
+                        ..
+                    },
+                    ..
+                })
+            )
+        });
 
         tokens.append_all(quote! {
             __cx.element(

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

@@ -261,7 +261,7 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
 #[cfg(feature = "hot_reload")]
 #[derive(Default, Debug)]
 struct DynamicMapping {
-    attribute_to_idx: std::collections::HashMap<ElementAttr, Vec<usize>>,
+    attribute_to_idx: std::collections::HashMap<AttributeType, Vec<usize>>,
     last_attribute_idx: usize,
     node_to_idx: std::collections::HashMap<BodyNode, Vec<usize>>,
     last_element_idx: usize,
@@ -277,7 +277,7 @@ impl DynamicMapping {
         new
     }
 
-    fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option<usize> {
+    fn get_attribute_idx(&mut self, attr: &AttributeType) -> Option<usize> {
         self.attribute_to_idx
             .get_mut(attr)
             .and_then(|idxs| idxs.pop())
@@ -287,7 +287,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;
 
@@ -309,10 +309,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);
                         }
                     }
                 }
@@ -340,7 +347,7 @@ impl DynamicMapping {
 #[derive(Default, Debug)]
 pub struct DynamicContext<'a> {
     dynamic_nodes: Vec<&'a BodyNode>,
-    dynamic_attributes: Vec<&'a ElementAttrNamed>,
+    dynamic_attributes: Vec<&'a AttributeType>,
     current_path: Vec<u8>,
 
     node_paths: Vec<Vec<u8>>,
@@ -360,10 +367,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)
@@ -377,7 +390,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);
@@ -447,47 +460,50 @@ impl<'a> DynamicContext<'a> {
                     ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
                     ElementName::Custom(_) => quote! { None },
                 };
-                let static_attrs = el
-                    .merged_attributes
-                    .iter()
-                    .map(|attr| match &attr.attr.value {
-                        ElementAttrValue::AttrLiteral(value) if value.is_static() => {
-                            let value = value.to_static().unwrap();
-                            let ns = {
-                                match &attr.attr.name {
-                                    ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
-                                    ElementAttrName::Custom(_) => quote!(None),
-                                }
-                            };
-                            let name = &attr.attr.name;
-                            let name = match (el_name, name) {
-                                (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
-                                    quote! { #el_name::#name.0 }
-                                }
-                                _ => {
-                                    let as_string = name.to_string();
-                                    quote! { #as_string }
-                                }
-                            };
-                            quote! {
-                                ::dioxus::core::TemplateAttribute::Static {
-                                    name: #name,
-                                    namespace: #ns,
-                                    value: #value,
-
-                                    // todo: we don't diff these so we never apply the volatile flag
-                                    // volatile: dioxus_elements::#el_name::#name.2,
-                                }
+                let static_attrs = el.merged_attributes.iter().map(|attr| match attr {
+                    AttributeType::Named(ElementAttrNamed {
+                        attr:
+                            ElementAttr {
+                                value: ElementAttrValue::AttrLiteral(value),
+                                name,
+                            },
+                        ..
+                    }) if value.is_static() => {
+                        let value = value.to_static().unwrap();
+                        let ns = {
+                            match name {
+                                ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
+                                ElementAttrName::Custom(_) => quote!(None),
+                            }
+                        };
+                        let name = match (el_name, name) {
+                            (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
+                                quote! { #el_name::#name.0 }
+                            }
+                            _ => {
+                                let as_string = name.to_string();
+                                quote! { #as_string }
+                            }
+                        };
+                        quote! {
+                            ::dioxus::core::TemplateAttribute::Static {
+                                name: #name,
+                                namespace: #ns,
+                                value: #value,
+
+                                // todo: we don't diff these so we never apply the volatile flag
+                                // volatile: dioxus_elements::#el_name::#name.2,
                             }
                         }
+                    }
 
-                        _ => {
-                            let ct = self.dynamic_attributes.len();
-                            self.dynamic_attributes.push(attr);
-                            self.attr_paths.push(self.current_path.clone());
-                            quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
-                        }
-                    });
+                    _ => {
+                        let ct = self.dynamic_attributes.len();
+                        self.dynamic_attributes.push(attr);
+                        self.attr_paths.push(self.current_path.clone());
+                        quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
+                    }
+                });
 
                 let attrs = quote! { #(#static_attrs),*};