Skip to main content

dioxus_document/elements/
link.rs

1use super::*;
2use crate::document;
3use dioxus_core::{VNode, use_hook};
4
5#[non_exhaustive]
6#[derive(Clone, Props, PartialEq)]
7pub struct OtherLinkProps {
8    pub rel: String,
9    #[props(extends = link, extends = GlobalAttributes)]
10    pub additional_attributes: Vec<Attribute>,
11}
12
13#[non_exhaustive]
14#[derive(Clone, Props, PartialEq)]
15pub struct LinkProps {
16    pub rel: Option<String>,
17    pub media: Option<String>,
18    pub title: Option<String>,
19    pub disabled: Option<bool>,
20    pub r#as: Option<String>,
21    pub sizes: Option<String>,
22    /// Links are deduplicated by their href and rel attributes
23    pub href: Option<String>,
24    pub crossorigin: Option<String>,
25    pub referrerpolicy: Option<String>,
26    pub fetchpriority: Option<String>,
27    pub hreflang: Option<String>,
28    pub integrity: Option<String>,
29    pub r#type: Option<String>,
30    pub blocking: Option<String>,
31    #[props(extends = link, extends = GlobalAttributes)]
32    pub additional_attributes: Vec<Attribute>,
33    pub onload: Option<String>,
34}
35
36impl LinkProps {
37    /// Create link props for a stylesheet.
38    pub fn stylesheet(href: impl Into<String>) -> Self {
39        Self {
40            rel: Some("stylesheet".to_string()),
41            media: None,
42            title: None,
43            disabled: None,
44            r#as: None,
45            sizes: None,
46            href: Some(href.into()),
47            crossorigin: None,
48            referrerpolicy: None,
49            fetchpriority: None,
50            hreflang: None,
51            integrity: None,
52            r#type: None,
53            blocking: None,
54            additional_attributes: Vec::new(),
55            onload: None,
56        }
57    }
58
59    /// Get all the attributes for the link tag
60    pub fn attributes(&self) -> Vec<(&'static str, String)> {
61        let mut attributes = Vec::new();
62        extend_attributes(&mut attributes, &self.additional_attributes);
63        if let Some(rel) = &self.rel {
64            attributes.push(("rel", rel.clone()));
65        }
66        if let Some(media) = &self.media {
67            attributes.push(("media", media.clone()));
68        }
69        if let Some(title) = &self.title {
70            attributes.push(("title", title.clone()));
71        }
72        if let Some(disabled) = &self.disabled {
73            attributes.push(("disabled", disabled.to_string()));
74        }
75        if let Some(r#as) = &self.r#as {
76            attributes.push(("as", r#as.clone()));
77        }
78        if let Some(sizes) = &self.sizes {
79            attributes.push(("sizes", sizes.clone()));
80        }
81        if let Some(href) = &self.href {
82            attributes.push(("href", href.clone()));
83        }
84        if let Some(crossorigin) = &self.crossorigin {
85            attributes.push(("crossOrigin", crossorigin.clone()));
86        }
87        if let Some(referrerpolicy) = &self.referrerpolicy {
88            attributes.push(("referrerPolicy", referrerpolicy.clone()));
89        }
90        if let Some(fetchpriority) = &self.fetchpriority {
91            attributes.push(("fetchPriority", fetchpriority.clone()));
92        }
93        if let Some(hreflang) = &self.hreflang {
94            attributes.push(("hrefLang", hreflang.clone()));
95        }
96        if let Some(integrity) = &self.integrity {
97            attributes.push(("integrity", integrity.clone()));
98        }
99        if let Some(r#type) = &self.r#type {
100            attributes.push(("type", r#type.clone()));
101        }
102        if let Some(blocking) = &self.blocking {
103            attributes.push(("blocking", blocking.clone()));
104        }
105        if let Some(onload) = &self.onload {
106            attributes.push(("onload", onload.clone()));
107        }
108        attributes
109    }
110}
111
112/// Render a [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link) tag into the head of the page.
113///
114/// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different.
115/// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page.
116///
117/// # Example
118/// ```rust, no_run
119/// # use dioxus::prelude::*;
120/// fn RedBackground() -> Element {
121///     rsx! {
122///         // You can use the meta component to render a meta tag into the head of the page
123///         // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
124///         document::Link {
125///             href: asset!("/assets/style.css"),
126///             rel: "stylesheet",
127///         }
128///     }
129/// }
130/// ```
131///
132/// <div class="warning">
133///
134/// Any updates to the props after the first render will not be reflected in the head.
135///
136/// </div>
137#[doc(alias = "<link>")]
138#[component]
139pub fn Link(props: LinkProps) -> Element {
140    use_update_warning(&props, "Link {}");
141
142    use_hook(|| {
143        let document = document();
144        let mut insert_link = document.create_head_component();
145        if let Some(href) = &props.href
146            && !should_insert_link(href, props.rel.as_deref())
147        {
148            insert_link = false;
149        }
150
151        if !insert_link {
152            return;
153        }
154
155        document.create_link(props);
156    });
157
158    VNode::empty()
159}
160
161/// Insert a stylesheet `<link>` element into the head of the current document.
162///
163/// The stylesheet is deduplicated within each document: calling this multiple times with the
164/// same href (or rendering a [`Link`] component with the same href and `rel="stylesheet"`)
165/// only inserts the link once per document.
166pub fn insert_stylesheet(href: &str) {
167    // Deduplicate before creating the head component so that repeated calls with the same
168    // href (e.g. on every render) don't create or consume extra hydration entries.
169    if !should_insert_link(href, Some("stylesheet")) {
170        return;
171    }
172
173    let document = document();
174    if !document.create_head_component() {
175        return;
176    }
177
178    document.create_link(LinkProps::stylesheet(href.to_string()));
179}
180
181#[derive(Default, Clone)]
182struct LinkContext(DeduplicationContext);
183
184fn should_insert_link(href: &str, rel: Option<&str>) -> bool {
185    // Include rel in the deduplication key so that the same href can be used
186    // with different rel values (e.g., rel="preload" and rel="stylesheet")
187    let key = match rel {
188        Some(rel) => format!("{href}|{rel}"),
189        None => href.to_string(),
190    };
191    get_or_insert_root_context::<LinkContext>()
192        .0
193        .should_insert(&key)
194}