Skip to content

Commit

Permalink
Extend head components with global attributes (#2888)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ealmloff authored Oct 1, 2024
1 parent c2b131f commit 95976d9
Show file tree
Hide file tree
Showing 8 changed files with 867 additions and 1,138 deletions.
1,729 changes: 652 additions & 1,077 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion examples/spread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions packages/core-macro/src/props/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 37 additions & 1 deletion packages/fullstack/src/document/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,15 @@ impl Document for ServerDocument {
http_equiv: props.http_equiv,
content: props.content,
property: props.property,
..props.additional_attributes
}
});
}

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,
Expand All @@ -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();
Expand Down
81 changes: 60 additions & 21 deletions packages/html/src/document/head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand All @@ -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:
Expand All @@ -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 }],
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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>,
Expand All @@ -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 {
Expand Down
29 changes: 26 additions & 3 deletions packages/html/src/document/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading

0 comments on commit 95976d9

Please sign in to comment.