mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-16 13:48:26 +00:00
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:
parent
c2b131f249
commit
95976d9a17
8 changed files with 863 additions and 1134 deletions
1721
Cargo.lock
generated
1721
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -19,7 +19,8 @@ fn app() -> Element {
|
|||
extra_data: "hello{1}",
|
||||
extra_data2: "hello{2}",
|
||||
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 {
|
||||
fn push_attribute(
|
||||
mut self,
|
||||
name: &'static str,
|
||||
ns: Option<&'static str>,
|
||||
attr: impl dioxus_core::prelude::IntoAttributeValue,
|
||||
volatile: bool
|
||||
____name: &'static str,
|
||||
____ns: Option<&'static str>,
|
||||
____attr: impl dioxus_core::prelude::IntoAttributeValue,
|
||||
____volatile: bool
|
||||
) -> Self {
|
||||
let ( #(#descructuring,)* ) = self.fields;
|
||||
self.#field_name.push(
|
||||
dioxus_core::Attribute::new(
|
||||
name,
|
||||
____name,
|
||||
{
|
||||
use dioxus_core::prelude::IntoAttributeValue;
|
||||
attr.into_value()
|
||||
____attr.into_value()
|
||||
},
|
||||
ns,
|
||||
volatile,
|
||||
____ns,
|
||||
____volatile,
|
||||
)
|
||||
);
|
||||
#builder_name {
|
||||
|
|
|
@ -91,6 +91,7 @@ impl Document for ServerDocument {
|
|||
http_equiv: props.http_equiv,
|
||||
content: props.content,
|
||||
property: props.property,
|
||||
..props.additional_attributes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -98,7 +99,7 @@ impl Document for ServerDocument {
|
|||
fn create_script(&self, props: ScriptProps) {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
let children = props.script_contents();
|
||||
let children = props.script_contents().ok();
|
||||
self.0.borrow_mut().script.push(rsx! {
|
||||
script {
|
||||
src: props.src,
|
||||
|
@ -110,11 +111,46 @@ impl Document for ServerDocument {
|
|||
nonce: props.nonce,
|
||||
referrerpolicy: props.referrerpolicy,
|
||||
r#type: props.r#type,
|
||||
..props.additional_attributes,
|
||||
{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) {
|
||||
self.warn_if_streaming();
|
||||
self.serialize_for_hydration();
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||
|
||||
use crate as dioxus_elements;
|
||||
use dioxus_core::{prelude::*, DynamicNode};
|
||||
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 {
|
||||
Element::Ok(vnode) => vnode,
|
||||
Element::Err(err) => {
|
||||
tracing::error!("Error while rendering {component}: {err}");
|
||||
return None;
|
||||
return Err(ExtractSingleTextNodeError::RenderError(err));
|
||||
}
|
||||
};
|
||||
// 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: &[],
|
||||
attr_paths: &[],
|
||||
..
|
||||
} => Some(text.to_string()),
|
||||
} => Ok(text.to_string()),
|
||||
// rsx! { "title: {dynamic_text}" }
|
||||
Template {
|
||||
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];
|
||||
match node {
|
||||
DynamicNode::Text(text) => Some(text.value.clone()),
|
||||
_ => {
|
||||
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
|
||||
}
|
||||
DynamicNode::Text(text) => Ok(text.value.clone()),
|
||||
_ => Err(ExtractSingleTextNodeError::NonTextNode),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::error!(
|
||||
"Error while rendering title: The children of title must be a single text node"
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => Err(ExtractSingleTextNodeError::NonTemplate),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,8 +113,12 @@ pub struct TitleProps {
|
|||
#[component]
|
||||
pub fn Title(props: TitleProps) -> Element {
|
||||
let children = props.children;
|
||||
let Some(text) = extract_single_text_node(&children, "Title") else {
|
||||
return VNode::empty();
|
||||
let text = match extract_single_text_node(&children) {
|
||||
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
|
||||
|
@ -112,6 +139,7 @@ pub fn Title(props: TitleProps) -> Element {
|
|||
VNode::empty()
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
/// Props for the [`Meta`] component
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct MetaProps {
|
||||
|
@ -120,6 +148,8 @@ pub struct MetaProps {
|
|||
pub charset: Option<String>,
|
||||
pub http_equiv: Option<String>,
|
||||
pub content: Option<String>,
|
||||
#[props(extends = meta, extends = GlobalAttributes)]
|
||||
pub additional_attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl MetaProps {
|
||||
|
@ -179,6 +209,7 @@ pub fn Meta(props: MetaProps) -> Element {
|
|||
VNode::empty()
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct ScriptProps {
|
||||
/// 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 referrerpolicy: Option<String>,
|
||||
pub r#type: Option<String>,
|
||||
#[props(extends = script, extends = GlobalAttributes)]
|
||||
pub additional_attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl ScriptProps {
|
||||
|
@ -228,8 +261,8 @@ impl ScriptProps {
|
|||
attributes
|
||||
}
|
||||
|
||||
pub fn script_contents(&self) -> Option<String> {
|
||||
extract_single_text_node(&self.children, "Script")
|
||||
pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||
extract_single_text_node(&self.children)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,6 +310,7 @@ pub fn Script(props: ScriptProps) -> Element {
|
|||
VNode::empty()
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct StyleProps {
|
||||
/// Styles are deduplicated by their href attribute
|
||||
|
@ -286,6 +320,8 @@ pub struct StyleProps {
|
|||
pub title: Option<String>,
|
||||
/// The contents of the style tag. If present, the children must be a single text node.
|
||||
pub children: Element,
|
||||
#[props(extends = style, extends = GlobalAttributes)]
|
||||
pub additional_attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl StyleProps {
|
||||
|
@ -306,8 +342,8 @@ impl StyleProps {
|
|||
attributes
|
||||
}
|
||||
|
||||
pub fn style_contents(&self) -> Option<String> {
|
||||
extract_single_text_node(&self.children, "Title")
|
||||
pub fn style_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||
extract_single_text_node(&self.children)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,6 +393,7 @@ pub fn Style(props: StyleProps) -> Element {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct LinkProps {
|
||||
pub rel: Option<String>,
|
||||
|
@ -374,6 +411,8 @@ pub struct LinkProps {
|
|||
pub integrity: Option<String>,
|
||||
pub r#type: Option<String>,
|
||||
pub blocking: Option<String>,
|
||||
#[props(extends = link, extends = GlobalAttributes)]
|
||||
pub additional_attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl LinkProps {
|
||||
|
|
|
@ -61,14 +61,37 @@ pub trait Document {
|
|||
/// Create a new script tag
|
||||
fn create_script(&self, props: ScriptProps) {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Create a new style tag
|
||||
fn create_style(&self, props: StyleProps) {
|
||||
let attributes = props.attributes();
|
||||
let js = create_element_in_head("style", &attributes, props.style_contents());
|
||||
let mut attributes = props.attributes();
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ use std::{collections::HashSet, vec};
|
|||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
token, AngleBracketedGenericArguments, Expr, PathArguments, Result,
|
||||
token, AngleBracketedGenericArguments, Expr, Ident, PathArguments, Result,
|
||||
};
|
||||
|
||||
#[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
|
||||
/// diagnostic, thankfully
|
||||
///
|
||||
/// Also ensure there's no stringly typed props
|
||||
fn validate_fields(&mut self) {
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for field in self.fields.iter() {
|
||||
match &field.name {
|
||||
AttributeName::Custom(name) => self.diagnostics.push(
|
||||
name.span()
|
||||
.error("Custom attributes are not supported for Components. Only known attributes are allowed."),
|
||||
),
|
||||
AttributeName::Custom(_) => {}
|
||||
AttributeName::BuiltIn(k) => {
|
||||
if !seen.contains(k) {
|
||||
seen.insert(k);
|
||||
} else {
|
||||
self.diagnostics.push(
|
||||
k.span()
|
||||
.error("Duplicate prop field found. Only one prop field per name is allowed."),
|
||||
);
|
||||
self.diagnostics.push(k.span().error(
|
||||
"Duplicate prop field found. Only one prop field per name is allowed.",
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
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) }
|
||||
};
|
||||
|
||||
for (name, value) in self.make_field_idents() {
|
||||
if manual_props.is_some() {
|
||||
tokens.append_all(quote! { __manual_props.#name = #value; })
|
||||
} else {
|
||||
tokens.append_all(quote! { .#name(#value) })
|
||||
}
|
||||
}
|
||||
tokens.append_all(self.add_fields_to_builder(
|
||||
manual_props.map(|_| Ident::new("__manual_props", proc_macro2::Span::call_site())),
|
||||
));
|
||||
|
||||
if !self.children.is_empty() {
|
||||
let children = &self.children;
|
||||
|
@ -259,11 +251,11 @@ impl Component {
|
|||
.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;
|
||||
self.component_props()
|
||||
.map(|attribute| {
|
||||
let release_value = attribute.value.to_token_stream();
|
||||
let mut tokens = TokenStream2::new();
|
||||
for attribute in self.component_props() {
|
||||
let release_value = attribute.value.to_token_stream();
|
||||
|
||||
// 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 {
|
||||
|
@ -286,9 +278,29 @@ impl Component {
|
|||
release_value
|
||||
};
|
||||
|
||||
(attribute.name.to_token_stream(), value)
|
||||
})
|
||||
.collect()
|
||||
match &attribute.name {
|
||||
AttributeName::BuiltIn(name) => {
|
||||
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 {
|
||||
|
|
43
packages/ssr/tests/spread.rs
Normal file
43
packages/ssr/tests/spread.rs
Normal file
|
@ -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}" }
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue