hirola_dom/
lib.rs

1pub mod app;
2pub mod effects;
3pub mod mixins;
4pub mod node_ref;
5pub mod types;
6
7use core::fmt;
8use discard::{Discard, DiscardOnDrop};
9use hirola_core::prelude::cancelable_future;
10use hirola_core::render::Render;
11use hirola_core::{
12    generic_node::{EventListener, GenericNode},
13    prelude::CancelableFutureHandle,
14    render::Error,
15    BoxedLocal,
16};
17use std::rc::Rc;
18use std::{cell::RefCell, future::Future};
19use wasm_bindgen::{prelude::*, JsCast};
20pub use web_sys::Event;
21use web_sys::{Element, Node, Text};
22
23pub enum DomSideEffect {
24    UnMounted(BoxedLocal<()>),
25    Mounted(CancelableFutureHandle),
26}
27
28pub type EventHandlers = Rc<RefCell<Vec<Closure<dyn Fn(Event)>>>>;
29
30/// Rendering backend for the DOM.
31///
32/// The `DomNode` struct represents a node in the Document Object Model (DOM) and serves as the
33/// rendering backend for the frontend application. It allows interacting with DOM nodes directly
34/// and provides utility methods for type conversion and cloning.
35///
36/// _This API requires the following crate features to be activated: `dom`_
37///
38#[derive(Clone)]
39pub struct Dom {
40    pub node: Node,
41    pub side_effects: Rc<RefCell<Vec<DomSideEffect>>>,
42    event_handlers: EventHandlers,
43    children: RefCell<Vec<Dom>>,
44}
45
46impl fmt::Debug for Dom {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.debug_struct("Dom")
49            .field("node", &self.node)
50            .field("side_effects", &self.side_effects.borrow().len())
51            .field("event_handlers", &self.event_handlers.borrow())
52            .field("children", &self.children.borrow())
53            .finish()
54    }
55}
56
57impl Drop for Dom {
58    fn drop(&mut self) {
59        // self.discard()
60    }
61}
62
63impl PartialEq for Dom {
64    fn eq(&self, other: &Self) -> bool {
65        self.node == other.node
66    }
67}
68
69impl Eq for Dom {}
70
71impl Default for Dom {
72    fn default() -> Self {
73        Dom {
74            node: document().create_document_fragment().dyn_into().unwrap(),
75            side_effects: Default::default(),
76            event_handlers: Default::default(),
77            children: Default::default(),
78        }
79    }
80}
81
82impl Dom {
83    pub fn inner_html(&self) -> String {
84        {
85            let window = web_sys::window().unwrap();
86            let document = window.document().unwrap();
87            let element = document.create_element("div").unwrap();
88            crate::render_to(self.clone(), &element.clone().try_into().unwrap()).unwrap();
89            element.inner_html()
90        }
91    }
92
93    pub fn new_from_node(node: &Node) -> Self {
94        Dom {
95            node: node.clone(),
96            side_effects: Rc::new(RefCell::new(vec![])),
97            event_handlers: Rc::new(RefCell::new(vec![])),
98            children: RefCell::new(vec![]),
99        }
100    }
101
102    pub fn discard(&mut self) {
103        let _cleanup: Vec<()> = self
104            .event_handlers
105            .take()
106            .into_iter()
107            .map(|c| c.forget())
108            .collect();
109        let _cleanup: Vec<()> = self
110            .side_effects
111            .take()
112            .into_iter()
113            .map(|e| match e {
114                DomSideEffect::Mounted(e) => e.discard(),
115                DomSideEffect::UnMounted(_) => {
116                    log::warn!("Dropping a side effect that was not mounted")
117                }
118            })
119            .collect();
120    }
121}
122
123impl Dom {
124    /// Retrieves the inner DOM node contained within the `DomNode`.
125    ///
126    /// # Returns
127    ///
128    /// The underlying DOM node represented by this `DomNode`.
129    pub fn inner_element(&self) -> Node {
130        self.node.clone()
131    }
132    /// Converts the `DomNode` into a specified type using unchecked casting.
133    ///
134    /// This method allows converting the `DomNode` into a specific type, without performing a
135    /// runtime type check. It can be used when you are confident about the type of the DOM node,
136    /// and it avoids the overhead of dynamic type checking.
137    ///
138    /// # Type Parameters
139    ///
140    /// * `T` - The target type to convert the `DomNode` into. It should implement the `JsCast`
141    ///         trait, which provides the unchecked casting functionality.
142    ///
143    /// # Returns
144    ///
145    /// The converted `DomNode` as the target type `T`.
146    pub fn unchecked_into<T: JsCast>(self) -> T {
147        self.node.clone().unchecked_into()
148    }
149    /// Attempts to dynamically cast the `DomNode` into a specified type.
150    ///
151    /// This method performs a runtime type check to determine if the `DomNode` can be converted
152    /// into the desired type. If the conversion succeeds, it returns the converted value;
153    /// otherwise, it returns an error containing the original `DomNode`.
154    ///
155    /// # Type Parameters
156    ///
157    /// * `T` - The target type to cast the `DomNode` into. It should implement the `JsCast`
158    ///         trait, which provides the dynamic type casting functionality.
159    ///
160    /// # Returns
161    ///
162    /// - `Ok(T)` if the `DomNode` was successfully cast into the target type `T`.
163    /// - `Err(Node)` if the `DomNode` could not be cast into the target type `T`.
164
165    pub fn dyn_into<T: JsCast>(self) -> Result<T, Node> {
166        self.node.clone().dyn_into()
167    }
168}
169
170impl AsRef<JsValue> for Dom {
171    fn as_ref(&self) -> &JsValue {
172        self.node.as_ref()
173    }
174}
175
176impl From<Dom> for JsValue {
177    fn from(node: Dom) -> Self {
178        node.node.clone().into()
179    }
180}
181
182fn document() -> web_sys::Document {
183    web_sys::window().unwrap().document().unwrap()
184}
185
186impl GenericNode for Dom {
187    fn element(tag: &str) -> Self {
188        Dom::new_from_node(&document().create_element(tag).unwrap().dyn_into().unwrap())
189    }
190
191    fn text_node(text: &str) -> Self {
192        Dom::new_from_node(&document().create_text_node(text).into())
193    }
194
195    fn fragment() -> Self {
196        Dom::new_from_node(&document().create_document_fragment().dyn_into().unwrap())
197    }
198
199    fn marker() -> Self {
200        Dom::new_from_node(&document().create_comment("").into())
201    }
202
203    fn set_attribute(&self, name: &str, value: &str) {
204        self.node
205            .unchecked_ref::<Element>()
206            .set_attribute(name, value)
207            .unwrap();
208    }
209
210    fn append_child(&self, child: &Self) {
211        match self.node.append_child(&child.node) {
212            Err(e) => log::warn!("Could not append child: {e:?}"),
213            _ => {
214                self.children.borrow_mut().push(child.clone());
215            }
216        }
217    }
218
219    fn insert_child_before(&self, new_node: &Self, reference_node: Option<&Self>) {
220        match self
221            .node
222            .insert_before(&new_node.node, reference_node.map(|n| &n.node))
223        {
224            Ok(_) => {}
225            Err(e) => log::warn!("Failed to insert child: {e:?}"),
226        }
227    }
228
229    fn remove_child(&self, child: &Self) {
230        match self.node.remove_child(&child.node) {
231            Ok(_) => {}
232            Err(e) => log::warn!("Failed to remove child: {e:?}"),
233        };
234    }
235
236    fn replace_child(&self, old: &Self, new: &Self) {
237        match self.node.replace_child(&old.node, &new.node) {
238            Ok(_) => {}
239            Err(e) => log::warn!("Failed to replace child: {e:?}"),
240        };
241    }
242
243    fn insert_sibling_before(&self, child: &Self) {
244        self.node
245            .unchecked_ref::<Element>()
246            .before_with_node_1(&child.node)
247            .unwrap();
248    }
249
250    fn parent_node(&self) -> Option<Self> {
251        let n = self.node.parent_node().unwrap();
252        Some(Dom::new_from_node(&n))
253    }
254
255    fn next_sibling(&self) -> Option<Self> {
256        self.node
257            .next_sibling()
258            .map(|node| Dom::new_from_node(&node))
259    }
260
261    fn remove_self(&self) {
262        self.node.unchecked_ref::<Element>().remove();
263    }
264
265    fn update_inner_text(&self, text: &str) {
266        self.node
267            .dyn_ref::<Text>()
268            .unwrap()
269            .set_text_content(Some(text));
270    }
271    fn replace_children_with(&self, node: &Self) {
272        let element = self.node.unchecked_ref::<Element>();
273        element.replace_children_with_node_1(&node.inner_element())
274    }
275
276    fn effect(&self, future: impl std::future::Future<Output = ()> + 'static) {
277        self.side_effects
278            .borrow_mut()
279            .push(DomSideEffect::Mounted(DiscardOnDrop::leak(spawn(future))));
280    }
281
282    fn children(&self) -> RefCell<Vec<Self>> {
283        self.children.clone()
284    }
285}
286
287/// Mounts a [`Dom`] and runs it forever
288/// See also [`render`] with `parent` being the `<body>` tag.
289pub fn mount(dom: Dom) -> Result<(), Error> {
290    let window = web_sys::window().ok_or(Error::DomError(Box::new("could not acquire window")))?;
291    let document = window
292        .document()
293        .ok_or(Error::DomError(Box::new("could not acquire document")))?;
294
295    mount_to(
296        dom,
297        &document
298            .body()
299            .ok_or(Error::DomError(Box::new("could not acquire body")))?
300            .into(),
301    )?;
302    Ok(())
303}
304
305/// Mount a [`Dom`] to a `parent` node.
306/// For rendering under the `<body>` tag, use [`render()`] instead.
307
308pub fn mount_to(dom: Dom, parent: &web_sys::Node) -> Result<(), Error> {
309    let parent = Dom::new_from_node(parent);
310    parent.append_child(&dom);
311    std::mem::forget(parent);
312    Ok(())
313}
314
315/// Render a [`Dom`] into the DOM.
316/// Alias for [`render_to`] with `parent` being the `<body>` tag.
317pub fn render(dom: Dom) -> Result<Dom, Error> {
318    let window = web_sys::window().unwrap();
319    let document = window.document().unwrap();
320
321    render_to(dom, &document.body().unwrap())
322}
323
324/// Render a [`Dom`] under a `parent` node.
325/// For rendering under the `<body>` tag, use [`render()`] instead.
326
327pub fn render_to(dom: Dom, parent: &web_sys::Node) -> Result<Dom, Error> {
328    let parent = Dom::new_from_node(parent);
329    parent.append_child(&dom);
330    Ok(parent)
331}
332
333impl<F: Fn(web_sys::Event) + 'static> EventListener<F> for Dom {
334    fn event(&self, name: &str, handler: F) {
335        let closure: Closure<dyn Fn(web_sys::Event)> = Closure::wrap(Box::new(handler));
336        self.node
337            .add_event_listener_with_callback(name, closure.as_ref().unchecked_ref())
338            .unwrap();
339        self.event_handlers.borrow_mut().push(closure);
340    }
341}
342
343#[inline]
344pub fn spawn<F>(future: F) -> DiscardOnDrop<CancelableFutureHandle>
345where
346    F: Future<Output = ()> + 'static,
347{
348    let (handle, future) = cancelable_future(future, || ());
349
350    wasm_bindgen_futures::spawn_local(future);
351
352    handle
353}
354
355impl Render<Dom> for Dom {
356    fn render_into(self: Box<Self>, parent: &Dom) -> Result<(), Error> {
357        parent.append_child(&self);
358        Ok(())
359    }
360}
361
362pub mod dom_test_utils {
363    use wasm_bindgen::{prelude::Closure, JsCast};
364
365    pub fn next_tick_with<N: Clone + 'static>(with: &N, f: impl Fn(&N) + 'static) {
366        let with = with.clone();
367        let f: Box<dyn Fn()> = Box::new(move || f(&with));
368        let a = Closure::<dyn Fn()>::new(f);
369        web_sys::window()
370            .unwrap()
371            .set_timeout_with_callback(a.as_ref().unchecked_ref())
372            .unwrap();
373    }
374
375    pub fn next_tick<F: Fn() + 'static>(f: F) {
376        let a = Closure::<dyn Fn()>::new(f);
377        web_sys::window()
378            .unwrap()
379            .set_timeout_with_callback(a.as_ref().unchecked_ref())
380            .unwrap();
381    }
382}