head.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. #![doc = include_str!("../../docs/head.md")]
  2. use std::{cell::RefCell, collections::HashSet, rc::Rc};
  3. use dioxus_core::{prelude::*, DynamicNode};
  4. use dioxus_core_macro::*;
  5. /// Warn the user if they try to change props on a element that is injected into the head
  6. #[allow(unused)]
  7. fn use_update_warning<T: PartialEq + Clone + 'static>(value: &T, name: &'static str) {
  8. #[cfg(debug_assertions)]
  9. {
  10. let cloned_value = value.clone();
  11. let initial = use_hook(move || value.clone());
  12. if initial != cloned_value {
  13. tracing::warn!("Changing the props of `{name}` is not supported ");
  14. }
  15. }
  16. }
  17. fn extract_single_text_node(children: &Element, component: &str) -> Option<String> {
  18. let vnode = match children {
  19. Element::Ok(vnode) => vnode,
  20. Element::Err(err) => {
  21. tracing::error!("Error while rendering {component}: {err}");
  22. return None;
  23. }
  24. };
  25. // The title's children must be in one of two forms:
  26. // 1. rsx! { "static text" }
  27. // 2. rsx! { "title: {dynamic_text}" }
  28. match vnode.template.get() {
  29. // rsx! { "static text" }
  30. Template {
  31. roots: &[TemplateNode::Text { text }],
  32. node_paths: &[],
  33. attr_paths: &[],
  34. ..
  35. } => Some(text.to_string()),
  36. // rsx! { "title: {dynamic_text}" }
  37. Template {
  38. roots: &[TemplateNode::Dynamic { id }],
  39. node_paths: &[&[0]],
  40. attr_paths: &[],
  41. ..
  42. } => {
  43. let node = &vnode.dynamic_nodes[id];
  44. match node {
  45. DynamicNode::Text(text) => Some(text.value.clone()),
  46. _ => {
  47. 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");
  48. None
  49. }
  50. }
  51. }
  52. _ => {
  53. tracing::error!(
  54. "Error while rendering title: The children of title must be a single text node"
  55. );
  56. None
  57. }
  58. }
  59. }
  60. #[derive(Clone, Props, PartialEq)]
  61. pub struct TitleProps {
  62. /// The contents of the title tag. The children must be a single text node.
  63. children: Element,
  64. }
  65. /// 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.
  66. ///
  67. /// 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.
  68. ///
  69. ///
  70. /// 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.
  71. ///
  72. /// # Example
  73. ///
  74. /// ```rust, no_run
  75. /// # use dioxus::prelude::*;
  76. /// fn App() -> Element {
  77. /// rsx! {
  78. /// // You can use the Title component to render a title tag into the head of the page or window
  79. /// Title { "My Page" }
  80. /// }
  81. /// }
  82. /// ```
  83. #[component]
  84. pub fn Title(props: TitleProps) -> Element {
  85. let children = props.children;
  86. let Some(text) = extract_single_text_node(&children, "Title") else {
  87. return rsx! {};
  88. };
  89. // Update the title as it changes. NOTE: We don't use use_effect here because we need this to run on the server
  90. let document = use_hook(document);
  91. let last_text = use_hook(|| {
  92. // Set the title initially
  93. document.set_title(text.clone());
  94. Rc::new(RefCell::new(text.clone()))
  95. });
  96. // If the text changes, update the title
  97. let mut last_text = last_text.borrow_mut();
  98. if text != *last_text {
  99. document.set_title(text.clone());
  100. *last_text = text;
  101. }
  102. rsx! {}
  103. }
  104. /// Props for the [`Meta`] component
  105. #[derive(Clone, Props, PartialEq)]
  106. pub struct MetaProps {
  107. pub property: Option<String>,
  108. pub name: Option<String>,
  109. pub charset: Option<String>,
  110. pub http_equiv: Option<String>,
  111. pub content: Option<String>,
  112. }
  113. impl MetaProps {
  114. pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
  115. let mut attributes = Vec::new();
  116. if let Some(property) = &self.property {
  117. attributes.push(("property", property.clone()));
  118. }
  119. if let Some(name) = &self.name {
  120. attributes.push(("name", name.clone()));
  121. }
  122. if let Some(charset) = &self.charset {
  123. attributes.push(("charset", charset.clone()));
  124. }
  125. if let Some(http_equiv) = &self.http_equiv {
  126. attributes.push(("http-equiv", http_equiv.clone()));
  127. }
  128. if let Some(content) = &self.content {
  129. attributes.push(("content", content.clone()));
  130. }
  131. attributes
  132. }
  133. }
  134. /// Render a [`meta`](crate::elements::meta) tag into the head of the page.
  135. ///
  136. /// # Example
  137. ///
  138. /// ```rust, no_run
  139. /// # use dioxus::prelude::*;
  140. /// fn RedirectToDioxusHomepageWithoutJS() -> Element {
  141. /// rsx! {
  142. /// // You can use the meta component to render a meta tag into the head of the page
  143. /// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
  144. /// Meta {
  145. /// http_equiv: "refresh",
  146. /// content: "10;url=https://dioxuslabs.com",
  147. /// }
  148. /// }
  149. /// }
  150. /// ```
  151. ///
  152. /// <div class="warning">
  153. ///
  154. /// Any updates to the props after the first render will not be reflected in the head.
  155. ///
  156. /// </div>
  157. #[component]
  158. pub fn Meta(props: MetaProps) -> Element {
  159. use_update_warning(&props, "Meta {}");
  160. use_hook(|| {
  161. let document = document();
  162. document.create_meta(props);
  163. });
  164. rsx! {}
  165. }
  166. #[derive(Clone, Props, PartialEq)]
  167. pub struct ScriptProps {
  168. /// The contents of the script tag. If present, the children must be a single text node.
  169. pub children: Element,
  170. /// Scripts are deduplicated by their src attribute
  171. pub src: Option<String>,
  172. pub defer: Option<bool>,
  173. pub crossorigin: Option<String>,
  174. pub fetchpriority: Option<String>,
  175. pub integrity: Option<String>,
  176. pub nomodule: Option<bool>,
  177. pub nonce: Option<String>,
  178. pub referrerpolicy: Option<String>,
  179. pub r#type: Option<String>,
  180. }
  181. impl ScriptProps {
  182. pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
  183. let mut attributes = Vec::new();
  184. if let Some(defer) = &self.defer {
  185. attributes.push(("defer", defer.to_string()));
  186. }
  187. if let Some(crossorigin) = &self.crossorigin {
  188. attributes.push(("crossorigin", crossorigin.clone()));
  189. }
  190. if let Some(fetchpriority) = &self.fetchpriority {
  191. attributes.push(("fetchpriority", fetchpriority.clone()));
  192. }
  193. if let Some(integrity) = &self.integrity {
  194. attributes.push(("integrity", integrity.clone()));
  195. }
  196. if let Some(nomodule) = &self.nomodule {
  197. attributes.push(("nomodule", nomodule.to_string()));
  198. }
  199. if let Some(nonce) = &self.nonce {
  200. attributes.push(("nonce", nonce.clone()));
  201. }
  202. if let Some(referrerpolicy) = &self.referrerpolicy {
  203. attributes.push(("referrerpolicy", referrerpolicy.clone()));
  204. }
  205. if let Some(r#type) = &self.r#type {
  206. attributes.push(("type", r#type.clone()));
  207. }
  208. attributes
  209. }
  210. pub fn script_contents(&self) -> Option<String> {
  211. extract_single_text_node(&self.children, "Script")
  212. }
  213. }
  214. /// Render a [`script`](crate::elements::script) tag into the head of the page.
  215. ///
  216. ///
  217. /// 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.
  218. ///
  219. ///
  220. /// Any scripts you add will be deduplicated by their `src` attribute (if present).
  221. ///
  222. /// # Example
  223. /// ```rust, no_run
  224. /// # use dioxus::prelude::*;
  225. /// fn LoadScript() -> Element {
  226. /// rsx! {
  227. /// // You can use the Script component to render a script tag into the head of the page
  228. /// Script {
  229. /// src: asset!("./assets/script.js"),
  230. /// }
  231. /// }
  232. /// }
  233. /// ```
  234. ///
  235. /// <div class="warning">
  236. ///
  237. /// Any updates to the props after the first render will not be reflected in the head.
  238. ///
  239. /// </div>
  240. #[component]
  241. pub fn Script(props: ScriptProps) -> Element {
  242. use_update_warning(&props, "Script {}");
  243. use_hook(|| {
  244. if let Some(src) = &props.src {
  245. if !should_insert_script(src) {
  246. return;
  247. }
  248. }
  249. let document = document();
  250. document.create_script(props);
  251. });
  252. rsx! {}
  253. }
  254. #[derive(Clone, Props, PartialEq)]
  255. pub struct StyleProps {
  256. /// Styles are deduplicated by their href attribute
  257. pub href: Option<String>,
  258. pub media: Option<String>,
  259. pub nonce: Option<String>,
  260. pub title: Option<String>,
  261. /// The contents of the style tag. If present, the children must be a single text node.
  262. pub children: Element,
  263. }
  264. impl StyleProps {
  265. pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
  266. let mut attributes = Vec::new();
  267. if let Some(href) = &self.href {
  268. attributes.push(("href", href.clone()));
  269. }
  270. if let Some(media) = &self.media {
  271. attributes.push(("media", media.clone()));
  272. }
  273. if let Some(nonce) = &self.nonce {
  274. attributes.push(("nonce", nonce.clone()));
  275. }
  276. if let Some(title) = &self.title {
  277. attributes.push(("title", title.clone()));
  278. }
  279. attributes
  280. }
  281. pub fn style_contents(&self) -> Option<String> {
  282. extract_single_text_node(&self.children, "Title")
  283. }
  284. }
  285. /// Render a [`style`](crate::elements::style) tag into the head of the page.
  286. ///
  287. /// 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.
  288. ///
  289. /// # Example
  290. /// ```rust, no_run
  291. /// # use dioxus::prelude::*;
  292. /// fn RedBackground() -> Element {
  293. /// rsx! {
  294. /// // You can use the style component to render a style tag into the head of the page
  295. /// // This style tag will set the background color of the page to red
  296. /// Style {
  297. /// r#"
  298. /// body {{
  299. /// background-color: red;
  300. /// }}
  301. /// "#
  302. /// }
  303. /// }
  304. /// }
  305. /// ```
  306. ///
  307. /// <div class="warning">
  308. ///
  309. /// Any updates to the props after the first render will not be reflected in the head.
  310. ///
  311. /// </div>
  312. #[component]
  313. pub fn Style(props: StyleProps) -> Element {
  314. use_update_warning(&props, "Style {}");
  315. use_hook(|| {
  316. if let Some(href) = &props.href {
  317. if !should_insert_style(href) {
  318. return;
  319. }
  320. }
  321. let document = document();
  322. document.create_style(props);
  323. });
  324. rsx! {}
  325. }
  326. use super::*;
  327. #[derive(Clone, Props, PartialEq)]
  328. pub struct LinkProps {
  329. pub rel: Option<String>,
  330. pub media: Option<String>,
  331. pub title: Option<String>,
  332. pub disabled: Option<bool>,
  333. pub r#as: Option<String>,
  334. pub sizes: Option<String>,
  335. /// Links are deduplicated by their href attribute
  336. pub href: Option<String>,
  337. pub crossorigin: Option<String>,
  338. pub referrerpolicy: Option<String>,
  339. pub fetchpriority: Option<String>,
  340. pub hreflang: Option<String>,
  341. pub integrity: Option<String>,
  342. pub r#type: Option<String>,
  343. pub blocking: Option<String>,
  344. }
  345. impl LinkProps {
  346. pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
  347. let mut attributes = Vec::new();
  348. if let Some(rel) = &self.rel {
  349. attributes.push(("rel", rel.clone()));
  350. }
  351. if let Some(media) = &self.media {
  352. attributes.push(("media", media.clone()));
  353. }
  354. if let Some(title) = &self.title {
  355. attributes.push(("title", title.clone()));
  356. }
  357. if let Some(disabled) = &self.disabled {
  358. attributes.push(("disabled", disabled.to_string()));
  359. }
  360. if let Some(r#as) = &self.r#as {
  361. attributes.push(("as", r#as.clone()));
  362. }
  363. if let Some(sizes) = &self.sizes {
  364. attributes.push(("sizes", sizes.clone()));
  365. }
  366. if let Some(href) = &self.href {
  367. attributes.push(("href", href.clone()));
  368. }
  369. if let Some(crossorigin) = &self.crossorigin {
  370. attributes.push(("crossOrigin", crossorigin.clone()));
  371. }
  372. if let Some(referrerpolicy) = &self.referrerpolicy {
  373. attributes.push(("referrerPolicy", referrerpolicy.clone()));
  374. }
  375. if let Some(fetchpriority) = &self.fetchpriority {
  376. attributes.push(("fetchPriority", fetchpriority.clone()));
  377. }
  378. if let Some(hreflang) = &self.hreflang {
  379. attributes.push(("hrefLang", hreflang.clone()));
  380. }
  381. if let Some(integrity) = &self.integrity {
  382. attributes.push(("integrity", integrity.clone()));
  383. }
  384. if let Some(r#type) = &self.r#type {
  385. attributes.push(("type", r#type.clone()));
  386. }
  387. if let Some(blocking) = &self.blocking {
  388. attributes.push(("blocking", blocking.clone()));
  389. }
  390. attributes
  391. }
  392. }
  393. /// Render a [`link`](crate::elements::link) tag into the head of the page.
  394. ///
  395. /// > The [Link](https://docs.rs/dioxus-router/latest/dioxus_router/components/fn.Link.html) component in dioxus router and this component are completely different.
  396. /// > This component links resources in the head of the page, while the router component creates clickable links in the body of the page.
  397. ///
  398. /// # Example
  399. /// ```rust, no_run
  400. /// # use dioxus::prelude::*;
  401. /// fn RedBackground() -> Element {
  402. /// rsx! {
  403. /// // You can use the meta component to render a meta tag into the head of the page
  404. /// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds
  405. /// head::Link {
  406. /// href: asset!("./assets/style.css"),
  407. /// rel: "stylesheet",
  408. /// }
  409. /// }
  410. /// }
  411. /// ```
  412. ///
  413. /// <div class="warning">
  414. ///
  415. /// Any updates to the props after the first render will not be reflected in the head.
  416. ///
  417. /// </div>
  418. #[doc(alias = "<link>")]
  419. #[component]
  420. pub fn Link(props: LinkProps) -> Element {
  421. use_update_warning(&props, "Link {}");
  422. use_hook(|| {
  423. if let Some(href) = &props.href {
  424. if !should_insert_link(href) {
  425. return;
  426. }
  427. }
  428. let document = document();
  429. document.create_link(props);
  430. });
  431. rsx! {}
  432. }
  433. fn get_or_insert_root_context<T: Default + Clone + 'static>() -> T {
  434. match ScopeId::ROOT.has_context::<T>() {
  435. Some(context) => context,
  436. None => {
  437. let context = T::default();
  438. ScopeId::ROOT.provide_context(context.clone());
  439. context
  440. }
  441. }
  442. }
  443. #[derive(Default, Clone)]
  444. struct LinkContext(DeduplicationContext);
  445. fn should_insert_link(href: &str) -> bool {
  446. get_or_insert_root_context::<LinkContext>()
  447. .0
  448. .should_insert(href)
  449. }
  450. #[derive(Default, Clone)]
  451. struct ScriptContext(DeduplicationContext);
  452. fn should_insert_script(src: &str) -> bool {
  453. get_or_insert_root_context::<ScriptContext>()
  454. .0
  455. .should_insert(src)
  456. }
  457. #[derive(Default, Clone)]
  458. struct StyleContext(DeduplicationContext);
  459. fn should_insert_style(href: &str) -> bool {
  460. get_or_insert_root_context::<StyleContext>()
  461. .0
  462. .should_insert(href)
  463. }
  464. #[derive(Default, Clone)]
  465. struct DeduplicationContext(Rc<RefCell<HashSet<String>>>);
  466. impl DeduplicationContext {
  467. fn should_insert(&self, href: &str) -> bool {
  468. let mut set = self.0.borrow_mut();
  469. let present = set.contains(href);
  470. if !present {
  471. set.insert(href.to_string());
  472. true
  473. } else {
  474. false
  475. }
  476. }
  477. }