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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user