dioxus_document/
document.rs

1use std::sync::Arc;
2
3use super::*;
4
5/// A context for the document
6pub type DocumentContext = Arc<dyn Document>;
7
8fn format_string_for_js(s: &str) -> String {
9    let escaped = s
10        .replace('\\', "\\\\")
11        .replace('\n', "\\n")
12        .replace('\r', "\\r")
13        .replace('"', "\\\"");
14    format!("\"{escaped}\"")
15}
16
17fn format_attributes(attributes: &[(&str, String)]) -> String {
18    let mut formatted = String::from("[");
19    for (key, value) in attributes {
20        formatted.push_str(&format!(
21            "[{}, {}],",
22            format_string_for_js(key),
23            format_string_for_js(value)
24        ));
25    }
26    if formatted.ends_with(',') {
27        formatted.pop();
28    }
29    formatted.push(']');
30    formatted
31}
32
33/// Create a new element in the head with javascript through the [`Document::eval`] method
34///
35/// This can be used to implement the head element creation logic for most [`Document`] implementations.
36pub fn create_element_in_head(
37    tag: &str,
38    attributes: &[(&str, String)],
39    children: Option<String>,
40) -> String {
41    let helpers = include_str!("./js/head.js");
42    let attributes = format_attributes(attributes);
43    let children = children
44        .as_deref()
45        .map(format_string_for_js)
46        .unwrap_or("null".to_string());
47    let tag = format_string_for_js(tag);
48    format!(r#"{helpers};window.createElementInHead({tag}, {attributes}, {children});"#)
49}
50
51/// A provider for document-related functionality.
52///
53/// Provides things like a history API, a title, a way to run JS, and some other basics/essentials used
54/// by nearly every platform.
55///
56/// An integration with some kind of navigation history.
57///
58/// Depending on your use case, your implementation may deviate from the described procedure. This
59/// is fine, as long as both `current_route` and `current_query` match the described format.
60///
61/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
62/// The described behaviors are designed to mimic a web browser, which most users should already
63/// know. Deviations might confuse them.
64pub trait Document: 'static {
65    /// Run `eval` against this document, returning an [`Eval`] that can be used to await the result.
66    fn eval(&self, js: String) -> Eval;
67
68    /// Set the title of the document
69    fn set_title(&self, title: String) {
70        self.eval(format!("document.title = {title:?};"));
71    }
72
73    /// Create a new element in the head
74    fn create_head_element(
75        &self,
76        name: &str,
77        attributes: &[(&str, String)],
78        contents: Option<String>,
79    ) {
80        // This default implementation remains to make the trait compatible with the 0.6 version, but it should not be used
81        // The element should only be created inside an effect so it is not called while the component is suspended
82        self.eval(create_element_in_head(name, attributes, contents));
83    }
84
85    /// Create a new meta tag in the head
86    fn create_meta(&self, props: MetaProps) {
87        let attributes = props.attributes();
88        self.create_head_element("meta", &attributes, None);
89    }
90
91    /// Create a new script tag in the head
92    fn create_script(&self, props: ScriptProps) {
93        let attributes = props.attributes();
94        self.create_head_element("script", &attributes, props.script_contents().ok());
95    }
96
97    /// Create a new style tag in the head
98    fn create_style(&self, props: StyleProps) {
99        let attributes = props.attributes();
100        self.create_head_element("style", &attributes, props.style_contents().ok());
101    }
102
103    /// Create a new link tag in the head
104    fn create_link(&self, props: LinkProps) {
105        let attributes = props.attributes();
106        self.create_head_element("link", &attributes, None);
107    }
108
109    /// Check if we should create a new head component at all. If it returns false, the head component will be skipped.
110    ///
111    /// This runs once per head component and is used to hydrate head components in fullstack.
112    fn create_head_component(&self) -> bool {
113        true
114    }
115}
116
117/// A document that does nothing
118#[derive(Default)]
119pub struct NoOpDocument;
120
121impl Document for NoOpDocument {
122    fn eval(&self, _: String) -> Eval {
123        let owner = generational_box::Owner::default();
124        struct NoOpEvaluator;
125        impl Evaluator for NoOpEvaluator {
126            fn poll_join(
127                &mut self,
128                _: &mut std::task::Context<'_>,
129            ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
130                std::task::Poll::Ready(Err(EvalError::Unsupported))
131            }
132
133            fn poll_recv(
134                &mut self,
135                _: &mut std::task::Context<'_>,
136            ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
137                std::task::Poll::Ready(Err(EvalError::Unsupported))
138            }
139
140            fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
141                Err(EvalError::Unsupported)
142            }
143        }
144        Eval::new(owner.insert(Box::new(NoOpEvaluator)))
145    }
146
147    fn set_title(&self, _: String) {}
148    fn create_meta(&self, _: MetaProps) {}
149    fn create_script(&self, _: ScriptProps) {}
150    fn create_style(&self, _: StyleProps) {}
151    fn create_link(&self, _: LinkProps) {}
152}