Prechádzať zdrojové kódy

Merge branch 'many_optional_attributes' into pr/atty303/1349

Evan Almloff 1 rok pred
rodič
commit
9e167dfdb7

+ 5 - 0
examples/rsx_usage.rs

@@ -53,6 +53,7 @@ fn App(cx: Scope) -> Element {
     let formatting = "formatting!";
     let formatting_tuple = ("a", "b");
     let lazy_fmt = format_args!("lazily formatted text");
+    let asd = 123;
     cx.render(rsx! {
         div {
             // Elements
@@ -80,6 +81,10 @@ fn App(cx: Scope) -> Element {
                 // pass simple rust expressions in
                 class: lazy_fmt,
                 id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
+                class: "asd",
+                class: "{asd}",
+                // if statements can be used to conditionally render attributes
+                class: if formatting.contains("form") { "{asd}" },
                 div {
                     class: {
                         const WORD: &str = "expressions";

+ 5 - 1
examples/tailwind/src/main.rs

@@ -14,9 +14,13 @@ fn main() {
 }
 
 pub fn app(cx: Scope) -> Element {
+    let grey_background = true;
     cx.render(rsx!(
         div {
-            header { class: "text-gray-400 bg-gray-900 body-font",
+            header {
+                class: "text-gray-400 body-font",
+                // you can use optional attributes to optionally apply a tailwind class
+                class: if grey_background { "bg-gray-900" },
                 div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
                     a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
                         StacksIcon {}

+ 41 - 25
packages/autofmt/src/element.rs

@@ -50,6 +50,7 @@ impl Writer<'_> {
             children,
             brace,
             extra_attributes,
+            ..
         } = el;
 
         /*
@@ -210,12 +211,34 @@ impl Writer<'_> {
         Ok(())
     }
 
-    fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
-        match &attr.attr {
-            ElementAttr::AttrText { name, value } => {
-                write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?;
+    fn write_attribute_name(&mut self, attr: &ElementAttrName) -> Result {
+        match attr {
+            ElementAttrName::BuiltIn(name) => {
+                write!(self.out, "{}", name)?;
+            }
+            ElementAttrName::Custom(name) => {
+                write!(self.out, "{}", name.to_token_stream())?;
             }
-            ElementAttr::AttrExpression { name, value } => {
+        }
+
+        Ok(())
+    }
+
+    fn write_attribute_value(&mut self, value: &ElementAttrValue) -> Result {
+        match value {
+            ElementAttrValue::AttrOptionalExpr { condition, value } => {
+                write!(
+                    self.out,
+                    "if {condition} {{ ",
+                    condition = prettyplease::unparse_expr(condition),
+                )?;
+                self.write_attribute_value(value)?;
+                write!(self.out, " }}")?;
+            }
+            ElementAttrValue::AttrLiteral(value) => {
+                write!(self.out, "{value}", value = ifmt_to_string(value))?;
+            }
+            ElementAttrValue::AttrExpr(value) => {
                 let out = prettyplease::unparse_expr(value);
                 let mut lines = out.split('\n').peekable();
                 let first = lines.next().unwrap();
@@ -223,9 +246,9 @@ impl Writer<'_> {
                 // a one-liner for whatever reason
                 // Does not need a new line
                 if lines.peek().is_none() {
-                    write!(self.out, "{name}: {first}")?;
+                    write!(self.out, "{first}")?;
                 } else {
-                    writeln!(self.out, "{name}: {first}")?;
+                    writeln!(self.out, "{first}")?;
 
                     while let Some(line) = lines.next() {
                         self.out.indented_tab()?;
@@ -238,22 +261,7 @@ impl Writer<'_> {
                     }
                 }
             }
-
-            ElementAttr::CustomAttrText { name, value } => {
-                write!(
-                    self.out,
-                    "{name}: {value}",
-                    name = name.to_token_stream(),
-                    value = ifmt_to_string(value)
-                )?;
-            }
-
-            ElementAttr::CustomAttrExpression { name, value } => {
-                let out = prettyplease::unparse_expr(value);
-                write!(self.out, "{}: {}", name.to_token_stream(), out)?;
-            }
-
-            ElementAttr::EventTokens { name, tokens } => {
+            ElementAttrValue::EventTokens(tokens) => {
                 let out = self.retrieve_formatted_expr(tokens).to_string();
 
                 let mut lines = out.split('\n').peekable();
@@ -262,9 +270,9 @@ impl Writer<'_> {
                 // a one-liner for whatever reason
                 // Does not need a new line
                 if lines.peek().is_none() {
-                    write!(self.out, "{name}: {first}")?;
+                    write!(self.out, "{first}")?;
                 } else {
-                    writeln!(self.out, "{name}: {first}")?;
+                    writeln!(self.out, "{first}")?;
 
                     while let Some(line) = lines.next() {
                         self.out.indented_tab()?;
@@ -282,6 +290,14 @@ impl Writer<'_> {
         Ok(())
     }
 
+    fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
+        self.write_attribute_name(&attr.attr.name)?;
+        write!(self.out, ": ")?;
+        self.write_attribute_value(&attr.attr.value)?;
+
+        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 {

+ 44 - 34
packages/autofmt/src/writer.rs

@@ -1,4 +1,4 @@
-use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop};
+use dioxus_rsx::{BodyNode, ElementAttrNamed, ElementAttrValue, ForLoop};
 use proc_macro2::{LineColumn, Span};
 use quote::ToTokens;
 use std::{
@@ -132,6 +132,39 @@ impl<'a> Writer<'a> {
         Ok(())
     }
 
+    pub(crate) fn attr_value_len(&mut self, value: &ElementAttrValue) -> usize {
+        match value {
+            ElementAttrValue::AttrOptionalExpr { condition, value } => {
+                let condition_len = self.retrieve_formatted_expr(condition).len();
+                let value_len = self.attr_value_len(value);
+
+                condition_len + value_len + 6
+            }
+            ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
+            ElementAttrValue::AttrExpr(expr) => expr.span().line_length(),
+            ElementAttrValue::EventTokens(tokens) => {
+                let location = Location::new(tokens.span().start());
+
+                let len = if let std::collections::hash_map::Entry::Vacant(e) =
+                    self.cached_formats.entry(location)
+                {
+                    let formatted = prettyplease::unparse_expr(tokens);
+                    let len = if formatted.contains('\n') {
+                        10000
+                    } else {
+                        formatted.len()
+                    };
+                    e.insert(formatted);
+                    len
+                } else {
+                    self.cached_formats[&location].len()
+                };
+
+                len
+            }
+        }
+    }
+
     pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
         let mut total = 0;
 
@@ -146,40 +179,17 @@ impl<'a> Writer<'a> {
                 }
             }
 
-            total += match &attr.attr {
-                ElementAttr::AttrText { value, name } => {
-                    ifmt_to_string(value).len() + name.span().line_length() + 6
-                }
-                ElementAttr::AttrExpression { name, value } => {
-                    value.span().line_length() + name.span().line_length() + 6
-                }
-                ElementAttr::CustomAttrText { value, name } => {
-                    ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6
-                }
-                ElementAttr::CustomAttrExpression { name, value } => {
-                    name.to_token_stream().to_string().len() + value.span().line_length() + 6
-                }
-                ElementAttr::EventTokens { tokens, name } => {
-                    let location = Location::new(tokens.span().start());
-
-                    let len = if let std::collections::hash_map::Entry::Vacant(e) =
-                        self.cached_formats.entry(location)
-                    {
-                        let formatted = prettyplease::unparse_expr(tokens);
-                        let len = if formatted.contains('\n') {
-                            10000
-                        } else {
-                            formatted.len()
-                        };
-                        e.insert(formatted);
-                        len
-                    } else {
-                        self.cached_formats[&location].len()
-                    };
-
-                    len + name.span().line_length() + 6
+            total += 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 += self.attr_value_len(&attr.attr.value);
+
+            total += 6;
         }
 
         total
@@ -218,7 +228,7 @@ impl<'a> Writer<'a> {
     }
 }
 
-trait SpanLength {
+pub(crate) trait SpanLength {
     fn line_length(&self) -> usize;
 }
 impl SpanLength for Span {

+ 1 - 1
packages/autofmt/tests/samples/simple.rsx

@@ -33,7 +33,7 @@ rsx! {
     }
 
     // No children, minimal props
-    img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
+    img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png" }
 
     // One level compression
     div {

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

@@ -851,6 +851,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
     }
 }
 
+impl<'a> IntoAttributeValue<'a> for String {
+    fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> {
+        AttributeValue::Text(bump.alloc(self))
+    }
+}
+
 impl<'a> IntoAttributeValue<'a> for f64 {
     fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
         AttributeValue::Float(self)

+ 4 - 1
packages/core/tests/kitchen_sink.rs

@@ -10,6 +10,8 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
         div { key: "12345",
             class: "asd",
             class: "{asd}",
+            class: if true { "{asd}" },
+            class: if false { "{asd}" },
             onclick: move |_| {},
             div { "{var}" }
             div {
@@ -24,6 +26,7 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element {
         }
     })
 }
+
 #[test]
 fn dual_stream() {
     let mut dom = VirtualDom::new(basic_syntax_is_a_template);
@@ -36,7 +39,7 @@ fn dual_stream() {
             LoadTemplate { name: "template", index: 0, id: ElementId(1) },
             SetAttribute {
                 name: "class",
-                value: (&*bump.alloc("123".into_value(&bump))).into(),
+                value: (&*bump.alloc("asd 123 123".into_value(&bump))).into(),
                 id: ElementId(1),
                 ns: None,
             },

+ 18 - 9
packages/rsx-rosetta/src/lib.rs

@@ -36,9 +36,11 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
 
                     ElementAttrNamed {
                         el_name: el_name.clone(),
-                        attr: ElementAttr::AttrText {
-                            value: ifmt_from_text(value.as_deref().unwrap_or("false")),
-                            name: ident,
+                        attr: ElementAttr {
+                            value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(
+                                value.as_deref().unwrap_or("false"),
+                            )),
+                            name: dioxus_rsx::ElementAttrName::BuiltIn(ident),
                         },
                     }
                 })
@@ -48,9 +50,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
             if !class.is_empty() {
                 attributes.push(ElementAttrNamed {
                     el_name: el_name.clone(),
-                    attr: ElementAttr::AttrText {
-                        name: Ident::new("class", Span::call_site()),
-                        value: ifmt_from_text(&class),
+                    attr: ElementAttr {
+                        name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
+                            "class",
+                            Span::call_site(),
+                        )),
+                        value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(&class)),
                     },
                 });
             }
@@ -58,9 +63,12 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
             if let Some(id) = &el.id {
                 attributes.push(ElementAttrNamed {
                     el_name: el_name.clone(),
-                    attr: ElementAttr::AttrText {
-                        name: Ident::new("id", Span::call_site()),
-                        value: ifmt_from_text(id),
+                    attr: ElementAttr {
+                        name: dioxus_rsx::ElementAttrName::BuiltIn(Ident::new(
+                            "id",
+                            Span::call_site(),
+                        )),
+                        value: dioxus_rsx::ElementAttrValue::AttrLiteral(ifmt_from_text(id)),
                     },
                 });
             }
@@ -71,6 +79,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
                 name: el_name,
                 children,
                 attributes,
+                merged_attributes: Default::default(),
                 key: None,
                 brace: Default::default(),
                 extra_attributes: None,

+ 2 - 0
packages/rsx/Cargo.toml

@@ -22,5 +22,7 @@ internment = { version = "0.7.0", optional = true }
 krates = { version = "0.12.6", optional = true }
 
 [features]
+default = ["html"]
 hot_reload = ["krates", "internment"]
 serde = ["dep:serde"]
+html = []

+ 309 - 0
packages/rsx/src/attribute.rs

@@ -0,0 +1,309 @@
+use std::fmt::{Display, Formatter};
+
+use super::*;
+
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::{quote, ToTokens, TokenStreamExt};
+use syn::{parse_quote, Expr, ExprIf, Ident, LitStr};
+
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+pub struct ElementAttrNamed {
+    pub el_name: ElementName,
+    pub attr: ElementAttr,
+}
+
+impl ElementAttrNamed {
+    pub(crate) fn try_combine(&self, other: &Self) -> Option<Self> {
+        if self.el_name == other.el_name && self.attr.name == other.attr.name {
+            if let Some(separator) = self.attr.name.multi_attribute_separator() {
+                return Some(ElementAttrNamed {
+                    el_name: self.el_name.clone(),
+                    attr: ElementAttr {
+                        name: self.attr.name.clone(),
+                        value: self.attr.value.combine(separator, &other.attr.value),
+                    },
+                });
+            }
+        }
+        None
+    }
+}
+
+impl ToTokens for ElementAttrNamed {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let ElementAttrNamed { el_name, attr } = self;
+
+        let ns = |name: &ElementAttrName| match (el_name, name) {
+            (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => {
+                quote! { dioxus_elements::#i::#name.1 }
+            }
+            _ => quote! { None },
+        };
+        let volitile = |name: &ElementAttrName| match (el_name, name) {
+            (ElementName::Ident(i), ElementAttrName::BuiltIn(_)) => {
+                quote! { dioxus_elements::#i::#name.2 }
+            }
+            _ => quote! { false },
+        };
+        let attribute = |name: &ElementAttrName| match name {
+            ElementAttrName::BuiltIn(name) => match el_name {
+                ElementName::Ident(_) => quote! { #el_name::#name.0 },
+                ElementName::Custom(_) => {
+                    let as_string = name.to_string();
+                    quote!(#as_string)
+                }
+            },
+            ElementAttrName::Custom(s) => quote! { #s },
+        };
+
+        let attribute = {
+            match &attr.value {
+                ElementAttrValue::AttrLiteral(_)
+                | ElementAttrValue::AttrExpr(_)
+                | ElementAttrValue::AttrOptionalExpr { .. } => {
+                    let name = &self.attr.name;
+                    let ns = ns(name);
+                    let volitile = volitile(name);
+                    let attribute = attribute(name);
+                    let value = &self.attr.value;
+                    let value = quote! { #value };
+                    quote! {
+                        __cx.attr(
+                            #attribute,
+                            #value,
+                            #ns,
+                            #volitile
+                        )
+                    }
+                }
+                ElementAttrValue::EventTokens(tokens) => match &self.attr.name {
+                    ElementAttrName::BuiltIn(name) => {
+                        quote! {
+                            dioxus_elements::events::#name(__cx, #tokens)
+                        }
+                    }
+                    ElementAttrName::Custom(_) => todo!(),
+                },
+            }
+        };
+
+        tokens.append_all(attribute);
+    }
+}
+
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+pub struct ElementAttr {
+    pub name: ElementAttrName,
+    pub value: ElementAttrValue,
+}
+
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+pub enum ElementAttrValue {
+    /// attribute: "value"
+    AttrLiteral(IfmtInput),
+    /// attribute: if bool { "value" }
+    AttrOptionalExpr {
+        condition: Expr,
+        value: Box<ElementAttrValue>,
+    },
+    /// attribute: true
+    AttrExpr(Expr),
+    /// onclick: move |_| {}
+    EventTokens(Expr),
+}
+
+impl Parse for ElementAttrValue {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        Ok(if input.peek(Token![if]) {
+            let if_expr = input.parse::<ExprIf>()?;
+            if is_if_chain_terminated(&if_expr) {
+                ElementAttrValue::AttrExpr(Expr::If(if_expr))
+            } else {
+                ElementAttrValue::AttrOptionalExpr {
+                    condition: *if_expr.cond,
+                    value: {
+                        let stmts = if_expr.then_branch.stmts;
+                        Box::new(syn::parse2(quote! {
+                            #(#stmts)*
+                        })?)
+                    },
+                }
+            }
+        } else if input.peek(LitStr) {
+            let value = input.parse()?;
+            ElementAttrValue::AttrLiteral(value)
+        } else {
+            let value = input.parse::<Expr>()?;
+            ElementAttrValue::AttrExpr(value)
+        })
+    }
+}
+
+impl ToTokens for ElementAttrValue {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match self {
+            ElementAttrValue::AttrLiteral(lit) => tokens.append_all(quote! { #lit }),
+            ElementAttrValue::AttrOptionalExpr { condition, value } => {
+                tokens.append_all(quote! { if #condition { Some(#value) } else { None } })
+            }
+            ElementAttrValue::AttrExpr(expr) => tokens.append_all(quote! { #expr }),
+            ElementAttrValue::EventTokens(expr) => tokens.append_all(quote! { #expr }),
+        }
+    }
+}
+
+impl ElementAttrValue {
+    fn to_str_expr(&self) -> Option<TokenStream2> {
+        match self {
+            ElementAttrValue::AttrLiteral(lit) => Some(quote!(#lit.to_string())),
+            ElementAttrValue::AttrOptionalExpr { value, .. } => value.to_str_expr(),
+            ElementAttrValue::AttrExpr(expr) => Some(quote!(#expr.to_string())),
+            _ => None,
+        }
+    }
+
+    fn combine(&self, separator: &str, other: &Self) -> Self {
+        match (self, other) {
+            (Self::AttrLiteral(lit1), Self::AttrLiteral(lit2)) => {
+                let fmt = lit1.clone().join(lit2.clone(), separator);
+                Self::AttrLiteral(fmt)
+            }
+            (Self::AttrLiteral(expr1), Self::AttrExpr(expr2)) => {
+                let mut ifmt = expr1.clone();
+                ifmt.push_str(separator);
+                ifmt.push_expr(expr2.clone());
+                Self::AttrLiteral(ifmt)
+            }
+            (Self::AttrExpr(expr1), Self::AttrLiteral(expr2)) => {
+                let mut ifmt = expr2.clone();
+                ifmt.push_str(separator);
+                ifmt.push_expr(expr1.clone());
+                Self::AttrLiteral(ifmt)
+            }
+            (Self::AttrExpr(expr1), Self::AttrExpr(expr2)) => {
+                let mut ifmt = IfmtInput::default();
+                ifmt.push_expr(expr1.clone());
+                ifmt.push_str(separator);
+                ifmt.push_expr(expr2.clone());
+                Self::AttrLiteral(ifmt)
+            }
+            (
+                Self::AttrOptionalExpr {
+                    condition: condition1,
+                    value: value1,
+                },
+                Self::AttrOptionalExpr {
+                    condition: condition2,
+                    value: value2,
+                },
+            ) => {
+                let first_as_string = value1.to_str_expr();
+                let second_as_string = value2.to_str_expr();
+                Self::AttrExpr(parse_quote! {
+                    {
+                        let mut __combined = String::new();
+                        if #condition1 {
+                            __combined.push_str(&#first_as_string);
+                        }
+                        if #condition2 {
+                            if __combined.len() > 0 {
+                                __combined.push_str(&#separator);
+                            }
+                            __combined.push_str(&#second_as_string);
+                        }
+                        __combined
+                    }
+                })
+            }
+            (Self::AttrOptionalExpr { condition, value }, other) => {
+                let first_as_string = value.to_str_expr();
+                let second_as_string = other.to_str_expr();
+                Self::AttrExpr(parse_quote! {
+                    {
+                        let mut __combined = #second_as_string;
+                        if #condition {
+                            __combined.push_str(&#separator);
+                            __combined.push_str(&#first_as_string);
+                        }
+                        __combined
+                    }
+                })
+            }
+            (other, Self::AttrOptionalExpr { condition, value }) => {
+                let first_as_string = other.to_str_expr();
+                let second_as_string = value.to_str_expr();
+                Self::AttrExpr(parse_quote! {
+                    {
+                        let mut __combined = #first_as_string;
+                        if #condition {
+                            __combined.push_str(&#separator);
+                            __combined.push_str(&#second_as_string);
+                        }
+                        __combined
+                    }
+                })
+            }
+            _ => todo!(),
+        }
+    }
+}
+
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+pub enum ElementAttrName {
+    BuiltIn(Ident),
+    Custom(LitStr),
+}
+
+impl ElementAttrName {
+    fn multi_attribute_separator(&self) -> Option<&'static str> {
+        match self {
+            ElementAttrName::BuiltIn(i) => match i.to_string().as_str() {
+                "class" => Some(" "),
+                "style" => Some(";"),
+                _ => None,
+            },
+            ElementAttrName::Custom(_) => None,
+        }
+    }
+
+    pub fn start(&self) -> Span {
+        match self {
+            ElementAttrName::BuiltIn(i) => i.span(),
+            ElementAttrName::Custom(s) => s.span(),
+        }
+    }
+}
+
+impl ToTokens for ElementAttrName {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match self {
+            ElementAttrName::BuiltIn(i) => tokens.append_all(quote! { #i }),
+            ElementAttrName::Custom(s) => tokens.append_all(quote! { #s }),
+        }
+    }
+}
+
+impl Display for ElementAttrName {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ElementAttrName::BuiltIn(i) => write!(f, "{}", i),
+            ElementAttrName::Custom(s) => write!(f, "{}", s.value()),
+        }
+    }
+}
+
+impl ElementAttr {
+    pub fn start(&self) -> Span {
+        self.name.start()
+    }
+
+    pub fn is_expr(&self) -> bool {
+        matches!(
+            self,
+            ElementAttr {
+                value: ElementAttrValue::AttrExpr(_) | ElementAttrValue::EventTokens(_),
+                ..
+            }
+        )
+    }
+}

+ 43 - 170
packages/rsx/src/element.rs

@@ -8,7 +8,7 @@ use syn::{
     parse::{Parse, ParseBuffer, ParseStream},
     punctuated::Punctuated,
     spanned::Spanned,
-    Error, Expr, Ident, LitStr, Result, Token,
+    Ident, LitStr, Result, Token,
 };
 
 // =======================================
@@ -19,6 +19,7 @@ pub struct Element {
     pub name: ElementName,
     pub key: Option<IfmtInput>,
     pub attributes: Vec<ElementAttrNamed>,
+    pub merged_attributes: Vec<ElementAttrNamed>,
     pub children: Vec<BodyNode>,
     pub brace: syn::token::Brace,
     pub extra_attributes: Option<Expr>,
@@ -35,8 +36,6 @@ impl Parse for Element {
         let mut attributes: Vec<ElementAttrNamed> = vec![];
         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
@@ -56,19 +55,14 @@ impl Parse for Element {
 
                 content.parse::<Token![:]>()?;
 
-                if content.peek(LitStr) {
-                    let value = content.parse()?;
-                    attributes.push(ElementAttrNamed {
-                        el_name: el_name.clone(),
-                        attr: ElementAttr::CustomAttrText { name, value },
-                    });
-                } else {
-                    let value = content.parse::<Expr>()?;
-                    attributes.push(ElementAttrNamed {
-                        el_name: el_name.clone(),
-                        attr: ElementAttr::CustomAttrExpression { name, value },
-                    });
-                }
+                let value = content.parse::<ElementAttrValue>()?;
+                attributes.push(ElementAttrNamed {
+                    el_name: el_name.clone(),
+                    attr: ElementAttr {
+                        name: ElementAttrName::Custom(name),
+                        value,
+                    },
+                });
 
                 if content.is_empty() {
                     break;
@@ -93,9 +87,9 @@ impl Parse for Element {
                 if name_str.starts_with("on") {
                     attributes.push(ElementAttrNamed {
                         el_name: el_name.clone(),
-                        attr: ElementAttr::EventTokens {
-                            name,
-                            tokens: content.parse()?,
+                        attr: ElementAttr {
+                            name: ElementAttrName::BuiltIn(name),
+                            value: ElementAttrValue::EventTokens(content.parse()?),
                         },
                     });
                 } else {
@@ -103,29 +97,15 @@ impl Parse for Element {
                         "key" => {
                             key = Some(content.parse()?);
                         }
-                        "classes" => todo!("custom class list not supported yet"),
-                        // "namespace" => todo!("custom namespace not supported yet"),
-                        "node_ref" => {
-                            _el_ref = Some(content.parse::<Expr>()?);
-                        }
                         _ => {
-                            if content.peek(LitStr) {
-                                attributes.push(ElementAttrNamed {
-                                    el_name: el_name.clone(),
-                                    attr: ElementAttr::AttrText {
-                                        name,
-                                        value: content.parse()?,
-                                    },
-                                });
-                            } else {
-                                attributes.push(ElementAttrNamed {
-                                    el_name: el_name.clone(),
-                                    attr: ElementAttr::AttrExpression {
-                                        name,
-                                        value: content.parse()?,
-                                    },
-                                });
-                            }
+                            let value = content.parse::<ElementAttrValue>()?;
+                            attributes.push(ElementAttrNamed {
+                                el_name: el_name.clone(),
+                                attr: ElementAttr {
+                                    name: ElementAttrName::BuiltIn(name),
+                                    value,
+                                },
+                            });
                         }
                     }
                 }
@@ -144,6 +124,23 @@ impl Parse for Element {
             break;
         }
 
+        // 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();
+        for attr in &attributes {
+            if let Some(old_attr_index) = merged_attributes
+                .iter()
+                .position(|a| a.attr.name == attr.attr.name)
+            {
+                let old_attr = &mut merged_attributes[old_attr_index];
+                if let Some(combined) = old_attr.try_combine(attr) {
+                    *old_attr = combined;
+                }
+            } else {
+                merged_attributes.push(attr.clone());
+            }
+        }
+
         while !content.is_empty() {
             if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
                 attr_after_element!(content.span());
@@ -165,6 +162,7 @@ impl Parse for Element {
             key,
             name: el_name,
             attributes,
+            merged_attributes,
             children,
             brace,
             extra_attributes,
@@ -183,14 +181,14 @@ impl ToTokens for Element {
         };
 
         let listeners = self
-            .attributes
+            .merged_attributes
             .iter()
-            .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. }));
+            .filter(|f| matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
 
         let attr = self
-            .attributes
+            .merged_attributes
             .iter()
-            .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. }));
+            .filter(|f| !matches!(f.attr.value, ElementAttrValue::EventTokens { .. }));
 
         tokens.append_all(quote! {
             __cx.element(
@@ -272,128 +270,3 @@ impl ToTokens for ElementName {
         }
     }
 }
-
-#[derive(PartialEq, Eq, Clone, Debug, Hash)]
-pub enum ElementAttr {
-    /// `attribute: "value"`
-    AttrText { name: Ident, value: IfmtInput },
-
-    /// `attribute: true`
-    AttrExpression { name: Ident, value: Expr },
-
-    /// `"attribute": "value"`
-    CustomAttrText { name: LitStr, value: IfmtInput },
-
-    /// `"attribute": true`
-    CustomAttrExpression { name: LitStr, value: Expr },
-
-    // /// onclick: move |_| {}
-    // EventClosure { name: Ident, closure: ExprClosure },
-    /// onclick: {}
-    EventTokens { name: Ident, tokens: Expr },
-}
-
-impl ElementAttr {
-    pub fn start(&self) -> Span {
-        match self {
-            ElementAttr::AttrText { name, .. } => name.span(),
-            ElementAttr::AttrExpression { name, .. } => name.span(),
-            ElementAttr::CustomAttrText { name, .. } => name.span(),
-            ElementAttr::CustomAttrExpression { name, .. } => name.span(),
-            ElementAttr::EventTokens { name, .. } => name.span(),
-        }
-    }
-
-    pub fn is_expr(&self) -> bool {
-        matches!(
-            self,
-            ElementAttr::AttrExpression { .. }
-                | ElementAttr::CustomAttrExpression { .. }
-                | ElementAttr::EventTokens { .. }
-        )
-    }
-}
-
-#[derive(PartialEq, Eq, Clone, Debug, Hash)]
-pub struct ElementAttrNamed {
-    pub el_name: ElementName,
-    pub attr: ElementAttr,
-}
-
-impl ToTokens for ElementAttrNamed {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        let ElementAttrNamed { el_name, attr } = self;
-
-        let ns = |name| match el_name {
-            ElementName::Ident(i) => quote! { dioxus_elements::#i::#name.1 },
-            ElementName::Custom(_) => quote! { None },
-        };
-        let volitile = |name| match el_name {
-            ElementName::Ident(_) => quote! { #el_name::#name.2 },
-            ElementName::Custom(_) => quote! { false },
-        };
-        let attribute = |name: &Ident| match el_name {
-            ElementName::Ident(_) => quote! { #el_name::#name.0 },
-            ElementName::Custom(_) => {
-                let as_string = name.to_string();
-                quote!(#as_string)
-            }
-        };
-
-        let attribute = match attr {
-            ElementAttr::AttrText { name, value } => {
-                let ns = ns(name);
-                let volitile = volitile(name);
-                let attribute = attribute(name);
-                quote! {
-                    __cx.attr(
-                        #attribute,
-                        #value,
-                        #ns,
-                        #volitile
-                    )
-                }
-            }
-            ElementAttr::AttrExpression { name, value } => {
-                let ns = ns(name);
-                let volitile = volitile(name);
-                let attribute = attribute(name);
-                quote! {
-                    __cx.attr(
-                        #attribute,
-                        #value,
-                        #ns,
-                        #volitile
-                    )
-                }
-            }
-            ElementAttr::CustomAttrText { name, value } => {
-                quote! {
-                    __cx.attr(
-                        #name,
-                        #value,
-                        None,
-                        false
-                    )
-                }
-            }
-            ElementAttr::CustomAttrExpression { name, value } => {
-                quote! {
-                    __cx.attr(
-                        #name,
-                        #value,
-                        None,
-                        false
-                    )
-                }
-            }
-            ElementAttr::EventTokens { name, tokens } => {
-                quote! {
-                    dioxus_elements::events::#name(__cx, #tokens)
-                }
-            }
-        };
-
-        tokens.append_all(attribute);
-    }
-}

+ 2 - 2
packages/rsx/src/errors.rs

@@ -1,12 +1,12 @@
 macro_rules! missing_trailing_comma {
     ($span:expr) => {
-        return Err(Error::new($span, "missing trailing comma"));
+        return Err(syn::Error::new($span, "missing trailing comma"));
     };
 }
 
 macro_rules! attr_after_element {
     ($span:expr) => {
-        return Err(Error::new($span, "expected element\n  = help move the attribute above all the children and text elements"));
+        return Err(syn::Error::new($span, "expected element\n  = help move the attribute above all the children and text elements"));
     };
 }
 

+ 23 - 2
packages/rsx/src/ifmt.rs

@@ -13,7 +13,7 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result<TokenStream> {
 }
 
 #[allow(dead_code)] // dumb compiler does not see the struct being used...
-#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
 pub struct IfmtInput {
     pub source: Option<LitStr>,
     pub segments: Vec<Segment>,
@@ -27,8 +27,29 @@ impl IfmtInput {
         }
     }
 
+    pub fn join(mut self, other: Self, separator: &str) -> Self {
+        if !self.segments.is_empty() {
+            self.segments.push(Segment::Literal(separator.to_string()));
+        }
+        self.segments.extend(other.segments);
+        self
+    }
+
+    pub fn push_expr(&mut self, expr: Expr) {
+        self.segments.push(Segment::Formatted(FormattedSegment {
+            format_args: String::new(),
+            segment: FormattedSegmentType::Expr(Box::new(expr)),
+        }));
+    }
+
+    pub fn push_str(&mut self, s: &str) {
+        self.segments.push(Segment::Literal(s.to_string()));
+    }
+
     pub fn is_static(&self) -> bool {
-        matches!(self.segments.as_slice(), &[Segment::Literal(_)] | &[])
+        self.segments
+            .iter()
+            .all(|seg| matches!(seg, Segment::Literal(_)))
     }
 }
 

+ 48 - 72
packages/rsx/src/lib.rs

@@ -13,6 +13,7 @@
 
 #[macro_use]
 mod errors;
+mod attribute;
 mod component;
 mod element;
 #[cfg(feature = "hot_reload")]
@@ -23,6 +24,7 @@ mod node;
 use std::{fmt::Debug, hash::Hash};
 
 // Re-export the namespaces into each other
+pub use attribute::*;
 pub use component::*;
 #[cfg(feature = "hot_reload")]
 use dioxus_core::{Template, TemplateAttribute, TemplateNode};
@@ -306,17 +308,10 @@ impl DynamicMapping {
     fn add_node(&mut self, node: BodyNode) {
         match node {
             BodyNode::Element(el) => {
-                for attr in el.attributes {
-                    match &attr.attr {
-                        ElementAttr::CustomAttrText { value, .. }
-                        | ElementAttr::AttrText { value, .. }
-                            if value.is_static() => {}
-
-                        ElementAttr::AttrExpression { .. }
-                        | ElementAttr::AttrText { .. }
-                        | ElementAttr::CustomAttrText { .. }
-                        | ElementAttr::CustomAttrExpression { .. }
-                        | ElementAttr::EventTokens { .. } => {
+                for attr in el.merged_attributes {
+                    match &attr.attr.value {
+                        ElementAttrValue::AttrLiteral(input) if input.is_static() => {}
+                        _ => {
                             self.insert_attribute(attr.attr);
                         }
                     }
@@ -364,10 +359,11 @@ impl<'a> DynamicContext<'a> {
                 let element_name_rust = el.name.to_string();
 
                 let mut static_attrs = Vec::new();
-                for attr in &el.attributes {
-                    match &attr.attr {
-                        ElementAttr::AttrText { name, value } if value.is_static() => {
+                for attr in &el.merged_attributes {
+                    match &attr.attr.value {
+                        ElementAttrValue::AttrLiteral(value) 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)
@@ -379,20 +375,7 @@ impl<'a> DynamicContext<'a> {
                             })
                         }
 
-                        ElementAttr::CustomAttrText { name, value } if value.is_static() => {
-                            let value = value.source.as_ref().unwrap();
-                            static_attrs.push(TemplateAttribute::Static {
-                                name: intern(name.value().as_str()),
-                                namespace: None,
-                                value: intern(value.value().as_str()),
-                            })
-                        }
-
-                        ElementAttr::AttrExpression { .. }
-                        | ElementAttr::AttrText { .. }
-                        | ElementAttr::CustomAttrText { .. }
-                        | ElementAttr::CustomAttrExpression { .. }
-                        | ElementAttr::EventTokens { .. } => {
+                        _ => {
                             let idx = match mapping {
                                 Some(mapping) => mapping.get_attribute_idx(&attr.attr)?,
                                 None => self.dynamic_attributes.len(),
@@ -464,54 +447,47 @@ impl<'a> DynamicContext<'a> {
                     ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
                     ElementName::Custom(_) => quote! { None },
                 };
-                let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
-                    ElementAttr::AttrText { name, value } if value.is_static() => {
-                        let value = value.to_static().unwrap();
-                        let ns = ns(quote!(#name.1));
-                        let name = match el_name {
-                            ElementName::Ident(_) => quote! { #el_name::#name.0 },
-                            ElementName::Custom(_) => {
-                                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.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,
+                                }
                             }
                         }
-                    }
-
-                    ElementAttr::CustomAttrText { name, value } if value.is_static() => {
-                        let value = value.to_static().unwrap();
-                        quote! {
-                            ::dioxus::core::TemplateAttribute::Static {
-                                name: #name,
-                                namespace: None,
-                                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 } }
                         }
-                    }
-
-                    ElementAttr::AttrExpression { .. }
-                    | ElementAttr::AttrText { .. }
-                    | ElementAttr::CustomAttrText { .. }
-                    | ElementAttr::CustomAttrExpression { .. }
-                    | ElementAttr::EventTokens { .. } => {
-                        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),*};
 

+ 1 - 1
packages/rsx/src/node.rs

@@ -247,7 +247,7 @@ impl Parse for ForLoop {
     }
 }
 
-fn is_if_chain_terminated(chain: &ExprIf) -> bool {
+pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool {
     let mut current = chain;
     loop {
         if let Some((_, else_block)) = &current.else_branch {

+ 2 - 2
packages/ssr/src/renderer.rs

@@ -216,7 +216,7 @@ fn to_string_works() {
             assert_eq!(
                 item.1.segments,
                 vec![
-                    PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
+                    PreRendered("<div class=\"asdasdasd asdasdasd\"".into(),),
                     Attr(0,),
                     StyleMarker {
                         inside_style_tag: false,
@@ -238,7 +238,7 @@ fn to_string_works() {
 
     use Segment::*;
 
-    assert_eq!(out, "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
+    assert_eq!(out, "<div class=\"asdasdasd asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
 }
 
 pub(crate) const BOOL_ATTRS: &[&str] = &[