dioxus_helmet/
lib.rs

1//! ## General
2//! Inspired by react-helmet, this small [Dioxus](https://crates.io/crates/dioxus) component allows you to place elements in the **head** of your code.
3//!
4//! ## Configuration
5//! Add the package as a dependency to your `Cargo.toml`.
6//! ```no_run
7//! cargo add dioxus-helmet
8//! ```
9//!
10//! ## Usage
11//! Import it in your code:
12//! ```
13//! use dioxus_helmet::Helmet;
14//! ```
15//! 
16//! Then use it as a component like this:
17//! 
18//! ```rust
19//! #[inline_props]
20//! fn HeadElements(cx: Scope, path: String) -> Element {
21//!     cx.render(rsx! {
22//!         Helmet {
23//!             link { rel: "icon", href: "{path}"}
24//!             title { "Helmet" }
25//!             style {
26//!                 [r#"
27//!                     body {
28//!                         color: blue;
29//!                     }
30//!                     a {
31//!                         color: red;
32//!                     }
33//!                 "#]
34//!             }
35//!         }
36//!     })
37//! }
38//! ```
39//! 
40//! Reach your dynamic values down as owned properties (eg `String` and **not** `&'a str`).
41//! 
42//! Also make sure that there are **no states** in your component where you use Helmet.
43//! 
44//! Any children passed to the helmet component will then be placed in the `<head></head>` of your document.
45//! 
46//! They will be visible while the component is rendered. Duplicates **won't** get appended multiple times.
47
48use dioxus::prelude::*;
49use fxhash::FxHasher;
50use lazy_static::lazy_static;
51use std::{
52    hash::{Hash, Hasher},
53    sync::Mutex,
54};
55
56lazy_static! {
57    static ref INIT_CACHE: Mutex<Vec<u64>> = Mutex::new(Vec::new());
58}
59
60#[derive(Props)]
61pub struct HelmetProps<'a> {
62    children: Element<'a>,
63}
64
65#[allow(non_snake_case)]
66pub fn Helmet<'a>(cx: Scope<'a, HelmetProps<'a>>) -> Element {
67    if let Some(window) = web_sys::window() {
68        if let Some(document) = window.document() {
69            if let Some(head) = document.head() {
70                if let Some(element_maps) = extract_element_maps(&cx.props.children) {
71                    if let Ok(mut init_cache) = INIT_CACHE.try_lock() {
72                        element_maps.iter().for_each(|element_map| {
73                            let mut hasher = FxHasher::default();
74                            element_map.hash(&mut hasher);
75                            let hash = hasher.finish();
76
77                            if !init_cache.contains(&hash) {
78                                init_cache.push(hash);
79
80                                if let Some(new_element) =
81                                    element_map.try_into_element(&document, &hash)
82                                {
83                                    let _ = head.append_child(&new_element);
84                                }
85                            }
86                        });
87                    }
88                }
89            }
90        }
91    }
92
93    None
94}
95
96impl Drop for HelmetProps<'_> {
97    fn drop(&mut self) {
98        if let Some(window) = web_sys::window() {
99            if let Some(document) = window.document() {
100                if let Some(element_maps) = extract_element_maps(&self.children) {
101                    if let Ok(mut init_cache) = INIT_CACHE.try_lock() {
102                        element_maps.iter().for_each(|element_map| {
103                            let mut hasher = FxHasher::default();
104                            element_map.hash(&mut hasher);
105                            let hash = hasher.finish();
106
107                            if let Some(index) = init_cache.iter().position(|&c| c == hash) {
108                                init_cache.remove(index);
109                            }
110
111                            if let Ok(children) =
112                                document.query_selector_all(&format!("[data-helmet-id='{hash}']"))
113                            {
114                                if let Ok(Some(children_iter)) = js_sys::try_iter(&children) {
115                                    children_iter.for_each(|child| {
116                                        if let Ok(child) = child {
117                                            let el = web_sys::Element::from(child);
118                                            el.remove();
119                                        };
120                                    });
121                                }
122                            }
123                        });
124                    }
125                }
126            }
127        }
128    }
129}
130
131#[derive(Debug, Hash)]
132struct ElementMap<'a> {
133    tag: &'a str,
134    attributes: Vec<(&'a str, &'a str)>,
135    inner_html: Option<&'a str>,
136}
137
138impl<'a> ElementMap<'a> {
139    fn try_into_element(
140        &self,
141        document: &web_sys::Document,
142        hash: &u64,
143    ) -> Option<web_sys::Element> {
144        if let Ok(new_element) = document.create_element(self.tag) {
145            self.attributes.iter().for_each(|(name, value)| {
146                let _ = new_element.set_attribute(name, value);
147            });
148            let _ = new_element.set_attribute("data-helmet-id", &hash.to_string());
149
150            if let Some(inner_html) = self.inner_html {
151                new_element.set_inner_html(inner_html);
152            }
153
154            Some(new_element)
155        } else {
156            None
157        }
158    }
159}
160
161fn extract_element_maps<'a>(children: &'a Element) -> Option<Vec<ElementMap<'a>>> {
162    if let Some(VNode::Fragment(fragment)) = &children {
163        let elements = fragment
164            .children
165            .iter()
166            .flat_map(|child| {
167                if let VNode::Element(element) = child {
168                    let attributes = element
169                        .attributes
170                        .iter()
171                        .map(|attribute| (attribute.name, attribute.value))
172                        .collect();
173
174                    let inner_html = match element.children.first() {
175                        Some(VNode::Text(vtext)) => Some(vtext.text),
176                        Some(VNode::Fragment(fragment)) if fragment.children.len() == 1 => {
177                            if let Some(VNode::Text(vtext)) = fragment.children.first() {
178                                Some(vtext.text)
179                            } else {
180                                None
181                            }
182                        }
183                        _ => None,
184                    };
185
186                    Some(ElementMap {
187                        tag: element.tag,
188                        attributes,
189                        inner_html,
190                    })
191                } else {
192                    None
193                }
194            })
195            .collect();
196
197        Some(elements)
198    } else {
199        None
200    }
201}