avalanche_web/
lib.rs

1use avalanche::renderer::{NativeHandle, NativeType, Renderer, Scheduler};
2use avalanche::{Component, View};
3
4use avalanche::shared::Shared;
5
6use std::collections::{HashMap, VecDeque};
7use std::rc::Rc;
8
9use crate::components::{Attr, RawElement, Text};
10use crate::events::Event;
11use gloo_events::{EventListener, EventListenerOptions};
12use wasm_bindgen::JsCast;
13use web_sys::Element;
14
15pub mod components;
16pub mod events;
17
18static TIMEOUT_MSG_NAME: &str = "avalanche_web_message_name";
19
20pub fn mount<C: Component + Default>(element: Element) {
21    let child: View = C::default().into();
22    let native_parent = RawElement {
23        attrs: Default::default(),
24        attrs_updated: false,
25        children: vec![child.clone()],
26        children_updated: false,
27        value_controlled: false,
28        checked_controlled: false,
29        key: None,
30        location: (0, 0),
31        tag: "@root",
32    };
33
34    let renderer = WebRenderer::new();
35    let scheduler = WebScheduler::new();
36    let native_parent_handle = WebNativeHandle {
37        children_offset: element.child_nodes().length(),
38        node: element.into(),
39        listeners: Default::default(),
40    };
41
42    let root = avalanche::vdom::Root::new(
43        child,
44        native_parent.into(),
45        Box::new(native_parent_handle),
46        renderer,
47        scheduler,
48    );
49
50    // TODO: more elegant solution that leaks less memory?
51    Box::leak(Box::new(root));
52}
53
54/// Renders the given view in the current document's body.
55pub fn mount_to_body<C: Component + Default>() {
56    let body = web_sys::window()
57        .expect("window")
58        .document()
59        .expect("document")
60        .body()
61        .expect("body");
62    mount::<C>(body.into());
63}
64
65struct WebScheduler {
66    window: web_sys::Window,
67    queued_fns: Shared<VecDeque<Box<dyn FnOnce()>>>,
68    _listener: EventListener,
69}
70
71impl WebScheduler {
72    fn new() -> Self {
73        let window = web_sys::window().unwrap();
74        let queued_fns = Shared::default();
75        let queued_fns_clone = queued_fns.clone();
76
77        // sets up fast execution of 0ms timeouts
78        // uses approach in https://dbaron.org/log/20100309-faster-timeouts
79        let _listener = EventListener::new(&window, "message", move |e| {
80            let e = e.clone();
81            if let Ok(event) = e.dyn_into::<web_sys::MessageEvent>() {
82                if event.data() == TIMEOUT_MSG_NAME {
83                    event.stop_propagation();
84                    // f may call schedule_on_ui_thread, so it must be called outside of exec_mut
85                    let f = queued_fns_clone
86                        .exec_mut(|queue: &mut VecDeque<Box<dyn FnOnce()>>| queue.pop_front());
87                    if let Some(f) = f {
88                        f();
89                    }
90                }
91            }
92        });
93
94        WebScheduler {
95            window,
96            queued_fns,
97            _listener,
98        }
99    }
100}
101
102impl Scheduler for WebScheduler {
103    fn schedule_on_ui_thread(&mut self, f: Box<dyn FnOnce()>) {
104        // post message for 0ms timeouts
105        // technique from https://dbaron.org/log/20100309-faster-timeouts
106        self.queued_fns.exec_mut(move |queue| {
107            queue.push_back(f);
108        });
109        self.window
110            .post_message(&TIMEOUT_MSG_NAME.into(), "*")
111            .unwrap();
112    }
113}
114
115struct WebNativeHandle {
116    node: web_sys::Node,
117    listeners: HashMap<&'static str, EventListener>,
118    /// position at which renderer indexing should begin
119    // TODO: more memory-efficient implementation?
120    children_offset: u32,
121}
122
123struct WebRenderer {
124    document: web_sys::Document,
125}
126
127impl WebRenderer {
128    fn new() -> Self {
129        WebRenderer {
130            document: web_sys::window().unwrap().document().unwrap(),
131        }
132    }
133
134    fn get_child(parent: &web_sys::Element, child_idx: usize, offset: u32) -> web_sys::Node {
135        Self::try_get_child(parent, child_idx, offset).unwrap()
136    }
137
138    fn try_get_child(
139        parent: &web_sys::Element,
140        child_idx: usize,
141        offset: u32,
142    ) -> Option<web_sys::Node> {
143        parent.child_nodes().item(child_idx as u32 + offset)
144    }
145
146    fn assert_handler_avalanche_web(native_type: &NativeType) {
147        assert_eq!(
148            native_type.handler, "avalanche_web",
149            "handler is not of type \"avalanche_web\""
150        )
151    }
152
153    fn handle_cast(native_handle: &NativeHandle) -> &WebNativeHandle {
154        native_handle
155            .downcast_ref::<WebNativeHandle>()
156            .expect("WebNativeHandle")
157    }
158
159    fn node_to_element(node: web_sys::Node) -> web_sys::Element {
160        node.dyn_into::<web_sys::Element>()
161            .expect("Element (not Text node)")
162    }
163}
164
165impl Renderer for WebRenderer {
166    fn create_component(&mut self, native_type: &NativeType, component: &View) -> NativeHandle {
167        let elem = match native_type.handler {
168            "avalanche_web_text" => {
169                let text_node = match component.downcast_ref::<Text>() {
170                    Some(text) => self.document.create_text_node(&text.text),
171                    None => panic!("WebRenderer: expected Text component for avalanche_web_text."),
172                };
173                WebNativeHandle {
174                    node: web_sys::Node::from(text_node),
175                    listeners: HashMap::new(),
176                    children_offset: 0,
177                }
178            }
179            "avalanche_web" => {
180                assert_ne!(
181                    native_type.name, "",
182                    "WebRenderer: expected tag name to not be empty."
183                );
184                let raw_element = component
185                    .downcast_ref::<RawElement>()
186                    .expect("component of type RawElement");
187
188                let element = self
189                    .document
190                    .create_element(native_type.name)
191                    .expect("WebRenderer: element creation failed from syntax error.");
192
193                let mut listeners = HashMap::new();
194
195                if raw_element.value_controlled {
196                    add_named_listener(
197                        &element,
198                        "input",
199                        "#v",
200                        false,
201                        Rc::new(|e| e.prevent_default()),
202                        &mut listeners,
203                    );
204                }
205                if raw_element.checked_controlled {
206                    add_named_listener(
207                        &element,
208                        "change",
209                        "#c",
210                        false,
211                        Rc::new(|e| e.prevent_default()),
212                        &mut listeners,
213                    );
214                }
215
216                match raw_element.tag {
217                    "input" => {
218                        let input_element = element
219                            .clone()
220                            .dyn_into::<web_sys::HtmlInputElement>()
221                            .expect("HTMLInputElement");
222
223                        for (name, (attr, _)) in raw_element.attrs.iter() {
224                            match attr {
225                                Attr::Prop(prop) => {
226                                    if let Some(prop) = prop {
227                                        match *name {
228                                            "value" => {
229                                                input_element.set_value(prop);
230                                            }
231                                            "checked" => {
232                                                input_element.set_checked(!prop.is_empty());
233                                            }
234                                            _ => {
235                                                input_element.set_attribute(name, prop).unwrap();
236                                            }
237                                        }
238                                    }
239                                }
240                                Attr::Handler(handler) => {
241                                    add_listener(&element, name, handler.clone(), &mut listeners)
242                                }
243                            }
244                        }
245                    }
246                    "textarea" => {
247                        let text_area_element = element
248                            .clone()
249                            .dyn_into::<web_sys::HtmlTextAreaElement>()
250                            .expect("HTMLTextAreaElement");
251
252                        for (name, (attr, _)) in raw_element.attrs.iter() {
253                            match attr {
254                                Attr::Prop(prop) => {
255                                    if let Some(prop) = prop {
256                                        match *name {
257                                            "value" => text_area_element.set_value(prop),
258                                            _ => {
259                                                text_area_element.set_attribute(name, prop).unwrap()
260                                            }
261                                        }
262                                    }
263                                }
264                                Attr::Handler(handler) => {
265                                    add_listener(&element, name, handler.clone(), &mut listeners)
266                                }
267                            }
268                        }
269                    }
270                    _ => {
271                        for (name, (attr, _)) in raw_element.attrs.iter() {
272                            match attr {
273                                Attr::Prop(prop) => {
274                                    if let Some(prop) = prop {
275                                        element.set_attribute(name, prop).unwrap();
276                                    }
277                                }
278                                Attr::Handler(handler) => {
279                                    add_listener(&element, name, handler.clone(), &mut listeners);
280                                }
281                            }
282                        }
283                    }
284                }
285
286                WebNativeHandle {
287                    node: web_sys::Node::from(element),
288                    listeners,
289                    children_offset: 0,
290                }
291            }
292            _ => panic!("Custom handlers not implemented yet."),
293        };
294
295        Box::new(elem)
296    }
297
298    fn update_component(
299        &mut self,
300        native_type: &NativeType,
301        native_handle: &mut NativeHandle,
302        component: &View,
303    ) {
304        let web_handle = native_handle.downcast_mut::<WebNativeHandle>().unwrap();
305        match native_type.handler {
306            "avalanche_web" => {
307                let node = web_handle.node.clone();
308                let element = node.dyn_into::<web_sys::Element>().unwrap();
309                let raw_element = component
310                    .downcast_ref::<RawElement>()
311                    .expect("component of type RawElement");
312
313                if raw_element.attrs_updated {
314                    match raw_element.tag {
315                        "input" => {
316                            let input_element = element
317                                .clone()
318                                .dyn_into::<web_sys::HtmlInputElement>()
319                                .expect("HTMLInputElement");
320                            for (name, (attr, updated)) in raw_element.attrs.iter() {
321                                if *updated {
322                                    match attr {
323                                        Attr::Prop(prop) => match *name {
324                                            "value" => {
325                                                if let Some(prop) = prop {
326                                                    input_element.set_value(prop);
327                                                }
328                                            }
329                                            "checked" => {
330                                                input_element.set_checked(prop.is_some());
331                                            }
332                                            _ => {
333                                                update_generic_prop(&element, name, prop.as_deref())
334                                            }
335                                        },
336                                        Attr::Handler(handler) => {
337                                            update_listener(
338                                                &element,
339                                                name,
340                                                handler.clone(),
341                                                &mut web_handle.listeners,
342                                            );
343                                        }
344                                    }
345                                }
346                            }
347                        }
348                        "textarea" => {
349                            let text_area_element = element
350                                .clone()
351                                .dyn_into::<web_sys::HtmlTextAreaElement>()
352                                .expect("HTMLTextAreaElement");
353                            for (name, (attr, updated)) in raw_element.attrs.iter() {
354                                if *updated {
355                                    match attr {
356                                        Attr::Prop(prop) => {
357                                            if *name == "value" {
358                                                if let Some(prop) = prop {
359                                                    text_area_element.set_value(prop);
360                                                }
361                                            } else {
362                                                update_generic_prop(&element, name, prop.as_deref())
363                                            }
364                                        }
365                                        Attr::Handler(handler) => {
366                                            update_listener(
367                                                &element,
368                                                name,
369                                                handler.clone(),
370                                                &mut web_handle.listeners,
371                                            );
372                                        }
373                                    }
374                                }
375                            }
376                        }
377                        _ => {
378                            for (name, (attr, updated)) in raw_element.attrs.iter() {
379                                if *updated {
380                                    match attr {
381                                        Attr::Prop(prop) => {
382                                            update_generic_prop(&element, name, prop.as_deref())
383                                        }
384                                        Attr::Handler(handler) => {
385                                            update_listener(
386                                                &element,
387                                                name,
388                                                handler.clone(),
389                                                &mut web_handle.listeners,
390                                            );
391                                        }
392                                    }
393                                }
394                            }
395                        }
396                    }
397                }
398            }
399            "avalanche_web_text" => {
400                let new_text = component.downcast_ref::<Text>().expect("Text component");
401                if new_text.updated() {
402                    //TODO: compare with old text?
403                    web_handle.node.set_text_content(Some(&new_text.text));
404                }
405            }
406            _ => panic!("Custom handlers not implemented yet."),
407        };
408    }
409
410    fn append_child(
411        &mut self,
412        parent_type: &NativeType,
413        parent_handle: &mut NativeHandle,
414        _child_type: &NativeType,
415        child_handle: &NativeHandle,
416    ) {
417        Self::assert_handler_avalanche_web(parent_type);
418        let parent_node = Self::handle_cast(parent_handle).node.clone();
419        let parent_element = Self::node_to_element(parent_node);
420        let child_node = &Self::handle_cast(child_handle).node;
421        parent_element
422            .append_with_node_1(child_node)
423            .expect("append success");
424    }
425
426    fn insert_child(
427        &mut self,
428        parent_type: &NativeType,
429        parent_handle: &mut NativeHandle,
430        index: usize,
431        _child_type: &NativeType,
432        child_handle: &NativeHandle,
433    ) {
434        self.log("inserting child");
435        Self::assert_handler_avalanche_web(parent_type);
436        let parent_handle = Self::handle_cast(parent_handle);
437        let parent_element = Self::node_to_element(parent_handle.node.clone());
438        let child_node = &Self::handle_cast(child_handle).node;
439        let component_after =
440            Self::try_get_child(&parent_element, index, parent_handle.children_offset);
441        parent_element
442            .insert_before(child_node, component_after.as_ref())
443            .expect("insert success");
444    }
445
446    fn swap_children(
447        &mut self,
448        parent_type: &NativeType,
449        parent_handle: &mut NativeHandle,
450        a: usize,
451        b: usize,
452    ) {
453        Self::assert_handler_avalanche_web(parent_type);
454        let parent_handle = Self::handle_cast(parent_handle);
455        let parent_element = Self::node_to_element(parent_handle.node.clone());
456        let lesser = std::cmp::min(a, b);
457        let greater = std::cmp::max(a, b);
458
459        // TODO: throw exception if a and b are equal but out of bounds?
460        if a != b {
461            let a = Self::get_child(&parent_element, lesser, parent_handle.children_offset);
462            let b = Self::get_child(&parent_element, greater, parent_handle.children_offset);
463            let after_b = b.next_sibling();
464            // note: idiosyncratic order, a is being replaced with b
465            parent_element
466                .replace_child(&b, &a)
467                .expect("replace succeeded");
468            parent_element
469                .insert_before(&a, after_b.as_ref())
470                .expect("insert succeeded");
471        }
472    }
473
474    fn replace_child(
475        &mut self,
476        parent_type: &NativeType,
477        parent_handle: &mut NativeHandle,
478        index: usize,
479        _child_type: &NativeType,
480        child_handle: &NativeHandle,
481    ) {
482        Self::assert_handler_avalanche_web(parent_type);
483        let parent_handle = Self::handle_cast(parent_handle);
484        let parent_element = Self::node_to_element(parent_handle.node.clone());
485        let curr_child_node =
486            Self::get_child(&parent_element, index, parent_handle.children_offset);
487        let replace_child_node = &Self::handle_cast(child_handle).node;
488        if &curr_child_node != replace_child_node {
489            parent_element
490                .replace_child(replace_child_node, &curr_child_node)
491                .expect("successful replace");
492        }
493    }
494
495    fn move_child(
496        &mut self,
497        parent_type: &NativeType,
498        parent_handle: &mut NativeHandle,
499        old: usize,
500        new: usize,
501    ) {
502        Self::assert_handler_avalanche_web(parent_type);
503        let parent_handle = Self::handle_cast(parent_handle);
504        let parent_element = Self::node_to_element(parent_handle.node.clone());
505        let curr_child_node = Self::get_child(&parent_element, old, parent_handle.children_offset);
506        let removed = parent_element
507            .remove_child(&curr_child_node)
508            .expect("successful remove");
509        let component_after_insert =
510            Self::try_get_child(&parent_element, new, parent_handle.children_offset);
511        parent_element
512            .insert_before(&removed, component_after_insert.as_ref())
513            .expect("insert success");
514    }
515
516    fn remove_child(
517        &mut self,
518        parent_type: &NativeType,
519        parent_handle: &mut NativeHandle,
520        index: usize,
521    ) {
522        Self::assert_handler_avalanche_web(parent_type);
523        let parent_handle = Self::handle_cast(parent_handle);
524        let parent_element = Self::node_to_element(parent_handle.node.clone());
525        let child_node = Self::get_child(&parent_element, index, parent_handle.children_offset);
526        parent_element
527            .remove_child(&child_node)
528            .expect("successful remove");
529    }
530
531    fn log(&self, string: &str) {
532        let js_val: wasm_bindgen::JsValue = string.into();
533        web_sys::console::log_1(&js_val);
534    }
535}
536
537fn update_generic_prop(element: &Element, name: &str, prop: Option<&str>) {
538    match prop {
539        Some(prop) => {
540            element.set_attribute(name, prop).unwrap();
541        }
542        None => {
543            element.remove_attribute(name).unwrap();
544        }
545    }
546}
547
548fn add_listener(
549    element: &web_sys::Element,
550    name: &'static str,
551    callback: Rc<dyn Fn(Event)>,
552    listeners: &mut HashMap<&'static str, EventListener>,
553) {
554    add_named_listener(element, name, name, true, callback, listeners)
555}
556
557fn add_named_listener(
558    element: &web_sys::Element,
559    event: &'static str,
560    name: &'static str,
561    passive: bool,
562    callback: Rc<dyn Fn(Event)>,
563    listeners: &mut HashMap<&'static str, EventListener>,
564) {
565    let options = EventListenerOptions {
566        passive,
567        ..Default::default()
568    };
569    let listener = EventListener::new_with_options(element, event, options, move |event| {
570        callback(event.clone())
571    });
572    listeners.insert(name, listener);
573}
574
575fn update_listener(
576    element: &web_sys::Element,
577    name: &'static str,
578    callback: Rc<dyn Fn(Event)>,
579    listeners: &mut HashMap<&'static str, EventListener>,
580) {
581    let _ = listeners.remove(name);
582    let listener = EventListener::new(element, name, move |event| callback(event.clone()));
583    listeners.insert(name, listener);
584}
585
586// Mdbook's testing doesn't quite work, so we inject our book test cases into the crate to make sure they compile.
587#[cfg(doctest)]
588mod book_tests {
589    use doc_comment::doc_comment;
590    doc_comment!(include_str!("../../docs/src/getting_started.md"));
591    doc_comment!(include_str!("../../docs/src/basic_components.md"));
592    doc_comment!(include_str!("../../docs/src/state.md"));
593    doc_comment!(include_str!("../../docs/src/reactivity.md"));
594    doc_comment!(include_str!("../../docs/src/events.md"));
595}