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#[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 }
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 pub fn inner_element(&self) -> Node {
130 self.node.clone()
131 }
132 pub fn unchecked_into<T: JsCast>(self) -> T {
147 self.node.clone().unchecked_into()
148 }
149 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
287pub 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
305pub 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
315pub 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
324pub 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}