Move the document trait into a separate crate (#3035)
* add a default head method through eval * remove the old document trait * implement document for each platform * pull out document into a dedicated crate to cut down on shared dependencies --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
2b219c826c
commit
519ec9d294
91 files changed
+1113
-1044
No files matched your search
@@ -0,0 +1,126 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct LinkProps {
|
||||
pub rel: Option<String>,
|
||||
pub media: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub disabled: Option<bool>,
|
||||
pub r#as: Option<String>,
|
||||
pub sizes: Option<String>,
|
||||
/// Links are deduplicated by their href attribute
|
||||
pub href: Option<String>,
|
||||
pub crossorigin: Option<String>,
|
||||
pub referrerpolicy: Option<String>,
|
||||
pub fetchpriority: Option<String>,
|
||||
pub hreflang: Option<String>,
|
||||
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 {
|
||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||
let mut attributes = Vec::new();
|
||||
if let Some(rel) = &self.rel {
|
||||
attributes.push(("rel", rel.clone()));
|
||||
}
|
||||
if let Some(media) = &self.media {
|
||||
attributes.push(("media", media.clone()));
|
||||
}
|
||||
if let Some(title) = &self.title {
|
||||
attributes.push(("title", title.clone()));
|
||||
}
|
||||
if let Some(disabled) = &self.disabled {
|
||||
attributes.push(("disabled", disabled.to_string()));
|
||||
}
|
||||
if let Some(r#as) = &self.r#as {
|
||||
attributes.push(("as", r#as.clone()));
|
||||
}
|
||||
if let Some(sizes) = &self.sizes {
|
||||
attributes.push(("sizes", sizes.clone()));
|
||||
}
|
||||
if let Some(href) = &self.href {
|
||||
attributes.push(("href", href.clone()));
|
||||
}
|
||||
if let Some(crossorigin) = &self.crossorigin {
|
||||
attributes.push(("crossOrigin", crossorigin.clone()));
|
||||
}
|
||||
if let Some(referrerpolicy) = &self.referrerpolicy {
|
||||
attributes.push(("referrerPolicy", referrerpolicy.clone()));
|
||||
}
|
||||
if let Some(fetchpriority) = &self.fetchpriority {
|
||||
attributes.push(("fetchPriority", fetchpriority.clone()));
|
||||
}
|
||||
if let Some(hreflang) = &self.hreflang {
|
||||
attributes.push(("hrefLang", hreflang.clone()));
|
||||
}
|
||||
if let Some(integrity) = &self.integrity {
|
||||
attributes.push(("integrity", integrity.clone()));
|
||||
}
|
||||
if let Some(r#type) = &self.r#type {
|
||||
attributes.push(("type", r#type.clone()));
|
||||
}
|
||||
if let Some(blocking) = &self.blocking {
|
||||
attributes.push(("blocking", blocking.clone()));
|
||||
}
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a [`link`](crate::elements::link) tag into the head of the page.
|
||||
///
|
||||
/// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different.
|
||||
/// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn RedBackground() -> Element {
|
||||
/// rsx! {
|
||||
/// // You can use the meta component to render a meta tag into the head of the page
|
||||
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
||||
/// document::Link {
|
||||
/// href: asset!("./assets/style.css"),
|
||||
/// rel: "stylesheet",
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Any updates to the props after the first render will not be reflected in the head.
|
||||
///
|
||||
/// </div>
|
||||
#[doc(alias = "<link>")]
|
||||
#[component]
|
||||
pub fn Link(props: LinkProps) -> Element {
|
||||
use_update_warning(&props, "Link {}");
|
||||
|
||||
use_hook(|| {
|
||||
if let Some(href) = &props.href {
|
||||
if !should_insert_link(href) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let document = document();
|
||||
document.create_link(props);
|
||||
});
|
||||
|
||||
VNode::empty()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct LinkContext(DeduplicationContext);
|
||||
|
||||
fn should_insert_link(href: &str) -> bool {
|
||||
get_or_insert_root_context::<LinkContext>()
|
||||
.0
|
||||
.should_insert(href)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[non_exhaustive]
|
||||
/// Props for the [`Meta`] component
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct MetaProps {
|
||||
pub property: Option<String>,
|
||||
pub name: Option<String>,
|
||||
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 {
|
||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||
let mut attributes = Vec::new();
|
||||
if let Some(property) = &self.property {
|
||||
attributes.push(("property", property.clone()));
|
||||
}
|
||||
if let Some(name) = &self.name {
|
||||
attributes.push(("name", name.clone()));
|
||||
}
|
||||
if let Some(charset) = &self.charset {
|
||||
attributes.push(("charset", charset.clone()));
|
||||
}
|
||||
if let Some(http_equiv) = &self.http_equiv {
|
||||
attributes.push(("http-equiv", http_equiv.clone()));
|
||||
}
|
||||
if let Some(content) = &self.content {
|
||||
attributes.push(("content", content.clone()));
|
||||
}
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a [`meta`](crate::elements::meta) tag into the head of the page.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn RedirectToDioxusHomepageWithoutJS() -> Element {
|
||||
/// rsx! {
|
||||
/// // You can use the meta component to render a meta tag into the head of the page
|
||||
/// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
|
||||
/// document::Meta {
|
||||
/// http_equiv: "refresh",
|
||||
/// content: "10;url=https://dioxuslabs.com",
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Any updates to the props after the first render will not be reflected in the head.
|
||||
///
|
||||
/// </div>
|
||||
#[component]
|
||||
#[doc(alias = "<meta>")]
|
||||
pub fn Meta(props: MetaProps) -> Element {
|
||||
use_update_warning(&props, "Meta {}");
|
||||
|
||||
use_hook(|| {
|
||||
let document = document();
|
||||
document.create_meta(props);
|
||||
});
|
||||
|
||||
VNode::empty()
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
#![doc = include_str!("../../docs/head.md")]
|
||||
|
||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||
|
||||
use dioxus_core::{prelude::*, DynamicNode};
|
||||
use dioxus_core_macro::*;
|
||||
|
||||
mod link;
|
||||
pub use link::*;
|
||||
mod meta;
|
||||
pub use meta::*;
|
||||
mod script;
|
||||
pub use script::*;
|
||||
mod style;
|
||||
pub use style::*;
|
||||
mod title;
|
||||
pub use title::*;
|
||||
|
||||
/// Warn the user if they try to change props on a element that is injected into the head
|
||||
#[allow(unused)]
|
||||
fn use_update_warning<T: PartialEq + Clone + 'static>(value: &T, name: &'static str) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let cloned_value = value.clone();
|
||||
let initial = use_hook(move || value.clone());
|
||||
|
||||
if initial != cloned_value {
|
||||
tracing::warn!("Changing the props of `{name}` is not supported ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) => {
|
||||
return Err(ExtractSingleTextNodeError::RenderError(err));
|
||||
}
|
||||
};
|
||||
// The title's children must be in one of two forms:
|
||||
// 1. rsx! { "static text" }
|
||||
// 2. rsx! { "title: {dynamic_text}" }
|
||||
match vnode.template {
|
||||
// rsx! { "static text" }
|
||||
Template {
|
||||
roots: &[TemplateNode::Text { text }],
|
||||
node_paths: &[],
|
||||
attr_paths: &[],
|
||||
..
|
||||
} => Ok(text.to_string()),
|
||||
// rsx! { "title: {dynamic_text}" }
|
||||
Template {
|
||||
roots: &[TemplateNode::Dynamic { id }],
|
||||
node_paths: &[&[0]],
|
||||
attr_paths: &[],
|
||||
..
|
||||
} => {
|
||||
let node = &vnode.dynamic_nodes[id];
|
||||
match node {
|
||||
DynamicNode::Text(text) => Ok(text.value.clone()),
|
||||
_ => Err(ExtractSingleTextNodeError::NonTextNode),
|
||||
}
|
||||
}
|
||||
_ => Err(ExtractSingleTextNodeError::NonTemplate),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {
|
||||
match ScopeId::ROOT.has_context::<T>() {
|
||||
Some(context) => context,
|
||||
None => {
|
||||
let context = T::default();
|
||||
ScopeId::ROOT.provide_context(context.clone());
|
||||
context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct DeduplicationContext(Rc<RefCell<HashSet<String>>>);
|
||||
|
||||
impl DeduplicationContext {
|
||||
fn should_insert(&self, href: &str) -> bool {
|
||||
let mut set = self.0.borrow_mut();
|
||||
let present = set.contains(href);
|
||||
if !present {
|
||||
set.insert(href.to_string());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[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.
|
||||
pub children: Element,
|
||||
/// Scripts are deduplicated by their src attribute
|
||||
pub src: Option<String>,
|
||||
pub defer: Option<bool>,
|
||||
pub crossorigin: Option<String>,
|
||||
pub fetchpriority: Option<String>,
|
||||
pub integrity: Option<String>,
|
||||
pub nomodule: Option<bool>,
|
||||
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 {
|
||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||
let mut attributes = Vec::new();
|
||||
if let Some(defer) = &self.defer {
|
||||
attributes.push(("defer", defer.to_string()));
|
||||
}
|
||||
if let Some(crossorigin) = &self.crossorigin {
|
||||
attributes.push(("crossorigin", crossorigin.clone()));
|
||||
}
|
||||
if let Some(fetchpriority) = &self.fetchpriority {
|
||||
attributes.push(("fetchpriority", fetchpriority.clone()));
|
||||
}
|
||||
if let Some(integrity) = &self.integrity {
|
||||
attributes.push(("integrity", integrity.clone()));
|
||||
}
|
||||
if let Some(nomodule) = &self.nomodule {
|
||||
attributes.push(("nomodule", nomodule.to_string()));
|
||||
}
|
||||
if let Some(nonce) = &self.nonce {
|
||||
attributes.push(("nonce", nonce.clone()));
|
||||
}
|
||||
if let Some(referrerpolicy) = &self.referrerpolicy {
|
||||
attributes.push(("referrerpolicy", referrerpolicy.clone()));
|
||||
}
|
||||
if let Some(r#type) = &self.r#type {
|
||||
attributes.push(("type", r#type.clone()));
|
||||
}
|
||||
if let Some(src) = &self.src {
|
||||
attributes.push(("src", src.clone()));
|
||||
}
|
||||
attributes
|
||||
}
|
||||
|
||||
pub fn script_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||
extract_single_text_node(&self.children)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a [`script`](crate::elements::script) tag into the head of the page.
|
||||
///
|
||||
///
|
||||
/// If present, the children of the script component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the script will not be added.
|
||||
///
|
||||
///
|
||||
/// Any scripts you add will be deduplicated by their `src` attribute (if present).
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn LoadScript() -> Element {
|
||||
/// rsx! {
|
||||
/// // You can use the Script component to render a script tag into the head of the page
|
||||
/// document::Script {
|
||||
/// src: asset!("./assets/script.js"),
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Any updates to the props after the first render will not be reflected in the head.
|
||||
///
|
||||
/// </div>
|
||||
#[component]
|
||||
pub fn Script(props: ScriptProps) -> Element {
|
||||
use_update_warning(&props, "Script {}");
|
||||
|
||||
use_hook(|| {
|
||||
if let Some(src) = &props.src {
|
||||
if !should_insert_script(src) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let document = document();
|
||||
document.create_script(props);
|
||||
});
|
||||
|
||||
VNode::empty()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct ScriptContext(DeduplicationContext);
|
||||
|
||||
fn should_insert_script(src: &str) -> bool {
|
||||
get_or_insert_root_context::<ScriptContext>()
|
||||
.0
|
||||
.should_insert(src)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use super::*;
|
||||
use crate::document;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct StyleProps {
|
||||
/// Styles are deduplicated by their href attribute
|
||||
pub href: Option<String>,
|
||||
pub media: Option<String>,
|
||||
pub nonce: Option<String>,
|
||||
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 {
|
||||
pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
|
||||
let mut attributes = Vec::new();
|
||||
if let Some(href) = &self.href {
|
||||
attributes.push(("href", href.clone()));
|
||||
}
|
||||
if let Some(media) = &self.media {
|
||||
attributes.push(("media", media.clone()));
|
||||
}
|
||||
if let Some(nonce) = &self.nonce {
|
||||
attributes.push(("nonce", nonce.clone()));
|
||||
}
|
||||
if let Some(title) = &self.title {
|
||||
attributes.push(("title", title.clone()));
|
||||
}
|
||||
attributes
|
||||
}
|
||||
|
||||
pub fn style_contents(&self) -> Result<String, ExtractSingleTextNodeError<'_>> {
|
||||
extract_single_text_node(&self.children)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a [`style`](crate::elements::style) tag into the head of the page.
|
||||
///
|
||||
/// If present, the children of the style component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the style will not be added.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn RedBackground() -> Element {
|
||||
/// rsx! {
|
||||
/// // You can use the style component to render a style tag into the head of the page
|
||||
/// // This style tag will set the background color of the page to red
|
||||
/// document::Style {
|
||||
/// r#"
|
||||
/// body {{
|
||||
/// background-color: red;
|
||||
/// }}
|
||||
/// "#
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Any updates to the props after the first render will not be reflected in the head.
|
||||
///
|
||||
/// </div>
|
||||
#[component]
|
||||
pub fn Style(props: StyleProps) -> Element {
|
||||
use_update_warning(&props, "Style {}");
|
||||
|
||||
use_hook(|| {
|
||||
if let Some(href) = &props.href {
|
||||
if !should_insert_style(href) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let document = document();
|
||||
document.create_style(props);
|
||||
});
|
||||
|
||||
VNode::empty()
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct StyleContext(DeduplicationContext);
|
||||
|
||||
fn should_insert_style(href: &str) -> bool {
|
||||
get_or_insert_root_context::<StyleContext>()
|
||||
.0
|
||||
.should_insert(href)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::document;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Props, PartialEq)]
|
||||
pub struct TitleProps {
|
||||
/// The contents of the title tag. The children must be a single text node.
|
||||
children: Element,
|
||||
}
|
||||
|
||||
/// Render the title of the page. On web renderers, this will set the [title](crate::elements::title) in the head. On desktop, it will set the window title.
|
||||
///
|
||||
/// Unlike most head components, the Title can be modified after the first render. Only the latest update to the title will be reflected if multiple title components are rendered.
|
||||
///
|
||||
///
|
||||
/// The children of the title component must be a single static or formatted string. If there are more children or the children contain components, conditionals, loops, or fragments, the title will not be updated.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn App() -> Element {
|
||||
/// rsx! {
|
||||
/// // You can use the Title component to render a title tag into the head of the page or window
|
||||
/// document::Title { "My Page" }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component]
|
||||
#[doc(alias = "<title>")]
|
||||
pub fn Title(props: TitleProps) -> Element {
|
||||
let children = props.children;
|
||||
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
|
||||
let document = use_hook(document);
|
||||
let last_text = use_hook(|| {
|
||||
// Set the title initially
|
||||
document.set_title(text.clone());
|
||||
Rc::new(RefCell::new(text.clone()))
|
||||
});
|
||||
|
||||
// If the text changes, update the title
|
||||
let mut last_text = last_text.borrow_mut();
|
||||
if text != *last_text {
|
||||
document.set_title(text.clone());
|
||||
*last_text = text;
|
||||
}
|
||||
|
||||
VNode::empty()
|
||||
}
|
||||
Reference in new issue
Block a user