Skip to main content

rustolio_web/renderer/
browser.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11use std::rc::Rc;
12
13use wasm_bindgen::prelude::*;
14
15use crate::elements::{InnerElement, InnerElements};
16use crate::hooks::{signal_updater_callback, signal_updater_callback_full};
17use crate::prelude::*;
18
19const ERROR_MSG: &str = r#"
20The `BrowserRenderer` failed to render an element properly.
21
22This is an internal error and should be fixed.
23Please open an issue at `https://gitlab.com/TobiasBinnewies/rustolio/-/issues`. 
24"#;
25
26#[wasm_bindgen(raw_module = "./rustolio-web/utils/main.js")]
27extern "C" {
28    #[wasm_bindgen(catch)]
29    fn apply_tw_classes(
30        elem: &web_sys::Element,
31        classes: &str,
32    ) -> Result<(), wasm_bindgen::JsValue>;
33}
34
35#[wasm_bindgen]
36impl Elements {
37    #[wasm_bindgen]
38    pub fn render(&self, parent: &Node) {
39        BrowserRenderer::render_elements_rec(self, parent);
40    }
41}
42
43#[derive(Clone, Default)]
44pub struct BrowserRenderer {}
45
46impl BrowserRenderer {
47    pub fn new() -> Self {
48        Self {}
49    }
50
51    pub fn run<F>(self, element: F) -> crate::Result<()>
52    where
53        F: Fn() -> Element + 'static,
54    {
55        GlobalWrapper::register_globals();
56
57        let root = document()?
58            .get_element_by_id("root")
59            .expect("No element with id: `root`");
60        let app = Self::render_dynamic(Rc::new(element));
61        root.append_child(&app)?;
62        // Self::render_elements_rec(&Elements::single(element()), &root);
63        Ok(())
64    }
65
66    pub fn run_result<F>(self, element: F) -> crate::Result<()>
67    where
68        F: Fn() -> crate::Result<Element> + 'static,
69    {
70        Self::run(self, crate::error::convert_fn_0("BrowserRenderer", element))
71    }
72
73    fn render_elements_rec(elements: &Elements, parent_node: &Node) {
74        match &elements.inner {
75            InnerElements::Element(e) => {
76                let node = Self::render(e.clone());
77                parent_node.append_child(&node).expect(ERROR_MSG);
78            }
79            InnerElements::Dynamic(d) => {
80                let node = Self::render_dynamic(d.clone());
81                parent_node.append_child(&node).expect(ERROR_MSG);
82            }
83            InnerElements::Elements(i) => {
84                for elements in i.iter() {
85                    Self::render_elements_rec(elements, parent_node);
86                }
87            }
88            InnerElements::Overlay(o) => {
89                Self::render_overlay(o.clone());
90            }
91        }
92    }
93
94    fn render_dynamic(dynamic: Rc<dyn Fn() -> Element + 'static>) -> Node {
95        signal_updater_callback_full(
96            |current: &Node| {
97                // Check if the current node is still connected to the DOM
98                current.is_connected()
99                    && current
100                        .parent_node()
101                        .map(|p| p.is_connected())
102                        .unwrap_or(false)
103            },
104            move |current| {
105                let new = Self::render(Rc::new(dynamic()));
106                if let Some(current) = current {
107                    current
108                        .parent_node()
109                        .expect(ERROR_MSG)
110                        .replace_child(&new, current)
111                        .expect(ERROR_MSG);
112                }
113                new
114            },
115        )
116    }
117
118    fn render_overlay(overlay: Rc<dyn Fn() -> Element + 'static>) {
119        let overlay = signal_updater_callback_full(
120            |_: &Node| true,
121            move |current| {
122                let new = Self::render(Rc::new(overlay()));
123                if let Some(current) = current {
124                    current
125                        .parent_node()
126                        .expect(ERROR_MSG)
127                        .replace_child(&new, current)
128                        .expect(ERROR_MSG);
129                }
130                new
131            },
132        );
133        body()
134            .expect(ERROR_MSG)
135            .append_child(&overlay)
136            .expect(ERROR_MSG);
137    }
138
139    pub fn render(elem: Rc<Element>) -> Node {
140        fn set_class(element: &web_sys::Element, elem: Rc<Element>) {
141            match elem.attributes.class_dynamic {
142                true => {
143                    let c = element.clone();
144                    let c1 = element.clone();
145                    signal_updater_callback(
146                        move || c1.is_connected(),
147                        move || {
148                            apply_tw_classes(&c, &super::build_multi_attrs(&elem.attributes.class))
149                                .expect(ERROR_MSG);
150                        },
151                    );
152                }
153                false => {
154                    if !elem.attributes.class.is_empty() {
155                        apply_tw_classes(
156                            element,
157                            &super::build_multi_attrs(&elem.attributes.class),
158                        )
159                        .expect(ERROR_MSG);
160                    }
161                }
162            };
163        }
164        fn set_style(element: &web_sys::Element, elem: Rc<Element>) {
165            match elem.attributes.style_dynamic {
166                true => {
167                    let c = element.clone();
168                    let c1 = element.clone();
169                    signal_updater_callback(
170                        move || c1.is_connected(),
171                        move || {
172                            let v = super::build_multi_attrs(&elem.attributes.style);
173                            c.set_attribute("style", v.as_str()).expect(ERROR_MSG);
174                        },
175                    )
176                }
177                false => {
178                    if !elem.attributes.style.is_empty() {
179                        element
180                            .set_attribute(
181                                "style",
182                                &super::build_multi_attrs(&elem.attributes.style),
183                            )
184                            .expect(ERROR_MSG)
185                    }
186                }
187            };
188        }
189        fn set_value(element: &web_sys::Element, attr: &html::AttributeValue) {
190            let value = match attr {
191                html::AttributeValue::Dynamic(f) => {
192                    let f = f.clone();
193                    let e1 = element.clone();
194                    let e2 = element.clone();
195                    signal_updater_callback(
196                        move || e1.is_connected(),
197                        move || {
198                            let value = f();
199                            e2.unchecked_ref::<web_sys::HtmlInputElement>()
200                                .set_value(value.str_value())
201                        },
202                    );
203                    return;
204                }
205                _ => attr.str_value(),
206            };
207            element
208                .unchecked_ref::<web_sys::HtmlInputElement>()
209                .set_value(value);
210        }
211        fn set_attr(
212            element: &web_sys::Element,
213            attr: &html::AttributeValue,
214            set_fn: impl Fn(&web_sys::Element, bool) + 'static,
215        ) {
216            if let html::AttributeValue::Dynamic(f) = attr {
217                let f = f.clone();
218                let e1 = element.clone();
219                let e2 = element.clone();
220                signal_updater_callback(
221                    move || e1.is_connected(),
222                    move || set_fn(&e2, f().is_true()),
223                );
224                return;
225            }
226            set_fn(element, attr.is_true());
227        }
228        let set_checked = |element, attr| {
229            set_attr(element, attr, |element, checked| {
230                element
231                    .unchecked_ref::<web_sys::HtmlInputElement>()
232                    .set_checked(checked);
233            })
234        };
235        let set_selected = |element, attr| {
236            set_attr(element, attr, |element, selected| {
237                element
238                    .unchecked_ref::<web_sys::HtmlOptionElement>()
239                    .set_selected(selected);
240            })
241        };
242        fn set_attrs(element: &web_sys::Element, attributes: &[html::Attribute]) {
243            for attr in attributes {
244                let html::Attribute { name, value } = attr;
245                match value {
246                    html::AttributeValue::False => element.remove_attribute(name).expect(ERROR_MSG),
247                    html::AttributeValue::True => element.set_attribute(name, "").expect(ERROR_MSG),
248
249                    html::AttributeValue::Static(value) => {
250                        element.set_attribute(name, value).expect(ERROR_MSG)
251                    }
252
253                    html::AttributeValue::Dynamic(f) => {
254                        let name = *name;
255                        let f = f.clone();
256                        let c = element.clone();
257                        let c1 = element.clone();
258                        signal_updater_callback(
259                            move || c1.is_connected(),
260                            move || match f() {
261                                html::AttributeValue::Dynamic(_) => unreachable!(
262                                    "A dynamic `AttributeValue` cannot return another dynamic `AttributeValue`"
263                                ),
264                                html::AttributeValue::False => {
265                                    c.remove_attribute(name).expect(ERROR_MSG)
266                                }
267                                html::AttributeValue::True => {
268                                    c.set_attribute(name, "").expect(ERROR_MSG)
269                                }
270                                html::AttributeValue::Static(value) => {
271                                    c.set_attribute(name, &value).expect(ERROR_MSG)
272                                }
273                            },
274                        );
275                    }
276                }
277            }
278        }
279
280        let document = document().expect(ERROR_MSG);
281
282        let node: Node = match &elem.inner {
283            InnerElement::HtmlTag {
284                tag,
285                children,
286                namespace,
287            } => {
288                // Cannot use `document.create_element_ns(None, ..)` here as this leads to unexpected behavior on the `display` property of the created elements
289                let element = match namespace.is_some() {
290                    true => document
291                        .create_element_ns(*namespace, tag)
292                        .expect(ERROR_MSG),
293                    false => document.create_element(tag).expect(ERROR_MSG),
294                };
295
296                // Class
297                set_class(&element, elem.clone());
298
299                // Style
300                set_style(&element, elem.clone());
301
302                // Value, Checked and Selected cannot be set using `set_attribute` as this does not update the property on the DOM element.
303                set_checked(&element, &elem.attributes.checked);
304                set_selected(&element, &elem.attributes.selected);
305
306                // Other attributes
307                // They have to be set before children are created, as some children depend on attributes being set, e.g. <select> options depend on the "multiple" attribute.
308                // If multiple <option> elements with a "selected" attribute are created before the "multiple" attribute is set on the parent <select>, only the last option will be selected.
309                set_attrs(&element, &elem.attributes.attributes);
310
311                // Children
312                Self::render_elements_rec(children, &element);
313
314                // Value has to be set after the children are created.
315                // This is important for elements like `HtmlSelectElement` that depend on child option elements.
316                // Otherwise setting the initial value does not work.
317                set_value(&element, &elem.attributes.value);
318
319                element.into()
320            }
321            InnerElement::Raw {
322                raw,
323                supported_type,
324            } => {
325                let parser = web_sys::DomParser::new().expect(ERROR_MSG);
326                let doc = parser
327                    .parse_from_string(raw, *supported_type)
328                    .expect(ERROR_MSG);
329                let element = doc.document_element().expect(ERROR_MSG);
330                set_class(&element, elem.clone());
331                set_style(&element, elem.clone());
332                set_checked(&element, &elem.attributes.checked);
333                set_selected(&element, &elem.attributes.selected);
334                set_value(&element, &elem.attributes.value);
335                set_attrs(&element, &elem.attributes.attributes);
336
337                element.into()
338            }
339            InnerElement::Text(text) => document.create_text_node(text).into(),
340            InnerElement::None => {
341                // Create an empty text node to represent None
342                // This ensures that if None is retured by a dynamic element, it can be replaced later by a real element.
343                document.create_text_node("").into()
344            }
345        };
346
347        // Listeners
348        for lstr in &elem.attributes.listener {
349            node.add_event_listener(lstr).expect(ERROR_MSG);
350        }
351
352        node
353    }
354}