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 Box::leak(Box::new(root));
52}
53
54pub 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 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 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 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 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 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 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 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#[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}