Extend head components with global attributes (#2888)
* Fix hot reloading components with keys * include component formatted segment keys, but not dynamic component value * extend each head component with the corresponding element * Allow spreading custom attributes into components * Fix component_literal_dyn_idx index * add a new test for hot reloading components with keys * FIx script without body warning and rendering styles with a href set * fix clippy
This commit is contained in:
1 parent
c2b131f249
commit
95976d9a17
8 files changed
+863
-1134
No files matched your search
Generated
+648
-1073
File diff suppressed because it is too large.
Load diff
+2
-1
@@ -19,7 +19,8 @@ fn app() -> Element {
|
|||||||
extra_data: "hello{1}",
|
extra_data: "hello{1}",
|
||||||
extra_data2: "hello{2}",
|
extra_data2: "hello{2}",
|
||||||
height: "10px",
|
height: "10px",
|
||||||
left: 1
|
left: 1,
|
||||||
|
"data-custom-attribute": "value",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1017,21 +1017,21 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||||||
impl #impl_generics dioxus_core::prelude::HasAttributes for #builder_name < #( #ty_generics ),* > #where_clause {
|
impl #impl_generics dioxus_core::prelude::HasAttributes for #builder_name < #( #ty_generics ),* > #where_clause {
|
||||||
fn push_attribute(
|
fn push_attribute(
|
||||||
mut self,
|
mut self,
|
||||||
name: &'static str,
|
____name: &'static str,
|
||||||
ns: Option<&'static str>,
|
____ns: Option<&'static str>,
|
||||||
attr: impl dioxus_core::prelude::IntoAttributeValue,
|
____attr: impl dioxus_core::prelude::IntoAttributeValue,
|
||||||
volatile: bool
|
____volatile: bool
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ( #(#descructuring,)* ) = self.fields;
|
let ( #(#descructuring,)* ) = self.fields;
|
||||||
self.#field_name.push(
|
self.#field_name.push(
|
||||||
dioxus_core::Attribute::new(
|
dioxus_core::Attribute::new(
|
||||||
name,
|
____name,
|
||||||
{
|
{
|
||||||
use dioxus_core::prelude::IntoAttributeValue;
|
use dioxus_core::prelude::IntoAttributeValue;
|
||||||
attr.into_value()
|
____attr.into_value()
|
||||||
},
|
},
|
||||||
ns,
|
____ns,
|
||||||
volatile,
|
____volatile,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#builder_name {
|
#builder_name {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ impl Document for ServerDocument {
|
|||||||
http_equiv: props.http_equiv,
|
http_equiv: props.http_equiv,
|
||||||
content: props.content,
|
content: props.content,
|
||||||
property: props.property,
|
property: props.property,
|
||||||
|
..props.additional_attributes
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ impl Document for ServerDocument {
|
|||||||
fn create_script(&self, props: ScriptProps) {
|
fn create_script(&self, props: ScriptProps) {
|
||||||
self.warn_if_streaming();
|
self.warn_if_streaming();
|
||||||
self.serialize_for_hydration();
|
self.serialize_for_hydration();
|
||||||
let children = props.script_contents();
|
let children = props.script_contents().ok();
|
||||||
self.0.borrow_mut().script.push(rsx! {
|
self.0.borrow_mut().script.push(rsx! {
|
||||||
script {
|
script {
|
||||||
src: props.src,
|
src: props.src,
|
||||||
@@ -110,11 +111,46 @@ impl Document for ServerDocument {
|
|||||||
nonce: props.nonce,
|
nonce: props.nonce,
|
||||||
referrerpolicy: props.referrerpolicy,
|
referrerpolicy: props.referrerpolicy,
|
||||||
r#type: props.r#type,
|
r#type: props.r#type,
|
||||||
|
..props.additional_attributes,
|
||||||
{children}
|
{children}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_style(&self, props: StyleProps) {
|
||||||
|
self.warn_if_streaming();
|
||||||
|
self.serialize_for_hydration();
|
||||||
|
match (&props.href, props.style_contents()) {
|
||||||
|
// The style has inline contents, render it as a style tag
|
||||||
|
(_, Ok(contents)) => self.0.borrow_mut().script.push(rsx! {
|
||||||
|
style {
|
||||||
|
media: props.media,
|
||||||
|
nonce: props.nonce,
|
||||||
|
title: props.title,
|
||||||
|
..props.additional_attributes,
|
||||||
|
{contents}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// The style has a href, render it as a link tag
|
||||||
|
(Some(_), _) => {
|
||||||
|
self.0.borrow_mut().script.push(rsx! {
|
||||||
|
link {
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: props.href,
|
||||||
|
media: props.media,
|
||||||
|
nonce: props.nonce,
|
||||||
|
title: props.title,
|
||||||
|
..props.additional_attributes,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The style has neither contents nor src, log an error
|
||||||
|
(None, Err(err)) => {
|
||||||
|
err.log("Style");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn create_link(&self, props: head::LinkProps) {
|
fn create_link(&self, props: head::LinkProps) {
|
||||||
self.warn_if_streaming();
|
self.warn_if_streaming();
|
||||||
self.serialize_for_hydration();
|
self.serialize_for_hydration();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
|
use crate as dioxus_elements;
|
||||||
use dioxus_core::{prelude::*, DynamicNode};
|
use dioxus_core::{prelude::*, DynamicNode};
|
||||||
use dioxus_core_macro::*;
|
use dioxus_core_macro::*;
|
||||||
|
|
||||||
@@ -19,12 +20,42 @@ fn use_update_warning<T: PartialEq + Clone + 'static>(value: &T, name: &'static
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_single_text_node(children: &Element, component: &str) -> Option<String> {
|
/// An error that can occur when extracting a single text node from a component
|
||||||
|
pub enum ExtractSingleTextNodeError<'a> {
|
||||||
|
/// The node contained an render error, so we can't extract the text node
|
||||||
|
RenderError(&'a RenderError),
|
||||||
|
/// There was only one child, but it wasn't a text node
|
||||||
|
NonTextNode,
|
||||||
|
/// There is multiple child nodes
|
||||||
|
NonTemplate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtractSingleTextNodeError<'_> {
|
||||||
|
/// Log a warning depending on the error
|
||||||
|
pub fn log(&self, component: &str) {
|
||||||
|
match self {
|
||||||
|
ExtractSingleTextNodeError::RenderError(err) => {
|
||||||
|
tracing::error!("Error while rendering {component}: {err}");
|
||||||
|
}
|
||||||
|
ExtractSingleTextNodeError::NonTextNode => {
|
||||||
|
tracing::error!(
|
||||||
|
"Error while rendering {component}: The children of {component} must be a single text node"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ExtractSingleTextNodeError::NonTemplate => {
|
||||||
|
tracing::error!(
|
||||||
|
"Error while rendering {component}: The children of {component} must be a single text node"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_single_text_node(children: &Element) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||||
let vnode = match children {
|
let vnode = match children {
|
||||||
Element::Ok(vnode) => vnode,
|
Element::Ok(vnode) => vnode,
|
||||||
Element::Err(err) => {
|
Element::Err(err) => {
|
||||||
tracing::error!("Error while rendering {component}: {err}");
|
return Err(ExtractSingleTextNodeError::RenderError(err));
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// The title's children must be in one of two forms:
|
// The title's children must be in one of two forms:
|
||||||
@@ -37,7 +68,7 @@ fn extract_single_text_node(children: &Element, component: &str) -> Option<Strin
|
|||||||
node_paths: &[],
|
node_paths: &[],
|
||||||
attr_paths: &[],
|
attr_paths: &[],
|
||||||
..
|
..
|
||||||
} => Some(text.to_string()),
|
} => Ok(text.to_string()),
|
||||||
// rsx! { "title: {dynamic_text}" }
|
// rsx! { "title: {dynamic_text}" }
|
||||||
Template {
|
Template {
|
||||||
roots: &[TemplateNode::Dynamic { id }],
|
roots: &[TemplateNode::Dynamic { id }],
|
||||||
@@ -47,19 +78,11 @@ fn extract_single_text_node(children: &Element, component: &str) -> Option<Strin
|
|||||||
} => {
|
} => {
|
||||||
let node = &vnode.dynamic_nodes[id];
|
let node = &vnode.dynamic_nodes[id];
|
||||||
match node {
|
match node {
|
||||||
DynamicNode::Text(text) => Some(text.value.clone()),
|
DynamicNode::Text(text) => Ok(text.value.clone()),
|
||||||
_ => {
|
_ => Err(ExtractSingleTextNodeError::NonTextNode),
|
||||||
tracing::error!("Error while rendering {component}: The children of {component} must be a single text node. It cannot be a component, if statement, loop, or a fragment");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => Err(ExtractSingleTextNodeError::NonTemplate),
|
||||||
tracing::error!(
|
|
||||||
"Error while rendering title: The children of title must be a single text node"
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +113,12 @@ pub struct TitleProps {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn Title(props: TitleProps) -> Element {
|
pub fn Title(props: TitleProps) -> Element {
|
||||||
let children = props.children;
|
let children = props.children;
|
||||||
let Some(text) = extract_single_text_node(&children, "Title") else {
|
let text = match extract_single_text_node(&children) {
|
||||||
return VNode::empty();
|
Ok(text) => text,
|
||||||
|
Err(err) => {
|
||||||
|
err.log("Title");
|
||||||
|
return VNode::empty();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
|
// Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
|
||||||
@@ -112,6 +139,7 @@ pub fn Title(props: TitleProps) -> Element {
|
|||||||
VNode::empty()
|
VNode::empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
/// Props for the [`Meta`] component
|
/// Props for the [`Meta`] component
|
||||||
#[derive(Clone, Props, PartialEq)]
|
#[derive(Clone, Props, PartialEq)]
|
||||||
pub struct MetaProps {
|
pub struct MetaProps {
|
||||||
@@ -120,6 +148,8 @@ pub struct MetaProps {
|
|||||||
pub charset: Option<String>,
|
pub charset: Option<String>,
|
||||||
pub http_equiv: Option<String>,
|
pub http_equiv: Option<String>,
|
||||||
pub content: Option<String>,
|
pub content: Option<String>,
|
||||||
|
#[props(extends = meta, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetaProps {
|
impl MetaProps {
|
||||||
@@ -179,6 +209,7 @@ pub fn Meta(props: MetaProps) -> Element {
|
|||||||
VNode::empty()
|
VNode::empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Props, PartialEq)]
|
#[derive(Clone, Props, PartialEq)]
|
||||||
pub struct ScriptProps {
|
pub struct ScriptProps {
|
||||||
/// The contents of the script tag. If present, the children must be a single text node.
|
/// The contents of the script tag. If present, the children must be a single text node.
|
||||||
@@ -193,6 +224,8 @@ pub struct ScriptProps {
|
|||||||
pub nonce: Option<String>,
|
pub nonce: Option<String>,
|
||||||
pub referrerpolicy: Option<String>,
|
pub referrerpolicy: Option<String>,
|
||||||
pub r#type: Option<String>,
|
pub r#type: Option<String>,
|
||||||
|
#[props(extends = script, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptProps {
|
impl ScriptProps {
|
||||||
@@ -228,8 +261,8 @@ impl ScriptProps {
|
|||||||
attributes
|
attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn script_contents(&self) -> Option<String> {
|
pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||||
extract_single_text_node(&self.children, "Script")
|
extract_single_text_node(&self.children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +310,7 @@ pub fn Script(props: ScriptProps) -> Element {
|
|||||||
VNode::empty()
|
VNode::empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Props, PartialEq)]
|
#[derive(Clone, Props, PartialEq)]
|
||||||
pub struct StyleProps {
|
pub struct StyleProps {
|
||||||
/// Styles are deduplicated by their href attribute
|
/// Styles are deduplicated by their href attribute
|
||||||
@@ -286,6 +320,8 @@ pub struct StyleProps {
|
|||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
/// The contents of the style tag. If present, the children must be a single text node.
|
/// The contents of the style tag. If present, the children must be a single text node.
|
||||||
pub children: Element,
|
pub children: Element,
|
||||||
|
#[props(extends = style, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyleProps {
|
impl StyleProps {
|
||||||
@@ -306,8 +342,8 @@ impl StyleProps {
|
|||||||
attributes
|
attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn style_contents(&self) -> Option<String> {
|
pub fn style_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||||
extract_single_text_node(&self.children, "Title")
|
extract_single_text_node(&self.children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +393,7 @@ pub fn Style(props: StyleProps) -> Element {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Props, PartialEq)]
|
#[derive(Clone, Props, PartialEq)]
|
||||||
pub struct LinkProps {
|
pub struct LinkProps {
|
||||||
pub rel: Option<String>,
|
pub rel: Option<String>,
|
||||||
@@ -374,6 +411,8 @@ pub struct LinkProps {
|
|||||||
pub integrity: Option<String>,
|
pub integrity: Option<String>,
|
||||||
pub r#type: Option<String>,
|
pub r#type: Option<String>,
|
||||||
pub blocking: Option<String>,
|
pub blocking: Option<String>,
|
||||||
|
#[props(extends = link, extends = GlobalAttributes)]
|
||||||
|
pub additional_attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LinkProps {
|
impl LinkProps {
|
||||||
|
|||||||
@@ -61,14 +61,37 @@ pub trait Document {
|
|||||||
/// Create a new script tag
|
/// Create a new script tag
|
||||||
fn create_script(&self, props: ScriptProps) {
|
fn create_script(&self, props: ScriptProps) {
|
||||||
let attributes = props.attributes();
|
let attributes = props.attributes();
|
||||||
let js = create_element_in_head("script", &attributes, props.script_contents());
|
let js = match (&props.src, props.script_contents()) {
|
||||||
|
// The script has inline contents, render it as a script tag
|
||||||
|
(_, Ok(contents)) => create_element_in_head("script", &attributes, Some(contents)),
|
||||||
|
// The script has a src, render it as a script tag without a body
|
||||||
|
(Some(_), _) => create_element_in_head("script", &attributes, None),
|
||||||
|
// The script has neither contents nor src, log an error
|
||||||
|
(None, Err(err)) => {
|
||||||
|
err.log("Script");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
self.new_evaluator(js);
|
self.new_evaluator(js);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new style tag
|
/// Create a new style tag
|
||||||
fn create_style(&self, props: StyleProps) {
|
fn create_style(&self, props: StyleProps) {
|
||||||
let attributes = props.attributes();
|
let mut attributes = props.attributes();
|
||||||
let js = create_element_in_head("style", &attributes, props.style_contents());
|
let js = match (&props.href, props.style_contents()) {
|
||||||
|
// The style has inline contents, render it as a style tag
|
||||||
|
(_, Ok(contents)) => create_element_in_head("style", &attributes, Some(contents)),
|
||||||
|
// The style has a src, render it as a link tag
|
||||||
|
(Some(_), _) => {
|
||||||
|
attributes.push(("type", "text/css".into()));
|
||||||
|
create_element_in_head("link", &attributes, None)
|
||||||
|
}
|
||||||
|
// The style has neither contents nor src, log an error
|
||||||
|
(None, Err(err)) => {
|
||||||
|
err.log("Style");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
self.new_evaluator(js);
|
self.new_evaluator(js);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use std::{collections::HashSet, vec};
|
|||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
token, AngleBracketedGenericArguments, Expr, PathArguments, Result,
|
token, AngleBracketedGenericArguments, Expr, Ident, PathArguments, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
@@ -179,29 +179,25 @@ impl Component {
|
|||||||
|
|
||||||
/// Ensure there's no duplicate props - this will be a compile error but we can move it to a
|
/// Ensure there's no duplicate props - this will be a compile error but we can move it to a
|
||||||
/// diagnostic, thankfully
|
/// diagnostic, thankfully
|
||||||
///
|
|
||||||
/// Also ensure there's no stringly typed props
|
|
||||||
fn validate_fields(&mut self) {
|
fn validate_fields(&mut self) {
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
|
|
||||||
for field in self.fields.iter() {
|
for field in self.fields.iter() {
|
||||||
match &field.name {
|
match &field.name {
|
||||||
AttributeName::Custom(name) => self.diagnostics.push(
|
AttributeName::Custom(_) => {}
|
||||||
name.span()
|
|
||||||
.error("Custom attributes are not supported for Components. Only known attributes are allowed."),
|
|
||||||
),
|
|
||||||
AttributeName::BuiltIn(k) => {
|
AttributeName::BuiltIn(k) => {
|
||||||
if !seen.contains(k) {
|
if !seen.contains(k) {
|
||||||
seen.insert(k);
|
seen.insert(k);
|
||||||
} else {
|
} else {
|
||||||
self.diagnostics.push(
|
self.diagnostics.push(k.span().error(
|
||||||
k.span()
|
"Duplicate prop field found. Only one prop field per name is allowed.",
|
||||||
.error("Duplicate prop field found. Only one prop field per name is allowed."),
|
));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
AttributeName::Spread(_) => {
|
AttributeName::Spread(_) => {
|
||||||
unreachable!("Spread attributes should be handled in the spread validation step.")
|
unreachable!(
|
||||||
|
"Spread attributes should be handled in the spread validation step."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,13 +218,9 @@ impl Component {
|
|||||||
quote! { fc_to_builder(#name #generics) }
|
quote! { fc_to_builder(#name #generics) }
|
||||||
};
|
};
|
||||||
|
|
||||||
for (name, value) in self.make_field_idents() {
|
tokens.append_all(self.add_fields_to_builder(
|
||||||
if manual_props.is_some() {
|
manual_props.map(|_| Ident::new("__manual_props", proc_macro2::Span::call_site())),
|
||||||
tokens.append_all(quote! { __manual_props.#name = #value; })
|
));
|
||||||
} else {
|
|
||||||
tokens.append_all(quote! { .#name(#value) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.children.is_empty() {
|
if !self.children.is_empty() {
|
||||||
let children = &self.children;
|
let children = &self.children;
|
||||||
@@ -259,11 +251,11 @@ impl Component {
|
|||||||
.filter(move |attr| !attr.name.is_likely_key())
|
.filter(move |attr| !attr.name.is_likely_key())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_field_idents(&self) -> Vec<(TokenStream2, TokenStream2)> {
|
fn add_fields_to_builder(&self, manual_props: Option<Ident>) -> TokenStream2 {
|
||||||
let mut dynamic_literal_index = 0;
|
let mut dynamic_literal_index = 0;
|
||||||
self.component_props()
|
let mut tokens = TokenStream2::new();
|
||||||
.map(|attribute| {
|
for attribute in self.component_props() {
|
||||||
let release_value = attribute.value.to_token_stream();
|
let release_value = attribute.value.to_token_stream();
|
||||||
|
|
||||||
// In debug mode, we try to grab the value from the dynamic literal pool if possible
|
// In debug mode, we try to grab the value from the dynamic literal pool if possible
|
||||||
let value = if let AttributeValue::AttrLiteral(literal) = &attribute.value {
|
let value = if let AttributeValue::AttrLiteral(literal) = &attribute.value {
|
||||||
@@ -286,9 +278,29 @@ impl Component {
|
|||||||
release_value
|
release_value
|
||||||
};
|
};
|
||||||
|
|
||||||
(attribute.name.to_token_stream(), value)
|
match &attribute.name {
|
||||||
})
|
AttributeName::BuiltIn(name) => {
|
||||||
.collect()
|
if let Some(manual_props) = &manual_props {
|
||||||
|
tokens.append_all(quote! { #manual_props.#name = #value; })
|
||||||
|
} else {
|
||||||
|
tokens.append_all(quote! { .#name(#value) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttributeName::Custom(name) => {
|
||||||
|
if manual_props.is_some() {
|
||||||
|
tokens.append_all(name.span().error(
|
||||||
|
"Custom attributes are not supported for components that are spread",
|
||||||
|
).emit_as_expr_tokens());
|
||||||
|
} else {
|
||||||
|
tokens.append_all(quote! { .push_attribute(#name, None, #value, false) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// spreads are handled elsewhere
|
||||||
|
AttributeName::Spread(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty(name: syn::Path, generics: Option<AngleBracketedGenericArguments>) -> Self {
|
fn empty(name: syn::Path, generics: Option<AngleBracketedGenericArguments>) -> Self {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spread() {
|
||||||
|
let dom = VirtualDom::prebuilt(app);
|
||||||
|
let html = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
html,
|
||||||
|
r#"<audio data-custom-attribute="value" style="width:10px;height:10px;left:1;">1: hello1
|
||||||
|
2: hello2</audio>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
rsx! {
|
||||||
|
SpreadableComponent {
|
||||||
|
width: "10px",
|
||||||
|
extra_data: "hello{1}",
|
||||||
|
extra_data2: "hello{2}",
|
||||||
|
height: "10px",
|
||||||
|
left: 1,
|
||||||
|
"data-custom-attribute": "value",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, PartialEq, Clone)]
|
||||||
|
struct Props {
|
||||||
|
#[props(extends = GlobalAttributes)]
|
||||||
|
attributes: Vec<Attribute>,
|
||||||
|
|
||||||
|
extra_data: String,
|
||||||
|
|
||||||
|
extra_data2: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn SpreadableComponent(props: Props) -> Element {
|
||||||
|
rsx! {
|
||||||
|
audio { ..props.attributes, "1: {props.extra_data}\n2: {props.extra_data2}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user