respo/app/
patch.rs

1use std::cmp::Ordering;
2use std::fmt::Debug;
3
4use wasm_bindgen::prelude::Closure;
5use web_sys::{
6  Element, FocusEvent, HtmlElement, HtmlInputElement, HtmlLabelElement, HtmlTextAreaElement, InputEvent, KeyboardEvent, MouseEvent,
7  Node,
8};
9
10use wasm_bindgen::JsCast;
11use web_sys::console::warn_1;
12
13use crate::node::{RespoComponent, RespoEffectType, RespoEvent, RespoEventMark, RespoEventMarkFn, RespoNode};
14
15use super::renderer::load_coord_target_tree;
16use super::util;
17use crate::node::dom_change::{ChildDomOp, DomChange, RespoCoord};
18
19use crate::app::renderer::build_dom_tree;
20
21pub fn patch_tree<T>(
22  tree: &RespoNode<T>,
23  old_tree: &RespoNode<T>,
24  mount_target: &Node,
25  changes: &[DomChange<T>],
26  handle_event: RespoEventMarkFn,
27) -> Result<(), String>
28where
29  T: Debug + Clone,
30{
31  // let el = mount_target.dyn_ref::<Element>().expect("to element");
32
33  if mount_target.child_nodes().length() != 1 {
34    return Err(format!(
35      "expected a single node under mount target, got: {:?}",
36      mount_target.child_nodes().length()
37    ));
38  }
39
40  // handle BeforeUpdate before DOM changes
41  for op in changes {
42    if let DomChange::Effect {
43      coord,
44      effect_type,
45      skip_indexes,
46      ..
47    } = op
48    {
49      if effect_type == &RespoEffectType::BeforeUpdate {
50        let target = find_coord_dom_target(&mount_target.first_child().ok_or("mount position")?, op.get_dom_path())?;
51        let target_tree = if effect_type == &RespoEffectType::BeforeUnmount {
52          load_coord_target_tree(old_tree, coord)?
53        } else {
54          load_coord_target_tree(tree, coord)?
55        };
56        if let RespoNode::Component(RespoComponent { effects, .. }) = target_tree {
57          for (idx, effect) in effects.iter().enumerate() {
58            if !skip_indexes.contains(&(idx as u32)) {
59              effect.0.as_ref().run(effect_type.to_owned(), &target)?;
60            }
61          }
62        } else {
63          crate::util::warn_log!("expected component for effects, got: {}", target_tree);
64        }
65      }
66    }
67  }
68
69  for op in changes {
70    // crate::util::log!("op: {:?}", op);
71    let target = find_coord_dom_target(&mount_target.first_child().ok_or("mount position")?, op.get_dom_path())?;
72    match op {
73      DomChange::ModifyAttrs { set, unset, .. } => {
74        let el = target.dyn_ref::<Element>().expect("load as element");
75        for (k, v) in set {
76          let k = k.as_ref();
77          if k == "innerText" {
78            el.dyn_ref::<HtmlElement>().ok_or("to html element")?.set_inner_text(v);
79          } else if k == "innerHTML" {
80            el.set_inner_html(v);
81          } else if k == "htmlFor" {
82            el.dyn_ref::<HtmlLabelElement>().ok_or("to label element")?.set_html_for(v);
83          } else if k == "value" {
84            match el.tag_name().as_str() {
85              "INPUT" => {
86                let input_el = el.dyn_ref::<HtmlInputElement>().expect("to input");
87                let prev_value = input_el.value();
88                if &prev_value != v {
89                  input_el.set_value(v);
90                }
91              }
92              "TEXTAREA" => {
93                let textarea_el = el.dyn_ref::<HtmlTextAreaElement>().expect("to textarea");
94                let prev_value = textarea_el.value();
95                if &prev_value != v {
96                  textarea_el.set_value(v);
97                }
98              }
99              name => {
100                return Err(format!("unsupported value for {}", name));
101              }
102            }
103          } else {
104            el.set_attribute(k, v).expect("to set attribute");
105          }
106        }
107        for k in unset {
108          let k = k.as_ref();
109          if k == "innerText" {
110            el.dyn_ref::<HtmlElement>().ok_or("to html element")?.set_inner_text("");
111          } else if k == "innerHTML" {
112            el.set_inner_html("");
113          } else if k == "value" {
114            let input_el = el.dyn_ref::<HtmlInputElement>().expect("to input");
115            let prev_value = input_el.value();
116            if !prev_value.is_empty() {
117              input_el.set_value("");
118            }
119          } else {
120            el.remove_attribute(k).expect("to remove attribute");
121          }
122        }
123      }
124      DomChange::ModifyStyle { set, unset, .. } => {
125        let style = target.dyn_ref::<HtmlElement>().expect("into html element").style();
126        for s in unset {
127          style.remove_property(s).expect("remove style");
128        }
129        for (k, v) in set {
130          style.set_property(k, v).expect("set style");
131        }
132      }
133      DomChange::ModifyEvent { add, remove, coord, .. } => {
134        let el = target.dyn_ref::<Element>().expect("to element");
135        for k in add.iter() {
136          attach_event(el, k, coord, handle_event.to_owned())?;
137        }
138        let el = el.dyn_ref::<HtmlElement>().expect("html element");
139        for k in remove {
140          match k.as_ref() {
141            "click" => {
142              el.set_onclick(None);
143            }
144            "input" => {
145              el.set_oninput(None);
146            }
147            _ => warn_1(&format!("TODO event {}", k).into()),
148          }
149        }
150      }
151      DomChange::ReplaceElement { node, coord, .. } => {
152        let parent = target.parent_element().expect("load parent");
153        let new_element = build_dom_tree(node, coord, handle_event.to_owned()).expect("build element");
154        parent
155          .dyn_ref::<Node>()
156          .expect("to node")
157          .insert_before(&new_element, Some(&target))
158          .expect("element inserted");
159        target.dyn_ref::<Element>().expect("get node").remove();
160      }
161      DomChange::ModifyChildren { operations, coord, .. } => {
162        let base_tree = load_coord_target_tree(tree, coord)?;
163        let old_base_tree = load_coord_target_tree(old_tree, coord)?;
164        for op in operations {
165          let handler = handle_event.to_owned();
166          match op {
167            ChildDomOp::Append(k, node) => {
168              let mut next_coord = coord.to_owned();
169              next_coord.push(RespoCoord::Key(k.to_owned()));
170              let new_element = build_dom_tree(node, &next_coord, handler).expect("new element");
171              target
172                .dyn_ref::<Node>()
173                .expect("to node")
174                .append_child(&new_element)
175                .expect("element appended");
176            }
177            ChildDomOp::Prepend(k, node) => {
178              let mut next_coord = coord.to_owned();
179              next_coord.push(RespoCoord::Key(k.to_owned()));
180              let new_element = build_dom_tree(node, &next_coord, handler).expect("new element");
181              if target.child_nodes().length() == 0 {
182                target
183                  .dyn_ref::<Node>()
184                  .expect("to node")
185                  .append_child(&new_element)
186                  .expect("element appended");
187              } else {
188                let base = target.dyn_ref::<Node>().expect("to node").first_child().ok_or("to first child")?;
189                target
190                  .dyn_ref::<Node>()
191                  .expect("to node")
192                  .insert_before(&new_element, Some(&base))
193                  .expect("element appended");
194              }
195            }
196            ChildDomOp::RemoveAt(idx) => {
197              let child = target
198                .dyn_ref::<Element>()
199                .expect("get node")
200                .children()
201                .item(*idx)
202                .ok_or_else(|| {
203                  util::warn_log!("child not found at {:?}", coord);
204                  format!("child to remove not found at {}", &idx)
205                })?;
206              target.remove_child(&child).expect("child removed");
207            }
208            ChildDomOp::InsertAfter(idx, k, node) => {
209              let children = target.dyn_ref::<Element>().expect("get node").children();
210              if idx >= &children.length() {
211                return Err(format!("child to insert not found at {}", &idx));
212              } else {
213                let handler = handle_event.to_owned();
214                let mut next_coord = coord.to_owned();
215                next_coord.push(RespoCoord::Key(k.to_owned()));
216                let new_element = build_dom_tree(node, &next_coord, handler).expect("new element");
217                match (idx + 1).cmp(&children.length()) {
218                  Ordering::Less => {
219                    let child = children.item(*idx + 1).ok_or_else(|| format!("child not found at {}", &idx))?;
220                    target.insert_before(&new_element, Some(&child)).expect("element inserted");
221                  }
222                  Ordering::Equal => {
223                    target.append_child(&new_element).expect("element appended");
224                  }
225                  Ordering::Greater => {
226                    return Err(format!("out of bounds: {} of {} at coord {:?}", idx, &children.length(), coord));
227                  }
228                }
229              }
230            }
231            ChildDomOp::NestedEffect {
232              nested_coord,
233              nested_dom_path: nesteed_dom_path,
234              effect_type,
235              skip_indexes,
236            } => {
237              let target_tree = if effect_type == &RespoEffectType::BeforeUnmount {
238                load_coord_target_tree(&old_base_tree, nested_coord)?
239              } else {
240                load_coord_target_tree(&base_tree, nested_coord)?
241              };
242              let nested_el = find_coord_dom_target(&target, nesteed_dom_path)?;
243              if let RespoNode::Component(RespoComponent { effects, .. }) = target_tree {
244                for (idx, effect) in effects.iter().enumerate() {
245                  if !skip_indexes.contains(&(idx as u32)) {
246                    effect.0.run(effect_type.to_owned(), &nested_el)?;
247                  }
248                }
249              } else {
250                crate::util::warn_log!("expected component for effects, got: {}", target_tree);
251              }
252            }
253          }
254        }
255      }
256
257      DomChange::Effect {
258        coord,
259        effect_type,
260        skip_indexes,
261        ..
262      } => {
263        if effect_type == &RespoEffectType::BeforeUpdate {
264          // should be handled before current pass
265          continue;
266        }
267        let target_tree = if effect_type == &RespoEffectType::BeforeUnmount {
268          load_coord_target_tree(old_tree, coord)?
269        } else {
270          load_coord_target_tree(tree, coord)?
271        };
272        if let RespoNode::Component(RespoComponent { effects, .. }) = target_tree {
273          for (idx, effect) in effects.iter().enumerate() {
274            if !skip_indexes.contains(&(idx as u32)) {
275              effect.0.run(effect_type.to_owned(), &target)?;
276            }
277          }
278        } else {
279          crate::util::warn_log!("expected component for effects, got: {}", target_tree);
280        }
281      }
282    }
283  }
284  Ok(())
285}
286
287fn find_coord_dom_target(mount_target: &Node, coord: &[u32]) -> Result<Node, String> {
288  let mut target = mount_target.to_owned();
289  for idx in coord {
290    let child = target.child_nodes().item(idx.to_owned());
291    if child.is_none() {
292      return Err(format!("no child at index {}", &idx));
293    }
294    target = child.ok_or_else(|| format!("does not find child at index: {}", &idx))?;
295  }
296  Ok(target)
297}
298
299pub fn attach_event(element: &Element, key: &str, coord: &[RespoCoord], handle_event: RespoEventMarkFn) -> Result<(), String> {
300  let coord = coord.to_owned();
301  // crate::util::log!("attach event {}", key);
302  match key {
303    "click" => {
304      let handler = Closure::wrap(Box::new(move |e: MouseEvent| {
305        let wrap_event = RespoEvent::Click {
306          client_x: e.client_x() as f64,
307          client_y: e.client_y() as f64,
308          original_event: e,
309        };
310        handle_event
311          .run(RespoEventMark::new("click", &coord, wrap_event))
312          .expect("handle click event");
313      }) as Box<dyn FnMut(MouseEvent)>);
314      element
315        .dyn_ref::<HtmlElement>()
316        .expect("convert to html element")
317        .set_onclick(Some(handler.as_ref().unchecked_ref()));
318      handler.forget();
319    }
320
321    "dblclick" => {
322      let handler = Closure::wrap(Box::new(move |e: MouseEvent| {
323        let wrap_event = RespoEvent::Click {
324          client_x: e.client_x() as f64,
325          client_y: e.client_y() as f64,
326          original_event: e,
327        };
328        handle_event
329          .run(RespoEventMark::new("dblclick", &coord, wrap_event))
330          .expect("handle dblclick event");
331      }) as Box<dyn FnMut(MouseEvent)>);
332      element
333        .dyn_ref::<HtmlElement>()
334        .expect("convert to html element")
335        .set_ondblclick(Some(handler.as_ref().unchecked_ref()));
336      handler.forget();
337    }
338    "input" => {
339      let handler = Closure::wrap(Box::new(move |e: InputEvent| {
340        let target = e.target().expect("get target");
341        let el = target.dyn_ref::<Element>().unwrap();
342        let value = match el.tag_name().as_str() {
343          "INPUT" => el.dyn_ref::<HtmlInputElement>().expect("to convert to html input element").value(),
344          "TEXTAREA" => el
345            .dyn_ref::<HtmlTextAreaElement>()
346            .expect("to convert to html text area element")
347            .value(),
348          _ => {
349            // TODO Error
350            return;
351          }
352        };
353        let wrap_event = RespoEvent::Input { value, original_event: e };
354        handle_event
355          .run(RespoEventMark::new("input", &coord, wrap_event))
356          .expect("handle input event");
357      }) as Box<dyn FnMut(InputEvent)>);
358      match element.tag_name().as_str() {
359        "INPUT" => {
360          element
361            .dyn_ref::<HtmlInputElement>()
362            .expect("convert to html input element")
363            .set_oninput(Some(handler.as_ref().unchecked_ref()));
364        }
365        "TEXTAREA" => {
366          element
367            .dyn_ref::<HtmlTextAreaElement>()
368            .expect("convert to html textarea element")
369            .set_oninput(Some(handler.as_ref().unchecked_ref()));
370        }
371        _ => {
372          return Err(format!("unsupported input event: {}", element.tag_name()));
373        }
374      }
375
376      handler.forget();
377    }
378    "change" => {
379      let handler = Closure::wrap(Box::new(move |e: InputEvent| {
380        let wrap_event = RespoEvent::Input {
381          value: e
382            .target()
383            .expect("to reach event target")
384            .dyn_ref::<HtmlInputElement>()
385            .expect("to convert to html input element")
386            .value(),
387          original_event: e,
388        };
389        handle_event
390          .run(RespoEventMark::new("change", &coord, wrap_event))
391          .expect("handle change event");
392      }) as Box<dyn FnMut(InputEvent)>);
393      match element.tag_name().as_str() {
394        "INPUT" => {
395          element
396            .dyn_ref::<HtmlInputElement>()
397            .expect("convert to html input element")
398            .set_onchange(Some(handler.as_ref().unchecked_ref()));
399          handler.forget();
400        }
401        "TEXTAREA" => {
402          element
403            .dyn_ref::<HtmlTextAreaElement>()
404            .expect("convert to html input element")
405            .set_onchange(Some(handler.as_ref().unchecked_ref()));
406          handler.forget();
407        }
408        _ => {
409          util::warn_log!("not handled change event for element: {}", element.tag_name());
410        }
411      }
412    }
413    "keydown" => {
414      let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| {
415        // crate::util::log!("calling handler");
416        let wrap_event = RespoEvent::Keyboard {
417          key: e.key(),
418          key_code: e.key_code(),
419          shift_key: e.shift_key(),
420          ctrl_key: e.ctrl_key(),
421          alt_key: e.alt_key(),
422          meta_key: e.meta_key(),
423          repeat: e.repeat(),
424          original_event: e,
425        };
426        handle_event
427          .run(RespoEventMark::new("keydown", &coord, wrap_event))
428          .expect("handle keydown event");
429      }) as Box<dyn FnMut(KeyboardEvent)>);
430
431      match element.tag_name().as_str() {
432        "INPUT" => {
433          element
434            .dyn_ref::<HtmlInputElement>()
435            .expect("convert to html input element")
436            .set_onkeydown(Some(handler.as_ref().unchecked_ref()));
437          handler.forget();
438        }
439        "TEXTAREA" => {
440          element
441            .dyn_ref::<HtmlTextAreaElement>()
442            .expect("convert to html input element")
443            .set_onkeydown(Some(handler.as_ref().unchecked_ref()));
444          handler.forget();
445        }
446        _ => {
447          util::warn_log!("not handled keydown event for element: {}", element.tag_name());
448        }
449      }
450    }
451    "keyup" => {
452      let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| {
453        let wrap_event = RespoEvent::Keyboard {
454          key: e.key(),
455          key_code: e.key_code(),
456          shift_key: e.shift_key(),
457          ctrl_key: e.ctrl_key(),
458          alt_key: e.alt_key(),
459          meta_key: e.meta_key(),
460          repeat: e.repeat(),
461          original_event: e,
462        };
463        handle_event
464          .run(RespoEventMark::new("keyup", &coord, wrap_event))
465          .expect("handle keyup event");
466      }) as Box<dyn FnMut(KeyboardEvent)>);
467      match element.tag_name().as_str() {
468        "INPUT" => {
469          element
470            .dyn_ref::<HtmlInputElement>()
471            .expect("convert to html input element")
472            .set_onkeyup(Some(handler.as_ref().unchecked_ref()));
473          handler.forget();
474        }
475        "TEXTAREA" => {
476          element
477            .dyn_ref::<HtmlTextAreaElement>()
478            .expect("convert to html input element")
479            .set_onkeyup(Some(handler.as_ref().unchecked_ref()));
480          handler.forget();
481        }
482        _ => {
483          util::warn_log!("not handled keyup event for element: {}", element.tag_name());
484        }
485      }
486    }
487    "keypress" => {
488      let handler = Closure::wrap(Box::new(move |e: KeyboardEvent| {
489        let wrap_event = RespoEvent::Keyboard {
490          key: e.key(),
491          key_code: e.key_code(),
492          shift_key: e.shift_key(),
493          ctrl_key: e.ctrl_key(),
494          alt_key: e.alt_key(),
495          meta_key: e.meta_key(),
496          repeat: e.repeat(),
497          original_event: e,
498        };
499        handle_event
500          .run(RespoEventMark::new("keypress", &coord, wrap_event))
501          .expect("handle keypress event");
502      }) as Box<dyn FnMut(KeyboardEvent)>);
503      match element.tag_name().as_str() {
504        "INPUT" => {
505          element
506            .dyn_ref::<HtmlInputElement>()
507            .expect("convert to html input element")
508            .set_onkeypress(Some(handler.as_ref().unchecked_ref()));
509          handler.forget();
510        }
511        "TEXTAREA" => {
512          element
513            .dyn_ref::<HtmlTextAreaElement>()
514            .expect("convert to html input element")
515            .set_onkeypress(Some(handler.as_ref().unchecked_ref()));
516          handler.forget();
517        }
518        _ => {
519          util::warn_log!("not handled keypress event for element: {}", element.tag_name());
520        }
521      }
522    }
523    "focus" => {
524      let handler = Closure::wrap(Box::new(move |e: FocusEvent| {
525        handle_event
526          .run(RespoEventMark::new("focus", &coord, RespoEvent::Focus(e)))
527          .expect("handle focus event");
528      }) as Box<dyn FnMut(FocusEvent)>);
529      element
530        .dyn_ref::<HtmlInputElement>()
531        .expect("convert to html input element")
532        .set_onfocus(Some(handler.as_ref().unchecked_ref()));
533      handler.forget();
534    }
535    "blur" => {
536      let handler = Closure::wrap(Box::new(move |e: FocusEvent| {
537        handle_event
538          .run(RespoEventMark::new("blur", &coord, RespoEvent::Focus(e)))
539          .expect("handle blur event");
540      }) as Box<dyn FnMut(FocusEvent)>);
541      element
542        .dyn_ref::<HtmlInputElement>()
543        .expect("convert to html input element")
544        .set_onblur(Some(handler.as_ref().unchecked_ref()));
545      handler.forget();
546    }
547    _ => {
548      warn_1(&format!("unhandled event: {}", key).into());
549    }
550  }
551  Ok(())
552}