dioxus_web/
document.rs

1use dioxus_core::queue_effect;
2use dioxus_core::ScopeId;
3use dioxus_core::{provide_context, Runtime};
4use dioxus_document::{
5    Document, Eval, EvalError, Evaluator, LinkProps, MetaProps, ScriptProps, StyleProps,
6};
7use dioxus_history::History;
8use futures_util::FutureExt;
9use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
10use js_sys::Function;
11use serde::Serialize;
12use serde_json::Value;
13use std::future::Future;
14use std::pin::Pin;
15use std::result;
16use std::{rc::Rc, str::FromStr};
17use wasm_bindgen::prelude::*;
18use wasm_bindgen_futures::JsFuture;
19
20use crate::history::WebHistory;
21
22#[wasm_bindgen::prelude::wasm_bindgen]
23pub struct JSOwner {
24    _owner: Box<dyn std::any::Any>,
25}
26
27impl JSOwner {
28    pub fn new(owner: impl std::any::Any) -> Self {
29        Self {
30            _owner: Box::new(owner),
31        }
32    }
33}
34
35#[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")]
36extern "C" {
37    pub type WeakDioxusChannel;
38
39    #[wasm_bindgen(method, js_name = "rustSend")]
40    pub fn rust_send(this: &WeakDioxusChannel, value: wasm_bindgen::JsValue);
41
42    #[wasm_bindgen(method, js_name = "rustRecv")]
43    pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue;
44}
45
46#[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")]
47extern "C" {
48    pub type WebDioxusChannel;
49
50    #[wasm_bindgen(constructor)]
51    pub fn new(owner: JSOwner) -> WebDioxusChannel;
52
53    #[wasm_bindgen(method, js_name = "rustSend")]
54    pub fn rust_send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
55
56    #[wasm_bindgen(method, js_name = "rustRecv")]
57    pub async fn rust_recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
58
59    #[wasm_bindgen(method)]
60    pub fn send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
61
62    #[wasm_bindgen(method)]
63    pub async fn recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
64
65    #[wasm_bindgen(method)]
66    pub fn weak(this: &WebDioxusChannel) -> WeakDioxusChannel;
67
68}
69
70fn init_document_with(document: impl FnOnce(), history: impl FnOnce()) {
71    use dioxus_core::has_context;
72    Runtime::current().in_scope(ScopeId::ROOT, || {
73        if has_context::<Rc<dyn Document>>().is_none() {
74            document();
75        }
76        if has_context::<Rc<dyn History>>().is_none() {
77            history();
78        }
79    })
80}
81
82/// Provides the Document through [`dioxus_core::provide_context`].
83pub fn init_document() {
84    // If hydrate is enabled, we add the FullstackWebDocument with the initial hydration data
85    #[cfg(not(feature = "hydrate"))]
86    {
87        use dioxus_history::provide_history_context;
88
89        init_document_with(
90            || {
91                provide_context(Rc::new(WebDocument) as Rc<dyn Document>);
92            },
93            || {
94                provide_history_context(Rc::new(WebHistory::default()));
95            },
96        );
97    }
98}
99
100#[cfg(feature = "hydrate")]
101pub fn init_fullstack_document() {
102    use dioxus_fullstack_core::{
103        document::FullstackWebDocument, history::provide_fullstack_history_context,
104    };
105
106    init_document_with(
107        || {
108            provide_context(Rc::new(FullstackWebDocument::from(WebDocument)) as Rc<dyn Document>);
109        },
110        || provide_fullstack_history_context(WebHistory::default()),
111    );
112}
113
114/// The web-target's document provider.
115#[derive(Clone)]
116pub struct WebDocument;
117impl Document for WebDocument {
118    fn eval(&self, js: String) -> Eval {
119        Eval::new(WebEvaluator::create(js))
120    }
121
122    /// Set the title of the document
123    fn set_title(&self, title: String) {
124        let myself = self.clone();
125        queue_effect(move || {
126            myself.eval(format!("document.title = {title:?};"));
127        });
128    }
129
130    /// Create a new meta tag in the head
131    fn create_meta(&self, props: MetaProps) {
132        queue_effect(move || {
133            _ = append_element_to_head("meta", &props.attributes(), None);
134        });
135    }
136
137    /// Create a new script tag in the head
138    fn create_script(&self, props: ScriptProps) {
139        queue_effect(move || {
140            _ = append_element_to_head(
141                "script",
142                &props.attributes(),
143                props.script_contents().ok().as_deref(),
144            );
145        });
146    }
147
148    /// Create a new style tag in the head
149    fn create_style(&self, props: StyleProps) {
150        queue_effect(move || {
151            _ = append_element_to_head(
152                "style",
153                &props.attributes(),
154                props.style_contents().ok().as_deref(),
155            );
156        });
157    }
158
159    /// Create a new link tag in the head
160    fn create_link(&self, props: LinkProps) {
161        queue_effect(move || {
162            _ = append_element_to_head("link", &props.attributes(), None);
163        });
164    }
165}
166
167fn append_element_to_head(
168    local_name: &str,
169    attributes: &Vec<(&'static str, String)>,
170    text_content: Option<&str>,
171) -> Result<(), JsValue> {
172    let window = web_sys::window().expect("no global `window` exists");
173    let document = window.document().expect("should have a document on window");
174    let head = document.head().expect("document should have a head");
175
176    let element = document.create_element(local_name)?;
177    for (name, value) in attributes {
178        element.set_attribute(name, value)?;
179    }
180    if text_content.is_some() {
181        element.set_text_content(text_content);
182    }
183    head.append_child(&element)?;
184
185    Ok(())
186}
187
188/// Required to avoid blocking the Rust WASM thread.
189const PROMISE_WRAPPER: &str = r#"
190    return (async function(){
191        {JS_CODE}
192
193        dioxus.close();
194    })();
195"#;
196
197type NextPoll = Pin<Box<dyn Future<Output = Result<serde_json::Value, EvalError>>>>;
198
199/// Represents a web-target's JavaScript evaluator.
200struct WebEvaluator {
201    channels: WeakDioxusChannel,
202    next_future: Option<NextPoll>,
203    result: Pin<Box<dyn Future<Output = result::Result<Value, EvalError>>>>,
204}
205
206impl WebEvaluator {
207    /// Creates a new evaluator for web-based targets.
208    fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
209        let owner = UnsyncStorage::owner();
210
211        // add the drop handler to DioxusChannel so that it gets dropped when the channel is dropped in js
212        let channels = WebDioxusChannel::new(JSOwner::new(owner.clone()));
213
214        // The Rust side of the channel is a weak reference to the DioxusChannel
215        let weak_channels = channels.weak();
216
217        // Wrap the evaluated JS in a promise so that wasm can continue running (send/receive data from js)
218        let code = PROMISE_WRAPPER.replace("{JS_CODE}", &js);
219
220        let result = match Function::new_with_args("dioxus", &code).call1(&JsValue::NULL, &channels)
221        {
222            Ok(result) => {
223                let future = js_sys::Promise::resolve(&result);
224                let js_future = JsFuture::from(future);
225                Box::pin(async move {
226                    let result = js_future.await.map_err(|e| {
227                        EvalError::Communication(format!("Failed to await result - {:?}", e))
228                    })?;
229                    let stringified = js_sys::JSON::stringify(&result).map_err(|e| {
230                        EvalError::Communication(format!("Failed to stringify result - {:?}", e))
231                    })?;
232                    if !stringified.is_undefined() && stringified.is_valid_utf16() {
233                        let string: String = stringified.into();
234                        Value::from_str(&string).map_err(|e| {
235                            EvalError::Communication(format!("Failed to parse result - {}", e))
236                        })
237                    } else {
238                        Err(EvalError::Communication(
239                            "Failed to stringify result - undefined or not valid utf16".to_string(),
240                        ))
241                    }
242                })
243                    as Pin<Box<dyn Future<Output = result::Result<Value, EvalError>>>>
244            }
245            Err(err) => Box::pin(futures_util::future::ready(Err(EvalError::InvalidJs(
246                err.as_string().unwrap_or("unknown".to_string()),
247            )))),
248        };
249
250        owner.insert(Box::new(Self {
251            channels: weak_channels,
252            result,
253            next_future: None,
254        }) as Box<dyn Evaluator>)
255    }
256}
257
258impl Evaluator for WebEvaluator {
259    /// Runs the evaluated JavaScript.
260    fn poll_join(
261        &mut self,
262        cx: &mut std::task::Context<'_>,
263    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
264        self.result.poll_unpin(cx)
265    }
266
267    /// Sends a message to the evaluated JavaScript.
268    fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
269        let serializer = serde_wasm_bindgen::Serializer::json_compatible();
270
271        let data = match data.serialize(&serializer) {
272            Ok(d) => d,
273            Err(e) => return Err(EvalError::Communication(e.to_string())),
274        };
275
276        self.channels.rust_send(data);
277        Ok(())
278    }
279
280    /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
281    fn poll_recv(
282        &mut self,
283        context: &mut std::task::Context<'_>,
284    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
285        if self.next_future.is_none() {
286            let channels: WebDioxusChannel = self.channels.clone().into();
287            let pinned = Box::pin(async move {
288                let fut = channels.rust_recv();
289                let data = fut.await;
290                serde_wasm_bindgen::from_value::<serde_json::Value>(data)
291                    .map_err(|err| EvalError::Communication(err.to_string()))
292            });
293            self.next_future = Some(pinned);
294        }
295        let fut = self.next_future.as_mut().unwrap();
296        let mut pinned = std::pin::pin!(fut);
297        let result = pinned.as_mut().poll(context);
298        if result.is_ready() {
299            self.next_future = None;
300        }
301        result
302    }
303}