瀏覽代碼

Merge pull request #121 from DioxusLabs/jk/unify

feat: unify web and desktop interpreter with the same typescript codebase
Jonathan Kelley 3 年之前
父節點
當前提交
b287a4c

+ 4 - 2
packages/desktop/src/index.html

@@ -12,7 +12,9 @@
     <div id="main">
     </div>
 </body>
-<script type="text/javascript" src="index.js">
-</script>
+
+<script>var exports = {};</script>
+<script type="text/javascript" src="index.js" type="module"></script>
+<script>main()</script>
 
 </html>

+ 0 - 468
packages/desktop/src/index.js

@@ -1,468 +0,0 @@
-const bool_attrs = {
-  allowfullscreen: true,
-  allowpaymentrequest: true,
-  async: true,
-  autofocus: true,
-  autoplay: true,
-  checked: true,
-  controls: true,
-  default: true,
-  defer: true,
-  disabled: true,
-  formnovalidate: true,
-  hidden: true,
-  ismap: true,
-  itemscope: true,
-  loop: true,
-  multiple: true,
-  muted: true,
-  nomodule: true,
-  novalidate: true,
-  open: true,
-  playsinline: true,
-  readonly: true,
-  required: true,
-  reversed: true,
-  selected: true,
-  truespeed: true,
-};
-
-function serialize_event(event) {
-  switch (event.type) {
-    case "copy":
-    case "cut":
-    case "past":
-      return {};
-
-    case "compositionend":
-    case "compositionstart":
-    case "compositionupdate":
-      return {
-        data: event.data,
-      };
-
-    case "keydown":
-    case "keypress":
-    case "keyup":
-      return {
-        char_code: event.charCode,
-        key: event.key,
-        alt_key: event.altKey,
-        ctrl_key: event.ctrlKey,
-        meta_key: event.metaKey,
-        key_code: event.keyCode,
-        shift_key: event.shiftKey,
-        locale: "locale",
-        location: event.location,
-        repeat: event.repeat,
-        which: event.which,
-        // locale: event.locale,
-      };
-
-    case "focus":
-    case "blur":
-      return {};
-
-    case "change":
-      let target = event.target;
-      let value;
-      if (target.type === "checkbox" || target.type === "radio") {
-        value = target.checked ? "true" : "false";
-      } else {
-        value = target.value ?? target.textContent;
-      }
-
-      return {
-        value: value,
-      };
-
-    case "input":
-    case "invalid":
-    case "reset":
-    case "submit": {
-      let target = event.target;
-      let value = target.value ?? target.textContent;
-
-      if (target.type == "checkbox") {
-        value = target.checked ? "true" : "false";
-      }
-
-      return {
-        value: value,
-      };
-    }
-
-    case "click":
-    case "contextmenu":
-    case "doubleclick":
-    case "drag":
-    case "dragend":
-    case "dragenter":
-    case "dragexit":
-    case "dragleave":
-    case "dragover":
-    case "dragstart":
-    case "drop":
-    case "mousedown":
-    case "mouseenter":
-    case "mouseleave":
-    case "mousemove":
-    case "mouseout":
-    case "mouseover":
-    case "mouseup":
-      return {
-        alt_key: event.altKey,
-        button: event.button,
-        buttons: event.buttons,
-        client_x: event.clientX,
-        client_y: event.clientY,
-        ctrl_key: event.ctrlKey,
-        meta_key: event.metaKey,
-        page_x: event.pageX,
-        page_y: event.pageY,
-        screen_x: event.screenX,
-        screen_y: event.screenY,
-        shift_key: event.shiftKey,
-      };
-
-    case "pointerdown":
-    case "pointermove":
-    case "pointerup":
-    case "pointercancel":
-    case "gotpointercapture":
-    case "lostpointercapture":
-    case "pointerenter":
-    case "pointerleave":
-    case "pointerover":
-    case "pointerout":
-      return {
-        alt_key: event.altKey,
-        button: event.button,
-        buttons: event.buttons,
-        client_x: event.clientX,
-        client_y: event.clientY,
-        ctrl_key: event.ctrlKey,
-        meta_key: event.metaKey,
-        page_x: event.pageX,
-        page_y: event.pageY,
-        screen_x: event.screenX,
-        screen_y: event.screenY,
-        shift_key: event.shiftKey,
-        pointer_id: event.pointerId,
-        width: event.width,
-        height: event.height,
-        pressure: event.pressure,
-        tangential_pressure: event.tangentialPressure,
-        tilt_x: event.tiltX,
-        tilt_y: event.tiltY,
-        twist: event.twist,
-        pointer_type: event.pointerType,
-        is_primary: event.isPrimary,
-      };
-
-    case "select":
-      return {};
-
-    case "touchcancel":
-    case "touchend":
-    case "touchmove":
-    case "touchstart":
-      return {
-        alt_key: event.altKey,
-        ctrl_key: event.ctrlKey,
-        meta_key: event.metaKey,
-        shift_key: event.shiftKey,
-
-        // changed_touches: event.changedTouches,
-        // target_touches: event.targetTouches,
-        // touches: event.touches,
-      };
-
-    case "scroll":
-      return {};
-
-    case "wheel":
-      return {
-        delta_x: event.deltaX,
-        delta_y: event.deltaY,
-        delta_z: event.deltaZ,
-        delta_mode: event.deltaMode,
-      };
-
-    case "animationstart":
-    case "animationend":
-    case "animationiteration":
-      return {
-        animation_name: event.animationName,
-        elapsed_time: event.elapsedTime,
-        pseudo_element: event.pseudoElement,
-      };
-
-    case "transitionend":
-      return {
-        property_name: event.propertyName,
-        elapsed_time: event.elapsedTime,
-        pseudo_element: event.pseudoElement,
-      };
-
-    case "abort":
-    case "canplay":
-    case "canplaythrough":
-    case "durationchange":
-    case "emptied":
-    case "encrypted":
-    case "ended":
-    case "error":
-    case "loadeddata":
-    case "loadedmetadata":
-    case "loadstart":
-    case "pause":
-    case "play":
-    case "playing":
-    case "progress":
-    case "ratechange":
-    case "seeked":
-    case "seeking":
-    case "stalled":
-    case "suspend":
-    case "timeupdate":
-    case "volumechange":
-    case "waiting":
-      return {};
-
-    case "toggle":
-      return {};
-
-    default:
-      return {};
-  }
-}
-
-class Interpreter {
-  constructor(root) {
-    this.root = root;
-    this.stack = [root];
-    this.listeners = {
-      onclick: {},
-    };
-    this.lastNodeWasText = false;
-    this.nodes = [root];
-  }
-
-  top() {
-    return this.stack[this.stack.length - 1];
-  }
-
-  pop() {
-    return this.stack.pop();
-  }
-
-  PushRoot(edit) {
-    const id = edit.root;
-    const node = this.nodes[id];
-    this.stack.push(node);
-  }
-
-  AppendChildren(edit) {
-    let root = this.stack[this.stack.length - (1 + edit.many)];
-
-    let to_add = this.stack.splice(this.stack.length - edit.many);
-
-    for (let i = 0; i < edit.many; i++) {
-      root.appendChild(to_add[i]);
-    }
-  }
-
-  ReplaceWith(edit) {
-    let root = this.nodes[edit.root];
-    let els = this.stack.splice(this.stack.length - edit.m);
-
-    root.replaceWith(...els);
-  }
-
-  InsertAfter(edit) {
-    let old = this.nodes[edit.root];
-    let new_nodes = this.stack.splice(this.stack.length - edit.n);
-    old.after(...new_nodes);
-  }
-
-  InsertBefore(edit) {
-    let old = this.nodes[edit.root];
-    let new_nodes = this.stack.splice(this.stack.length - edit.n);
-    old.before(...new_nodes);
-  }
-
-  Remove(edit) {
-    let node = this.nodes[edit.root];
-    if (node !== undefined) {
-      node.remove();
-    }
-  }
-
-  CreateTextNode(edit) {
-    const node = document.createTextNode(edit.text);
-    this.nodes[edit.root] = node;
-    this.stack.push(node);
-  }
-
-  CreateElement(edit) {
-    const tagName = edit.tag;
-    const el = document.createElement(tagName);
-    this.nodes[edit.root] = el;
-    el.setAttribute("dioxus-id", edit.root);
-    this.stack.push(el);
-  }
-
-  CreateElementNs(edit) {
-    let el = document.createElementNS(edit.ns, edit.tag);
-    this.stack.push(el);
-    this.nodes[edit.root] = el;
-  }
-
-  CreatePlaceholder(edit) {
-    let el = document.createElement("pre");
-    el.hidden = true;
-    this.stack.push(el);
-    this.nodes[edit.root] = el;
-  }
-
-  RemoveEventListener(edit) {}
-
-  NewEventListener(edit) {
-    const event_name = edit.event_name;
-    const mounted_node_id = edit.root;
-    const scope = edit.scope;
-
-    const element = this.nodes[edit.root];
-    element.setAttribute(
-      `dioxus-event-${event_name}`,
-      `${scope}.${mounted_node_id}`
-    );
-
-    if (this.listeners[event_name] === undefined) {
-      this.listeners[event_name] = true;
-
-      this.root.addEventListener(event_name, (event) => {
-
-        const target = event.target;
-        const real_id = target.getAttribute(`dioxus-id`);
-
-        const should_prevent_default = target.getAttribute(
-          `dioxus-prevent-default`
-        );
-
-        let contents = serialize_event(event);
-
-        if (should_prevent_default === `on${event.type}`) {
-          event.preventDefault();
-        }
-
-        if (event.type == "submit") {
-          event.preventDefault();
-        }
-        
-        if (event.type == "click") {
-          event.preventDefault();
-          if (should_prevent_default !== `onclick`) {
-            if(element.tagName == "A") {
-              const href = event.target.getAttribute("href")
-              if (href !== "" && href !== null && href !== undefined) {
-                rpc.call("browser_open", {
-                  mounted_dom_id: parseInt(real_id),
-                  href: event.target.getAttribute("href")
-                });
-              }
-            }
-          }
-        }
-
-        if (real_id == null) {
-          return;
-        }
-
-        rpc.call("user_event", {
-          event: event_name,
-          mounted_dom_id: parseInt(real_id),
-          contents: contents,
-        });
-      });
-    }
-  }
-
-  SetText(edit) {
-    this.nodes[edit.root].textContent = edit.text;
-  }
-
-  SetAttribute(edit) {
-
-    // console.log("setting attr", edit);
-    
-    const name = edit.field;
-    const value = edit.value;
-    const ns = edit.ns;
-    const node = this.nodes[edit.root];
-
-    if (ns == "style") {
-      node.style[name] = value;
-    } else if (ns != null || ns != undefined) {
-      node.setAttributeNS(ns, name, value);
-    } else {
-      switch (name) {
-        case "value":
-          if (value != node.value) {
-            node.value = value;
-          }
-          break;
-        case "checked":
-          node.checked = value === "true";
-          break;
-        case "selected":
-          node.selected = value === "true";
-          break;
-        case "dangerous_inner_html":
-          node.innerHTML = value;
-          break;
-        default:
-          // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
-          if (value == "false" && bool_attrs.hasOwnProperty(name)) {
-            node.removeAttribute(name);
-          } else {
-            node.setAttribute(name, value);
-          }
-      }
-    }
-  }
-  RemoveAttribute(edit) {
-    const name = edit.field;
-    const node = this.nodes[edit.root];
-    node.removeAttribute(name);
-
-    if (name === "value") {
-      node.value = null;
-    }
-    if (name === "checked") {
-      node.checked = false;
-    }
-    if (name === "selected") {
-      node.selected = false;
-    }
-  }
-
-  handleEdits(edits) {
-    this.stack.push(this.root);
-
-    for (let x = 0; x < edits.length; x++) {
-      let edit = edits[x];
-      let f = this[edit.type];
-      f.call(this, edit);
-    }
-  }
-}
-
-function main() {
-  let root = window.document.getElementById("main");
-  window.interpreter = new Interpreter(root);
-  rpc.call("initialize");
-}
-
-main();

+ 1 - 1
packages/desktop/src/lib.rs

@@ -217,7 +217,7 @@ pub fn launch_with_props<P: 'static + Send>(
                         } else if path.trim_end_matches('/') == "index.html/index.js" {
                             wry::http::ResponseBuilder::new()
                                 .mimetype("text/javascript")
-                                .body(include_bytes!("./index.js").to_vec())
+                                .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec())
                         } else {
                             wry::http::ResponseBuilder::new()
                                 .status(wry::http::status::StatusCode::NOT_FOUND)

+ 8 - 0
packages/jsinterpreter/README.md

@@ -0,0 +1,8 @@
+# JS Interpreter
+
+After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support.
+
+In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets.
+
+
+For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks.

+ 477 - 0
packages/jsinterpreter/interpreter.js

@@ -0,0 +1,477 @@
+function serialize_event(event) {
+    var _a, _b;
+    switch (event.type) {
+        case "copy":
+        case "cut":
+        case "past": {
+            return {};
+        }
+        case "compositionend":
+        case "compositionstart":
+        case "compositionupdate": {
+            var data = event.data;
+            return {
+                data: data,
+            };
+        }
+        case "keydown":
+        case "keypress":
+        case "keyup": {
+            var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which;
+            return {
+                char_code: charCode,
+                key: key,
+                alt_key: altKey,
+                ctrl_key: ctrlKey,
+                meta_key: metaKey,
+                key_code: keyCode,
+                shift_key: shiftKey,
+                location: location_1,
+                repeat: repeat,
+                which: which,
+                locale: "locale",
+            };
+        }
+        case "focus":
+        case "blur": {
+            return {};
+        }
+        case "change": {
+            var target = event.target;
+            var value = void 0;
+            if (target.type === "checkbox" || target.type === "radio") {
+                value = target.checked ? "true" : "false";
+            }
+            else {
+                value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent;
+            }
+            return {
+                value: value,
+            };
+        }
+        case "input":
+        case "invalid":
+        case "reset":
+        case "submit": {
+            var target = event.target;
+            var value = (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent;
+            if (target.type == "checkbox") {
+                value = target.checked ? "true" : "false";
+            }
+            return {
+                value: value,
+            };
+        }
+        case "click":
+        case "contextmenu":
+        case "doubleclick":
+        case "drag":
+        case "dragend":
+        case "dragenter":
+        case "dragexit":
+        case "dragleave":
+        case "dragover":
+        case "dragstart":
+        case "drop":
+        case "mousedown":
+        case "mouseenter":
+        case "mouseleave":
+        case "mousemove":
+        case "mouseout":
+        case "mouseover":
+        case "mouseup": {
+            var _d = event, altKey = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey = _d.ctrlKey, metaKey = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey = _d.shiftKey;
+            return {
+                alt_key: altKey,
+                button: button,
+                buttons: buttons,
+                client_x: clientX,
+                client_y: clientY,
+                ctrl_key: ctrlKey,
+                meta_key: metaKey,
+                page_x: pageX,
+                page_y: pageY,
+                screen_x: screenX_1,
+                screen_y: screenY_1,
+                shift_key: shiftKey,
+            };
+        }
+        case "pointerdown":
+        case "pointermove":
+        case "pointerup":
+        case "pointercancel":
+        case "gotpointercapture":
+        case "lostpointercapture":
+        case "pointerenter":
+        case "pointerleave":
+        case "pointerover":
+        case "pointerout": {
+            var _e = event, altKey = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey = _e.ctrlKey, metaKey = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary;
+            return {
+                alt_key: altKey,
+                button: button,
+                buttons: buttons,
+                client_x: clientX,
+                client_y: clientY,
+                ctrl_key: ctrlKey,
+                meta_key: metaKey,
+                page_x: pageX,
+                page_y: pageY,
+                screen_x: screenX_2,
+                screen_y: screenY_2,
+                shift_key: shiftKey,
+                pointer_id: pointerId,
+                width: width,
+                height: height,
+                pressure: pressure,
+                tangential_pressure: tangentialPressure,
+                tilt_x: tiltX,
+                tilt_y: tiltY,
+                twist: twist,
+                pointer_type: pointerType,
+                is_primary: isPrimary,
+            };
+        }
+        case "select": {
+            return {};
+        }
+        case "touchcancel":
+        case "touchend":
+        case "touchmove":
+        case "touchstart": {
+            var _f = event, altKey = _f.altKey, ctrlKey = _f.ctrlKey, metaKey = _f.metaKey, shiftKey = _f.shiftKey;
+            return {
+                // changed_touches: event.changedTouches,
+                // target_touches: event.targetTouches,
+                // touches: event.touches,
+                alt_key: altKey,
+                ctrl_key: ctrlKey,
+                meta_key: metaKey,
+                shift_key: shiftKey,
+            };
+        }
+        case "scroll": {
+            return {};
+        }
+        case "wheel": {
+            var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode;
+            return {
+                delta_x: deltaX,
+                delta_y: deltaY,
+                delta_z: deltaZ,
+                delta_mode: deltaMode,
+            };
+        }
+        case "animationstart":
+        case "animationend":
+        case "animationiteration": {
+            var _h = event, animationName = _h.animationName, elapsedTime = _h.elapsedTime, pseudoElement = _h.pseudoElement;
+            return {
+                animation_name: animationName,
+                elapsed_time: elapsedTime,
+                pseudo_element: pseudoElement,
+            };
+        }
+        case "transitionend": {
+            var _j = event, propertyName = _j.propertyName, elapsedTime = _j.elapsedTime, pseudoElement = _j.pseudoElement;
+            return {
+                property_name: propertyName,
+                elapsed_time: elapsedTime,
+                pseudo_element: pseudoElement,
+            };
+        }
+        case "abort":
+        case "canplay":
+        case "canplaythrough":
+        case "durationchange":
+        case "emptied":
+        case "encrypted":
+        case "ended":
+        case "error":
+        case "loadeddata":
+        case "loadedmetadata":
+        case "loadstart":
+        case "pause":
+        case "play":
+        case "playing":
+        case "progress":
+        case "ratechange":
+        case "seeked":
+        case "seeking":
+        case "stalled":
+        case "suspend":
+        case "timeupdate":
+        case "volumechange":
+        case "waiting": {
+            return {};
+        }
+        case "toggle": {
+            return {};
+        }
+        default: {
+            return {};
+        }
+    }
+}
+var bool_attrs = {
+    allowfullscreen: true,
+    allowpaymentrequest: true,
+    async: true,
+    autofocus: true,
+    autoplay: true,
+    checked: true,
+    controls: true,
+    default: true,
+    defer: true,
+    disabled: true,
+    formnovalidate: true,
+    hidden: true,
+    ismap: true,
+    itemscope: true,
+    loop: true,
+    multiple: true,
+    muted: true,
+    nomodule: true,
+    novalidate: true,
+    open: true,
+    playsinline: true,
+    readonly: true,
+    required: true,
+    reversed: true,
+    selected: true,
+    truespeed: true,
+};
+var Interpreter = /** @class */ (function () {
+    function Interpreter(root) {
+        this.root = root;
+        this.stack = [root];
+        this.listeners = {};
+        this.handlers = {};
+        this.lastNodeWasText = false;
+        this.nodes = [root];
+    }
+    Interpreter.prototype.top = function () {
+        return this.stack[this.stack.length - 1];
+    };
+    Interpreter.prototype.pop = function () {
+        return this.stack.pop();
+    };
+    Interpreter.prototype.PushRoot = function (root) {
+        var node = this.nodes[root];
+        this.stack.push(node);
+    };
+    Interpreter.prototype.AppendChildren = function (many) {
+        var root = this.stack[this.stack.length - (1 + many)];
+        var to_add = this.stack.splice(this.stack.length - many);
+        for (var i = 0; i < many; i++) {
+            root.appendChild(to_add[i]);
+        }
+    };
+    Interpreter.prototype.ReplaceWith = function (root_id, m) {
+        var root = this.nodes[root_id];
+        var els = this.stack.splice(this.stack.length - m);
+        root.replaceWith.apply(root, els);
+    };
+    Interpreter.prototype.InsertAfter = function (root, n) {
+        var old = this.nodes[root];
+        var new_nodes = this.stack.splice(this.stack.length - n);
+        old.after.apply(old, new_nodes);
+    };
+    Interpreter.prototype.InsertBefore = function (root, n) {
+        var old = this.nodes[root];
+        var new_nodes = this.stack.splice(this.stack.length - n);
+        old.before.apply(old, new_nodes);
+    };
+    Interpreter.prototype.Remove = function (root) {
+        var node = this.nodes[root];
+        if (node !== undefined) {
+            node.remove();
+        }
+    };
+    Interpreter.prototype.CreateTextNode = function (text, root) {
+        // todo: make it so the types are okay
+        var node = document.createTextNode(text);
+        this.nodes[root] = node;
+        this.stack.push(node);
+    };
+    Interpreter.prototype.CreateElement = function (tag, root) {
+        var el = document.createElement(tag);
+        el.setAttribute("dioxus-id", "".concat(root));
+        this.nodes[root] = el;
+        this.stack.push(el);
+    };
+    Interpreter.prototype.CreateElementNs = function (tag, root, ns) {
+        var el = document.createElementNS(ns, tag);
+        this.stack.push(el);
+        this.nodes[root] = el;
+    };
+    Interpreter.prototype.CreatePlaceholder = function (root) {
+        var el = document.createElement("pre");
+        el.hidden = true;
+        this.stack.push(el);
+        this.nodes[root] = el;
+    };
+    Interpreter.prototype.NewEventListener = function (event_name, scope, root, handler) {
+        // console.log('new event listener', event_name, root, scope);
+        var element = this.nodes[root];
+        element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(root));
+        if (this.listeners[event_name] === undefined) {
+            this.listeners[event_name] = 0;
+            this.handlers[event_name] = handler;
+            this.root.addEventListener(event_name, handler);
+        }
+        else {
+            this.listeners[event_name]++;
+        }
+    };
+    Interpreter.prototype.RemoveEventListener = function (root, event_name) {
+        var element = this.nodes[root];
+        element.removeAttribute("dioxus-event-".concat(event_name));
+        this.listeners[event_name]--;
+        if (this.listeners[event_name] === 0) {
+            this.root.removeEventListener(event_name, this.handlers[event_name]);
+            delete this.listeners[event_name];
+            delete this.handlers[event_name];
+        }
+    };
+    Interpreter.prototype.SetText = function (root, text) {
+        this.nodes[root].textContent = text;
+    };
+    Interpreter.prototype.SetAttribute = function (root, field, value, ns) {
+        var name = field;
+        var node = this.nodes[root];
+        if (ns == "style") {
+            // @ts-ignore
+            node.style[name] = value;
+        }
+        else if (ns != null || ns != undefined) {
+            node.setAttributeNS(ns, name, value);
+        }
+        else {
+            switch (name) {
+                case "value":
+                    if (value != node.value) {
+                        node.value = value;
+                    }
+                    break;
+                case "checked":
+                    node.checked = value === "true";
+                    break;
+                case "selected":
+                    node.selected = value === "true";
+                    break;
+                case "dangerous_inner_html":
+                    node.innerHTML = value;
+                    break;
+                default:
+                    // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+                    if (value == "false" && bool_attrs.hasOwnProperty(name)) {
+                        node.removeAttribute(name);
+                    }
+                    else {
+                        node.setAttribute(name, value);
+                    }
+            }
+        }
+    };
+    Interpreter.prototype.RemoveAttribute = function (root, name) {
+        var node = this.nodes[root];
+        node.removeAttribute(name);
+        if (name === "value") {
+            node.value = "";
+        }
+        if (name === "checked") {
+            node.checked = false;
+        }
+        if (name === "selected") {
+            node.selected = false;
+        }
+    };
+    Interpreter.prototype.handleEdits = function (edits) {
+        this.stack.push(this.root);
+        for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) {
+            var edit = edits_1[_i];
+            this.handleEdit(edit);
+        }
+    };
+    Interpreter.prototype.handleEdit = function (edit) {
+        switch (edit.type) {
+            case "PushRoot":
+                this.PushRoot(edit.root);
+                break;
+            case "AppendChildren":
+                this.AppendChildren(edit.many);
+                break;
+            case "ReplaceWith":
+                this.ReplaceWith(edit.root, edit.m);
+                break;
+            case "InsertAfter":
+                this.InsertAfter(edit.root, edit.n);
+                break;
+            case "InsertBefore":
+                this.InsertBefore(edit.root, edit.n);
+                break;
+            case "Remove":
+                this.Remove(edit.root);
+                break;
+            case "CreateTextNode":
+                this.CreateTextNode(edit.text, edit.root);
+                break;
+            case "CreateElement":
+                this.CreateElement(edit.tag, edit.root);
+                break;
+            case "CreateElementNs":
+                this.CreateElementNs(edit.tag, edit.root, edit.ns);
+                break;
+            case "CreatePlaceholder":
+                this.CreatePlaceholder(edit.root);
+                break;
+            case "RemoveEventListener":
+                this.RemoveEventListener(edit.root, edit.event_name);
+                break;
+            case "NewEventListener":
+                // todo: only on desktop should we make our own handler
+                var handler = function (event) {
+                    var target = event.target;
+                    // console.log("event", event);
+                    if (target != null) {
+                        var real_id = target.getAttribute("dioxus-id");
+                        var should_prevent_default = target.getAttribute("dioxus-prevent-default");
+                        var contents = serialize_event(event);
+                        if (should_prevent_default === "on".concat(event.type)) {
+                            event.preventDefault();
+                        }
+                        if (real_id == null) {
+                            return;
+                        }
+                        window.rpc.call("user_event", {
+                            event: edit.event_name,
+                            mounted_dom_id: parseInt(real_id),
+                            contents: contents,
+                        });
+                    }
+                };
+                this.NewEventListener(edit.event_name, edit.scope, edit.root, handler);
+                break;
+            case "SetText":
+                this.SetText(edit.root, edit.text);
+                break;
+            case "SetAttribute":
+                this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
+                break;
+            case "RemoveAttribute":
+                this.RemoveAttribute(edit.root, edit.name);
+                break;
+        }
+    };
+    return Interpreter;
+}());
+export { Interpreter };
+function main() {
+    var root = window.document.getElementById("main");
+    if (root != null) {
+        window.interpreter = new Interpreter(root);
+        window.rpc.call("initialize");
+    }
+}
+//# sourceMappingURL=interpreter.js.map

+ 676 - 0
packages/jsinterpreter/interpreter.ts

@@ -0,0 +1,676 @@
+function serialize_event(event: Event) {
+  switch (event.type) {
+    case "copy":
+    case "cut":
+    case "past": {
+      return {};
+    }
+
+    case "compositionend":
+    case "compositionstart":
+    case "compositionupdate": {
+      let { data } = (event as CompositionEvent);
+      return {
+        data,
+      };
+    }
+
+    case "keydown":
+    case "keypress":
+    case "keyup": {
+      let {
+        charCode,
+        key,
+        altKey,
+        ctrlKey,
+        metaKey,
+        keyCode,
+        shiftKey,
+        location,
+        repeat,
+        which,
+      } = (event as KeyboardEvent);
+
+      return {
+        char_code: charCode,
+        key: key,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        key_code: keyCode,
+        shift_key: shiftKey,
+        location: location,
+        repeat: repeat,
+        which: which,
+        locale: "locale",
+      };
+    }
+
+    case "focus":
+    case "blur": {
+      return {};
+    }
+
+    case "change": {
+      let target = event.target as HTMLInputElement;
+      let value;
+      if (target.type === "checkbox" || target.type === "radio") {
+        value = target.checked ? "true" : "false";
+      } else {
+        value = target.value ?? target.textContent;
+      }
+
+      return {
+        value: value,
+      };
+    }
+
+    case "input":
+    case "invalid":
+    case "reset":
+    case "submit": {
+      let target = event.target as HTMLFormElement;
+      let value = target.value ?? target.textContent;
+
+      if (target.type == "checkbox") {
+        value = target.checked ? "true" : "false";
+      }
+
+      return {
+        value: value,
+      };
+    }
+
+    case "click":
+    case "contextmenu":
+    case "doubleclick":
+    case "drag":
+    case "dragend":
+    case "dragenter":
+    case "dragexit":
+    case "dragleave":
+    case "dragover":
+    case "dragstart":
+    case "drop":
+    case "mousedown":
+    case "mouseenter":
+    case "mouseleave":
+    case "mousemove":
+    case "mouseout":
+    case "mouseover":
+    case "mouseup": {
+      const {
+        altKey,
+        button,
+        buttons,
+        clientX,
+        clientY,
+        ctrlKey,
+        metaKey,
+        pageX,
+        pageY,
+        screenX,
+        screenY,
+        shiftKey,
+      } = event as MouseEvent;
+
+      return {
+        alt_key: altKey,
+        button: button,
+        buttons: buttons,
+        client_x: clientX,
+        client_y: clientY,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        page_x: pageX,
+        page_y: pageY,
+        screen_x: screenX,
+        screen_y: screenY,
+        shift_key: shiftKey,
+      };
+    }
+
+    case "pointerdown":
+    case "pointermove":
+    case "pointerup":
+    case "pointercancel":
+    case "gotpointercapture":
+    case "lostpointercapture":
+    case "pointerenter":
+    case "pointerleave":
+    case "pointerover":
+    case "pointerout": {
+      const {
+        altKey,
+        button,
+        buttons,
+        clientX,
+        clientY,
+        ctrlKey,
+        metaKey,
+        pageX,
+        pageY,
+        screenX,
+        screenY,
+        shiftKey,
+        pointerId,
+        width,
+        height,
+        pressure,
+        tangentialPressure,
+        tiltX,
+        tiltY,
+        twist,
+        pointerType,
+        isPrimary,
+      } = event as PointerEvent;
+      return {
+        alt_key: altKey,
+        button: button,
+        buttons: buttons,
+        client_x: clientX,
+        client_y: clientY,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        page_x: pageX,
+        page_y: pageY,
+        screen_x: screenX,
+        screen_y: screenY,
+        shift_key: shiftKey,
+        pointer_id: pointerId,
+        width: width,
+        height: height,
+        pressure: pressure,
+        tangential_pressure: tangentialPressure,
+        tilt_x: tiltX,
+        tilt_y: tiltY,
+        twist: twist,
+        pointer_type: pointerType,
+        is_primary: isPrimary,
+      };
+    }
+
+    case "select": {
+      return {};
+    }
+
+    case "touchcancel":
+    case "touchend":
+    case "touchmove":
+    case "touchstart": {
+      const {
+        altKey,
+        ctrlKey,
+        metaKey,
+        shiftKey,
+      } = event as TouchEvent;
+      return {
+        // changed_touches: event.changedTouches,
+        // target_touches: event.targetTouches,
+        // touches: event.touches,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        shift_key: shiftKey,
+      };
+    }
+
+    case "scroll": {
+      return {};
+    }
+
+    case "wheel": {
+      const {
+        deltaX,
+        deltaY,
+        deltaZ,
+        deltaMode,
+      } = event as WheelEvent;
+      return {
+        delta_x: deltaX,
+        delta_y: deltaY,
+        delta_z: deltaZ,
+        delta_mode: deltaMode,
+      };
+    }
+
+    case "animationstart":
+    case "animationend":
+    case "animationiteration": {
+      const {
+        animationName,
+        elapsedTime,
+        pseudoElement,
+      } = event as AnimationEvent;
+      return {
+        animation_name: animationName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+
+    case "transitionend": {
+      const {
+        propertyName,
+        elapsedTime,
+        pseudoElement,
+      } = event as TransitionEvent;
+      return {
+        property_name: propertyName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+
+    case "abort":
+    case "canplay":
+    case "canplaythrough":
+    case "durationchange":
+    case "emptied":
+    case "encrypted":
+    case "ended":
+    case "error":
+    case "loadeddata":
+    case "loadedmetadata":
+    case "loadstart":
+    case "pause":
+    case "play":
+    case "playing":
+    case "progress":
+    case "ratechange":
+    case "seeked":
+    case "seeking":
+    case "stalled":
+    case "suspend":
+    case "timeupdate":
+    case "volumechange":
+    case "waiting": {
+      return {};
+    }
+
+    case "toggle": {
+      return {};
+    }
+
+    default: {
+      return {};
+    }
+  }
+}
+
+const bool_attrs = {
+  allowfullscreen: true,
+  allowpaymentrequest: true,
+  async: true,
+  autofocus: true,
+  autoplay: true,
+  checked: true,
+  controls: true,
+  default: true,
+  defer: true,
+  disabled: true,
+  formnovalidate: true,
+  hidden: true,
+  ismap: true,
+  itemscope: true,
+  loop: true,
+  multiple: true,
+  muted: true,
+  nomodule: true,
+  novalidate: true,
+  open: true,
+  playsinline: true,
+  readonly: true,
+  required: true,
+  reversed: true,
+  selected: true,
+  truespeed: true,
+};
+
+export class Interpreter {
+  root: Element;
+  stack: Element[];
+  listeners: { [key: string]: number };
+  handlers: { [key: string]: (evt: Event) => void };
+  lastNodeWasText: boolean;
+  nodes: Element[];
+
+
+  constructor(root: Element) {
+    this.root = root;
+    this.stack = [root];
+    this.listeners = {};
+    this.handlers = {};
+    this.lastNodeWasText = false;
+    this.nodes = [root];
+  }
+
+  top() {
+    return this.stack[this.stack.length - 1];
+  }
+
+  pop() {
+    return this.stack.pop();
+  }
+
+  PushRoot(root: number) {
+    const node = this.nodes[root];
+    this.stack.push(node);
+  }
+
+  AppendChildren(many: number) {
+    let root = this.stack[this.stack.length - (1 + many)];
+
+    let to_add = this.stack.splice(this.stack.length - many);
+
+    for (let i = 0; i < many; i++) {
+      root.appendChild(to_add[i]);
+    }
+  }
+
+  ReplaceWith(root_id: number, m: number) {
+    let root = this.nodes[root_id] as Element;
+    let els = this.stack.splice(this.stack.length - m);
+
+    root.replaceWith(...els);
+  }
+
+  InsertAfter(root: number, n: number) {
+    let old = this.nodes[root] as Element;
+    let new_nodes = this.stack.splice(this.stack.length - n);
+    old.after(...new_nodes);
+  }
+
+  InsertBefore(root: number, n: number) {
+    let old = this.nodes[root] as Element;
+    let new_nodes = this.stack.splice(this.stack.length - n);
+    old.before(...new_nodes);
+  }
+
+  Remove(root: number) {
+    let node = this.nodes[root] as Element;
+    if (node !== undefined) {
+      node.remove();
+    }
+  }
+
+  CreateTextNode(text: string, root: number) {
+    // todo: make it so the types are okay
+    const node = document.createTextNode(text) as any as Element;
+    this.nodes[root] = node;
+    this.stack.push(node);
+  }
+
+  CreateElement(tag: string, root: number) {
+    const el = document.createElement(tag);
+    el.setAttribute("dioxus-id", `${root}`);
+
+    this.nodes[root] = el;
+    this.stack.push(el);
+  }
+
+  CreateElementNs(tag: string, root: number, ns: string) {
+    let el = document.createElementNS(ns, tag);
+    this.stack.push(el);
+    this.nodes[root] = el;
+  }
+
+  CreatePlaceholder(root: number) {
+    let el = document.createElement("pre");
+    el.hidden = true;
+    this.stack.push(el);
+    this.nodes[root] = el;
+  }
+
+  NewEventListener(event_name: string, scope: number, root: number, handler: (evt: Event) => void) {
+    // console.log('new event listener', event_name, root, scope);
+    const element = this.nodes[root];
+    element.setAttribute(
+      `dioxus-event-${event_name}`,
+      `${scope}.${root}`
+    );
+
+    if (this.listeners[event_name] === undefined) {
+      this.listeners[event_name] = 0;
+      this.handlers[event_name] = handler;
+      this.root.addEventListener(event_name, handler);
+    } else {
+      this.listeners[event_name]++;
+    }
+  }
+
+  RemoveEventListener(root: number, event_name: string) {
+    const element = this.nodes[root];
+    element.removeAttribute(
+      `dioxus-event-${event_name}`
+    );
+
+    this.listeners[event_name]--;
+
+    if (this.listeners[event_name] === 0) {
+      this.root.removeEventListener(event_name, this.handlers[event_name]);
+      delete this.listeners[event_name];
+      delete this.handlers[event_name];
+    }
+  }
+
+
+  SetText(root: number, text: string) {
+    this.nodes[root].textContent = text;
+  }
+
+  SetAttribute(root: number, field: string, value: string, ns: string | undefined) {
+    const name = field;
+    const node = this.nodes[root];
+
+    if (ns == "style") {
+
+      // @ts-ignore
+      (node as HTMLElement).style[name] = value;
+
+    } else if (ns != null || ns != undefined) {
+      node.setAttributeNS(ns, name, value);
+    } else {
+      switch (name) {
+        case "value":
+          if (value != (node as HTMLInputElement).value) {
+            (node as HTMLInputElement).value = value;
+          }
+          break;
+        case "checked":
+          (node as HTMLInputElement).checked = value === "true";
+          break;
+        case "selected":
+          (node as HTMLOptionElement).selected = value === "true";
+          break;
+        case "dangerous_inner_html":
+          node.innerHTML = value;
+          break;
+        default:
+          // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+          if (value == "false" && bool_attrs.hasOwnProperty(name)) {
+            node.removeAttribute(name);
+          } else {
+            node.setAttribute(name, value);
+          }
+      }
+    }
+  }
+  RemoveAttribute(root: number, name: string) {
+
+    const node = this.nodes[root];
+    node.removeAttribute(name);
+
+    if (name === "value") {
+      (node as HTMLInputElement).value = "";
+    }
+
+    if (name === "checked") {
+      (node as HTMLInputElement).checked = false;
+    }
+
+    if (name === "selected") {
+      (node as HTMLOptionElement).selected = false;
+    }
+  }
+
+  handleEdits(edits: DomEdit[]) {
+    this.stack.push(this.root);
+
+    for (let edit of edits) {
+      this.handleEdit(edit);
+    }
+  }
+
+  handleEdit(edit: DomEdit) {
+    switch (edit.type) {
+      case "PushRoot":
+        this.PushRoot(edit.root);
+        break;
+      case "AppendChildren":
+        this.AppendChildren(edit.many);
+        break;
+      case "ReplaceWith":
+        this.ReplaceWith(edit.root, edit.m);
+        break;
+      case "InsertAfter":
+        this.InsertAfter(edit.root, edit.n);
+        break;
+      case "InsertBefore":
+        this.InsertBefore(edit.root, edit.n);
+        break;
+      case "Remove":
+        this.Remove(edit.root);
+        break;
+      case "CreateTextNode":
+        this.CreateTextNode(edit.text, edit.root);
+        break;
+      case "CreateElement":
+        this.CreateElement(edit.tag, edit.root);
+        break;
+      case "CreateElementNs":
+        this.CreateElementNs(edit.tag, edit.root, edit.ns);
+        break;
+      case "CreatePlaceholder":
+        this.CreatePlaceholder(edit.root);
+        break;
+      case "RemoveEventListener":
+        this.RemoveEventListener(edit.root, edit.event_name);
+        break;
+      case "NewEventListener":
+
+
+        // this handler is only provided on desktop implementations since this 
+        // method is not used by the web implementation
+        let handler = (event: Event) => {
+          const target = event.target as Element | null;
+
+          if (target != null) {
+            const realId = target.getAttribute(`dioxus-id`);
+
+            const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`);
+
+            let contents = serialize_event(event);
+
+            if (shouldPreventDefault === `on${event.type}`) {
+              event.preventDefault();
+            }
+
+            if (event.type == "submit") {
+              event.preventDefault();
+            }
+
+            if (event.type == "click") {
+              event.preventDefault();
+              if (shouldPreventDefault !== `onclick`) {
+                if (target.tagName == "A") {
+                  const href = target.getAttribute("href")
+                  if (href !== "" && href !== null && href !== undefined && realId != null) {
+                    window.rpc.call("browser_open", {
+                      mounted_dom_id: parseInt(realId),
+                      href
+                    });
+                  }
+                }
+              }
+            }
+
+            if (realId == null) {
+              return;
+            }
+
+            window.rpc.call("user_event", {
+              event: (edit as NewEventListener).event_name,
+              mounted_dom_id: parseInt(realId),
+              contents: contents,
+            });
+          }
+        };
+        this.NewEventListener(edit.event_name, edit.scope, edit.root, handler);
+        break;
+      case "SetText":
+        this.SetText(edit.root, edit.text);
+        break;
+      case "SetAttribute":
+        this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
+        break;
+      case "RemoveAttribute":
+        this.RemoveAttribute(edit.root, edit.name);
+        break;
+    }
+  }
+}
+
+function main() {
+  let root = window.document.getElementById("main");
+  if (root != null) {
+    window.interpreter = new Interpreter(root);
+    window.rpc.call("initialize");
+  }
+}
+
+
+type PushRoot = { type: "PushRoot", root: number };
+type AppendChildren = { type: "AppendChildren", many: number };
+type ReplaceWith = { type: "ReplaceWith", root: number, m: number };
+type InsertAfter = { type: "InsertAfter", root: number, n: number };
+type InsertBefore = { type: "InsertBefore", root: number, n: number };
+type Remove = { type: "Remove", root: number };
+type CreateTextNode = { type: "CreateTextNode", text: string, root: number };
+type CreateElement = { type: "CreateElement", tag: string, root: number };
+type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string };
+type CreatePlaceholder = { type: "CreatePlaceholder", root: number };
+type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number };
+type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number };
+type SetText = { type: "SetText", root: number, text: string };
+type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined };
+type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string };
+
+
+type DomEdit =
+  PushRoot |
+  AppendChildren |
+  ReplaceWith |
+  InsertAfter |
+  InsertBefore |
+  Remove |
+  CreateTextNode |
+  CreateElement |
+  CreateElementNs |
+  CreatePlaceholder |
+  NewEventListener |
+  RemoveEventListener |
+  SetText |
+  SetAttribute |
+  RemoveAttribute;
+
+
+export { };
+declare global {
+  interface Window {
+    interpreter: Interpreter;
+    rpc: { call: (method: string, args?: any) => void };
+  }
+}
+
+

+ 13 - 0
packages/jsinterpreter/tsconfig.json

@@ -0,0 +1,13 @@
+{
+    "compilerOptions": {
+        "target": "ES5",
+        "module": "ESNext",
+        "lib": [
+            "es2015",
+            "es5",
+            "es6",
+            "dom"
+        ],
+        "strict": true
+    }
+}

+ 2 - 2
packages/web/Cargo.toml

@@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com"
 keywords = ["dom", "ui", "gui", "react", "wasm"]
 
 [dependencies]
-dioxus-core = { path = "../core", version ="^0.1.7"}
-dioxus-html = { path = "../html", version ="^0.1.4"}
+dioxus-core = { path = "../core", version = "^0.1.7" }
+dioxus-html = { path = "../html", version = "^0.1.4" }
 js-sys = "0.3"
 wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
 lazy_static = "1.4.0"

+ 59 - 0
packages/web/src/bindings.rs

@@ -0,0 +1,59 @@
+use js_sys::Function;
+use wasm_bindgen::prelude::*;
+use web_sys::{Element, Node};
+
+#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")]
+extern "C" {
+    pub type Interpreter;
+
+    #[wasm_bindgen(constructor)]
+    pub fn new(arg: Element) -> Interpreter;
+
+    #[wasm_bindgen(method)]
+    pub fn set_node(this: &Interpreter, id: usize, node: Node);
+
+    #[wasm_bindgen(method)]
+    pub fn PushRoot(this: &Interpreter, root: u64);
+
+    #[wasm_bindgen(method)]
+    pub fn AppendChildren(this: &Interpreter, many: u32);
+
+    #[wasm_bindgen(method)]
+    pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32);
+
+    #[wasm_bindgen(method)]
+    pub fn InsertAfter(this: &Interpreter, root: u64, n: u32);
+
+    #[wasm_bindgen(method)]
+    pub fn InsertBefore(this: &Interpreter, root: u64, n: u32);
+
+    #[wasm_bindgen(method)]
+    pub fn Remove(this: &Interpreter, root: u64);
+
+    #[wasm_bindgen(method)]
+    pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64);
+
+    #[wasm_bindgen(method)]
+    pub fn CreateElement(this: &Interpreter, tag: &str, root: u64);
+
+    #[wasm_bindgen(method)]
+    pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str);
+
+    #[wasm_bindgen(method)]
+    pub fn CreatePlaceholder(this: &Interpreter, root: u64);
+
+    #[wasm_bindgen(method)]
+    pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function);
+
+    #[wasm_bindgen(method)]
+    pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str);
+
+    #[wasm_bindgen(method)]
+    pub fn SetText(this: &Interpreter, root: u64, text: &str);
+
+    #[wasm_bindgen(method)]
+    pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>);
+
+    #[wasm_bindgen(method)]
+    pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str);
+}

+ 1 - 1
packages/web/src/cache.rs

@@ -1,4 +1,4 @@
-pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[
+pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[
     // Important tags to dioxus
     "dioxus-id",
     "dioxus",

+ 2 - 3
packages/web/src/cfg.rs

@@ -3,10 +3,9 @@
 /// This struct helps configure the specifics of hydration and render destination for WebSys.
 ///
 /// # Example
+///
 /// ```rust, ignore
-/// fn main() {
-///     dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot"))
-/// }
+/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot"))
 /// ```
 pub struct WebConfig {
     pub(crate) hydrate: bool,

+ 55 - 444
packages/web/src/dom.rs

@@ -7,56 +7,51 @@
 //! - tests to ensure dyn_into works for various event types.
 //! - Partial delegation?>
 
-use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
-use fxhash::FxHashMap;
-use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
+use crate::bindings::Interpreter;
+use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
+use js_sys::Function;
+use std::{any::Any, rc::Rc, sync::Arc};
 use wasm_bindgen::{closure::Closure, JsCast};
-use web_sys::{
-    CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
-    HtmlOptionElement, HtmlTextAreaElement, Node,
-};
+use web_sys::{Document, Element, Event, HtmlElement};
 
-use crate::{nodeslab::NodeSlab, WebConfig};
+use crate::WebConfig;
 
 pub struct WebsysDom {
-    stack: Stack,
-
-    /// A map from ElementID (index) to Node
-    pub(crate) nodes: NodeSlab,
-
-    document: Document,
+    pub interpreter: Interpreter,
 
     pub(crate) root: Element,
 
-    sender_callback: Rc<dyn Fn(SchedulerMsg)>,
-
-    // map of listener types to number of those listeners
-    // This is roughly a delegater
-    // TODO: check how infero delegates its events - some are more performant
-    listeners: FxHashMap<&'static str, ListenerEntry>,
+    pub handler: Closure<dyn FnMut(&Event)>,
 }
 
-type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
-
 impl WebsysDom {
     pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
-        let document = load_document();
-
-        let nodes = NodeSlab::new(2000);
-        let listeners = FxHashMap::default();
+        // eventually, we just want to let the interpreter do all the work of decoding events into our event type
+        let callback: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
+            if let Ok(synthetic_event) = decode_trigger(event) {
+                // Try to prevent default if the attribute is set
+                if let Some(target) = event.target() {
+                    if let Some(node) = target.dyn_ref::<HtmlElement>() {
+                        if let Some(name) = node.get_attribute("dioxus-prevent-default") {
+                            if name == synthetic_event.name
+                                || name.trim_start_matches("on") == synthetic_event.name
+                            {
+                                log::trace!("Preventing default");
+                                event.prevent_default();
+                            }
+                        }
+                    }
+                }
 
-        let mut stack = Stack::with_capacity(10);
+                sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event))
+            }
+        });
 
         let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
-        let root_node = root.clone().dyn_into::<Node>().unwrap();
-        stack.push(root_node);
 
         Self {
-            stack,
-            nodes,
-            listeners,
-            document,
-            sender_callback,
+            interpreter: Interpreter::new(root.clone()),
+            handler: Closure::wrap(callback),
             root,
         }
     }
@@ -64,434 +59,49 @@ impl WebsysDom {
     pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
         for edit in edits.drain(..) {
             match edit {
-                DomEdit::PushRoot { root } => self.push(root),
-                DomEdit::AppendChildren { many } => self.append_children(many),
-                DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
-                DomEdit::Remove { root } => self.remove(root),
-                DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id),
-                DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id),
-                DomEdit::CreateElementNs { tag, root: id, ns } => {
-                    self.create_element(tag, Some(ns), id)
+                DomEdit::PushRoot { root } => self.interpreter.PushRoot(root),
+                DomEdit::AppendChildren { many } => self.interpreter.AppendChildren(many),
+                DomEdit::ReplaceWith { root, m } => self.interpreter.ReplaceWith(root, m),
+                DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n),
+                DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n),
+                DomEdit::Remove { root } => self.interpreter.Remove(root),
+                DomEdit::CreateTextNode { text, root } => {
+                    self.interpreter.CreateTextNode(text, root)
                 }
-                DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id),
+                DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root),
+                DomEdit::CreateElementNs { tag, root, ns } => {
+                    self.interpreter.CreateElementNs(tag, root, ns)
+                }
+                DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root),
                 DomEdit::NewEventListener {
                     event_name, root, ..
-                } => self.new_event_listener(event_name, root),
-
-                DomEdit::RemoveEventListener { event, root } => {
-                    self.remove_event_listener(event, root)
+                } => {
+                    let handler: &Function = self.handler.as_ref().unchecked_ref();
+                    self.interpreter.NewEventListener(event_name, root, handler);
                 }
 
-                DomEdit::SetText { text, root } => self.set_text(text, root),
+                DomEdit::RemoveEventListener { root, event } => {
+                    self.interpreter.RemoveEventListener(root, event)
+                }
+                DomEdit::SetText { root, text } => self.interpreter.SetText(root, text),
                 DomEdit::SetAttribute {
+                    root,
                     field,
                     value,
                     ns,
-                    root,
-                } => self.set_attribute(field, value, ns, root),
-                DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
-
-                DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
-                DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
-            }
-        }
-    }
-    fn push(&mut self, root: u64) {
-        let key = root as usize;
-        let domnode = &self.nodes[key];
-
-        let real_node: Node = match domnode {
-            Some(n) => n.clone(),
-            None => todo!(),
-        };
-
-        self.stack.push(real_node);
-    }
-
-    fn append_children(&mut self, many: u32) {
-        let root: Node = self
-            .stack
-            .list
-            .get(self.stack.list.len() - (1 + many as usize))
-            .unwrap()
-            .clone();
-
-        // We need to make sure to add comments between text nodes
-        // We ensure that the text siblings are patched by preventing the browser from merging
-        // neighboring text nodes. Originally inspired by some of React's work from 2016.
-        //  -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
-        //  -> https://github.com/facebook/react/pull/5753
-        /*
-        todo: we need to track this for replacing/insert after/etc
-        */
-        let mut last_node_was_text = false;
-
-        for child in self
-            .stack
-            .list
-            .drain((self.stack.list.len() - many as usize)..)
-        {
-            if child.dyn_ref::<web_sys::Text>().is_some() {
-                if last_node_was_text {
-                    let comment_node = self
-                        .document
-                        .create_comment("dioxus")
-                        .dyn_into::<Node>()
-                        .unwrap();
-                    root.append_child(&comment_node).unwrap();
-                }
-                last_node_was_text = true;
-            } else {
-                last_node_was_text = false;
-            }
-            root.append_child(&child).unwrap();
-        }
-    }
-
-    fn replace_with(&mut self, m: u32, root: u64) {
-        let old = self.nodes[root as usize].as_ref().unwrap();
-
-        let arr: js_sys::Array = self
-            .stack
-            .list
-            .drain((self.stack.list.len() - m as usize)..)
-            .collect();
-
-        if let Some(el) = old.dyn_ref::<Element>() {
-            el.replace_with_with_node(&arr).unwrap();
-        } else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
-            el.replace_with_with_node(&arr).unwrap();
-        } else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
-            el.replace_with_with_node(&arr).unwrap();
-        }
-    }
-
-    fn remove(&mut self, root: u64) {
-        let node = self.nodes[root as usize].as_ref().unwrap();
-        if let Some(element) = node.dyn_ref::<Element>() {
-            element.remove();
-        } else if let Some(parent) = node.parent_node() {
-            parent.remove_child(node).unwrap();
-        }
-    }
-
-    fn create_placeholder(&mut self, id: u64) {
-        self.create_element("pre", None, id);
-        self.set_attribute("hidden", "", None, id);
-    }
-
-    fn create_text_node(&mut self, text: &str, id: u64) {
-        let textnode = self
-            .document
-            .create_text_node(text)
-            .dyn_into::<Node>()
-            .unwrap();
-
-        self.stack.push(textnode.clone());
-
-        self.nodes[(id as usize)] = Some(textnode);
-    }
-
-    fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
-        let tag = wasm_bindgen::intern(tag);
-
-        let el = match ns {
-            Some(ns) => self
-                .document
-                .create_element_ns(Some(ns), tag)
-                .unwrap()
-                .dyn_into::<Node>()
-                .unwrap(),
-            None => self
-                .document
-                .create_element(tag)
-                .unwrap()
-                .dyn_into::<Node>()
-                .unwrap(),
-        };
-
-        use smallstr::SmallString;
-        use std::fmt::Write;
-
-        let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
-        write!(s, "{}", id).unwrap();
-
-        let el2 = el.dyn_ref::<Element>().unwrap();
-        el2.set_attribute("dioxus-id", s.as_str()).unwrap();
-
-        self.stack.push(el.clone());
-        self.nodes[(id as usize)] = Some(el);
-    }
-
-    pub fn new_event_listener(&mut self, event: &'static str, node: u64) {
-        let event = wasm_bindgen::intern(event);
-
-        // attach the correct attributes to the element
-        // these will be used by accessing the event's target
-        // This ensures we only ever have one handler attached to the root, but decide
-        // dynamically when we want to call a listener.
-
-        let el = self.nodes[(node as usize)].as_ref().unwrap();
-
-        let el = el.dyn_ref::<Element>().unwrap();
-
-        el.set_attribute("dioxus-event", event).unwrap();
-
-        // Register the callback to decode
-
-        if let Some(entry) = self.listeners.get_mut(event) {
-            entry.0 += 1;
-        } else {
-            let trigger = self.sender_callback.clone();
-
-            let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
-                // "Result" cannot be received from JS
-                // Instead, we just build and immediately execute a closure that returns result
-                match decode_trigger(event) {
-                    Ok(synthetic_event) => {
-                        let target = event.target().unwrap();
-                        if let Some(node) = target.dyn_ref::<HtmlElement>() {
-                            if let Some(name) = node.get_attribute("dioxus-prevent-default") {
-                                if name == synthetic_event.name
-                                    || name.trim_start_matches("on") == synthetic_event.name
-                                {
-                                    log::trace!("Preventing default");
-                                    event.prevent_default();
-                                }
-                            }
-                        }
-
-                        trigger.as_ref()(SchedulerMsg::Event(synthetic_event))
-                    }
-                    Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
-                };
-            });
-
-            let handler = Closure::wrap(c);
-
-            self.root
-                .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
-                .unwrap();
-
-            // Increment the listeners
-            self.listeners.insert(event, (1, handler));
-        }
-    }
-
-    fn remove_event_listener(&mut self, _event: &str, _root: u64) {
-        todo!()
-    }
-
-    fn set_text(&mut self, text: &str, root: u64) {
-        let el = self.nodes[root as usize].as_ref().unwrap();
-        el.set_text_content(Some(text))
-    }
-
-    fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
-        let node = self.nodes[root as usize].as_ref().unwrap();
-        if ns == Some("style") {
-            if let Some(el) = node.dyn_ref::<Element>() {
-                let el = el.dyn_ref::<HtmlElement>().unwrap();
-                let style_dc: CssStyleDeclaration = el.style();
-                style_dc.set_property(name, value).unwrap();
-            }
-        } else {
-            let fallback = || {
-                let el = node.dyn_ref::<Element>().unwrap();
-                el.set_attribute(name, value).unwrap()
-            };
-            match name {
-                "dangerous_inner_html" => {
-                    if let Some(el) = node.dyn_ref::<Element>() {
-                        el.set_inner_html(value);
-                    }
-                }
-                "value" => {
-                    if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
-                        /*
-                        if the attribute being set is the same as the value of the input, then don't bother setting it.
-                        This is used in controlled components to keep the cursor in the right spot.
-
-                        this logic should be moved into the virtualdom since we have the notion of "volatile"
-                        */
-                        if input.value() != value {
-                            input.set_value(value);
-                        }
-                    } else if let Some(node) = node.dyn_ref::<HtmlTextAreaElement>() {
-                        if name == "value" {
-                            node.set_value(value);
-                        }
-                    } else {
-                        fallback();
-                    }
-                }
-                "checked" => {
-                    if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
-                        match value {
-                            "true" => input.set_checked(true),
-                            "false" => input.set_checked(false),
-                            _ => fallback(),
-                        }
-                    } else {
-                        fallback();
-                    }
-                }
-                "selected" => {
-                    if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
-                        node.set_selected(true);
-                    } else {
-                        fallback();
-                    }
-                }
-                _ => {
-                    // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
-                    if value == "false" {
-                        if let Some(el) = node.dyn_ref::<Element>() {
-                            match name {
-                                "allowfullscreen"
-                                | "allowpaymentrequest"
-                                | "async"
-                                | "autofocus"
-                                | "autoplay"
-                                | "checked"
-                                | "controls"
-                                | "default"
-                                | "defer"
-                                | "disabled"
-                                | "formnovalidate"
-                                | "hidden"
-                                | "ismap"
-                                | "itemscope"
-                                | "loop"
-                                | "multiple"
-                                | "muted"
-                                | "nomodule"
-                                | "novalidate"
-                                | "open"
-                                | "playsinline"
-                                | "readonly"
-                                | "required"
-                                | "reversed"
-                                | "selected"
-                                | "truespeed" => {
-                                    let _ = el.remove_attribute(name);
-                                }
-                                _ => {
-                                    let _ = el.set_attribute(name, value);
-                                }
-                            };
-                        }
-                    } else {
-                        fallback();
-                    }
+                } => self.interpreter.SetAttribute(root, field, value, ns),
+                DomEdit::RemoveAttribute { root, name } => {
+                    self.interpreter.RemoveAttribute(root, name)
                 }
             }
         }
     }
-
-    fn remove_attribute(&mut self, name: &str, root: u64) {
-        let node = self.nodes[root as usize].as_ref().unwrap();
-        if let Some(node) = node.dyn_ref::<web_sys::Element>() {
-            node.remove_attribute(name).unwrap();
-        }
-        if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
-            // Some attributes are "volatile" and don't work through `removeAttribute`.
-            if name == "value" {
-                node.set_value("");
-            }
-            if name == "checked" {
-                node.set_checked(false);
-            }
-        }
-
-        if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
-            if name == "selected" {
-                node.set_selected(true);
-            }
-        }
-    }
-
-    fn insert_after(&mut self, n: u32, root: u64) {
-        let old = self.nodes[root as usize].as_ref().unwrap();
-
-        let arr: js_sys::Array = self
-            .stack
-            .list
-            .drain((self.stack.list.len() - n as usize)..)
-            .collect();
-
-        if let Some(el) = old.dyn_ref::<Element>() {
-            el.after_with_node(&arr).unwrap();
-        } else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
-            el.after_with_node(&arr).unwrap();
-        } else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
-            el.after_with_node(&arr).unwrap();
-        }
-    }
-
-    fn insert_before(&mut self, n: u32, root: u64) {
-        let anchor = self.nodes[root as usize].as_ref().unwrap();
-
-        if n == 1 {
-            let before = self.stack.pop();
-
-            anchor
-                .parent_node()
-                .unwrap()
-                .insert_before(&before, Some(anchor))
-                .unwrap();
-        } else {
-            let arr: js_sys::Array = self
-                .stack
-                .list
-                .drain((self.stack.list.len() - n as usize)..)
-                .collect();
-
-            if let Some(el) = anchor.dyn_ref::<Element>() {
-                el.before_with_node(&arr).unwrap();
-            } else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
-                el.before_with_node(&arr).unwrap();
-            } else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
-                el.before_with_node(&arr).unwrap();
-            }
-        }
-    }
-}
-
-#[derive(Debug, Default)]
-struct Stack {
-    list: Vec<Node>,
-}
-
-impl Stack {
-    #[inline]
-    fn with_capacity(cap: usize) -> Self {
-        Stack {
-            list: Vec::with_capacity(cap),
-        }
-    }
-
-    #[inline]
-    fn push(&mut self, node: Node) {
-        self.list.push(node);
-    }
-
-    #[inline]
-    fn pop(&mut self) -> Node {
-        self.list.pop().unwrap()
-    }
-
-    fn top(&self) -> &Node {
-        match self.list.last() {
-            Some(a) => a,
-            None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
-        }
-    }
 }
 
 pub struct DioxusWebsysEvent(web_sys::Event);
 
 // safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
+#[allow(clippy::non_send_fields_in_send_ty)]
 unsafe impl Send for DioxusWebsysEvent {}
 unsafe impl Sync for DioxusWebsysEvent {}
 
@@ -628,6 +238,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send
                 shift_key: evt.shift_key(),
             })
         }
+
         "scroll" => Arc::new(()),
         "wheel" => {
             let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();

+ 1 - 1
packages/web/src/lib.rs

@@ -61,10 +61,10 @@ pub use dioxus_core as dioxus;
 use dioxus_core::prelude::Component;
 use futures_util::FutureExt;
 
+pub(crate) mod bindings;
 mod cache;
 mod cfg;
 mod dom;
-mod nodeslab;
 mod rehydrate;
 mod ric_raf;
 

+ 0 - 34
packages/web/src/nodeslab.rs

@@ -1,34 +0,0 @@
-//! This module provides a mirror of the VirtualDOM Element Slab using a Vector.
-
-use std::ops::{Index, IndexMut};
-use web_sys::Node;
-
-pub(crate) struct NodeSlab {
-    nodes: Vec<Option<Node>>,
-}
-
-impl NodeSlab {
-    pub fn new(capacity: usize) -> NodeSlab {
-        let nodes = Vec::with_capacity(capacity);
-        NodeSlab { nodes }
-    }
-}
-impl Index<usize> for NodeSlab {
-    type Output = Option<Node>;
-    fn index(&self, index: usize) -> &Self::Output {
-        &self.nodes[index]
-    }
-}
-
-impl IndexMut<usize> for NodeSlab {
-    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
-        if index >= self.nodes.capacity() * 3 {
-            panic!("Trying to mutate an element way too far out of bounds");
-        }
-
-        if index + 1 > self.nodes.len() {
-            self.nodes.resize_with(index + 1, || None);
-        }
-        &mut self.nodes[index]
-    }
-}

+ 857 - 0
packages/web/src/olddom.rs

@@ -0,0 +1,857 @@
+//! Implementation of a renderer for Dioxus on the web.
+//!
+//! Oustanding todos:
+//! - Removing event listeners (delegation)
+//! - Passive event listeners
+//! - no-op event listener patch for safari
+//! - tests to ensure dyn_into works for various event types.
+//! - Partial delegation?>
+
+use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
+use fxhash::FxHashMap;
+use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
+use wasm_bindgen::{closure::Closure, JsCast};
+use web_sys::{
+    CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
+    HtmlOptionElement, HtmlTextAreaElement, Node,
+};
+
+use crate::{nodeslab::NodeSlab, WebConfig};
+
+pub struct WebsysDom {
+    stack: Stack,
+
+    /// A map from ElementID (index) to Node
+    pub(crate) nodes: NodeSlab,
+
+    document: Document,
+
+    pub(crate) root: Element,
+
+    sender_callback: Rc<dyn Fn(SchedulerMsg)>,
+
+    // map of listener types to number of those listeners
+    // This is roughly a delegater
+    // TODO: check how infero delegates its events - some are more performant
+    listeners: FxHashMap<&'static str, ListenerEntry>,
+}
+
+type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
+
+impl WebsysDom {
+    pub fn new(cfg: WebConfig, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
+        let document = load_document();
+
+        let nodes = NodeSlab::new(2000);
+        let listeners = FxHashMap::default();
+
+        let mut stack = Stack::with_capacity(10);
+
+        let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
+        let root_node = root.clone().dyn_into::<Node>().unwrap();
+        stack.push(root_node);
+
+        Self {
+            stack,
+            nodes,
+            listeners,
+            document,
+            sender_callback,
+            root,
+        }
+    }
+
+    pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
+        for edit in edits.drain(..) {
+            match edit {
+                DomEdit::PushRoot { root } => self.push(root),
+                DomEdit::AppendChildren { many } => self.append_children(many),
+                DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
+                DomEdit::Remove { root } => self.remove(root),
+                DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id),
+                DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id),
+                DomEdit::CreateElementNs { tag, root: id, ns } => {
+                    self.create_element(tag, Some(ns), id)
+                }
+                DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id),
+                DomEdit::NewEventListener {
+                    event_name,
+                    scope,
+                    root: mounted_node_id,
+                } => self.new_event_listener(event_name, scope, mounted_node_id),
+
+                DomEdit::RemoveEventListener { event, root } => {
+                    self.remove_event_listener(event, root)
+                }
+
+                DomEdit::SetText { text, root } => self.set_text(text, root),
+                DomEdit::SetAttribute {
+                    field,
+                    value,
+                    ns,
+                    root,
+                } => self.set_attribute(field, value, ns, root),
+                DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
+
+                DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
+                DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
+            }
+        }
+    }
+    fn push(&mut self, root: u64) {
+        let key = root as usize;
+        let domnode = &self.nodes[key];
+
+        let real_node: Node = match domnode {
+            Some(n) => n.clone(),
+            None => todo!(),
+        };
+
+        self.stack.push(real_node);
+    }
+
+    fn append_children(&mut self, many: u32) {
+        let root: Node = self
+            .stack
+            .list
+            .get(self.stack.list.len() - (1 + many as usize))
+            .unwrap()
+            .clone();
+
+        // We need to make sure to add comments between text nodes
+        // We ensure that the text siblings are patched by preventing the browser from merging
+        // neighboring text nodes. Originally inspired by some of React's work from 2016.
+        //  -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
+        //  -> https://github.com/facebook/react/pull/5753
+        /*
+        todo: we need to track this for replacing/insert after/etc
+        */
+        let mut last_node_was_text = false;
+
+        for child in self
+            .stack
+            .list
+            .drain((self.stack.list.len() - many as usize)..)
+        {
+            if child.dyn_ref::<web_sys::Text>().is_some() {
+                if last_node_was_text {
+                    let comment_node = self
+                        .document
+                        .create_comment("dioxus")
+                        .dyn_into::<Node>()
+                        .unwrap();
+                    root.append_child(&comment_node).unwrap();
+                }
+                last_node_was_text = true;
+            } else {
+                last_node_was_text = false;
+            }
+            root.append_child(&child).unwrap();
+        }
+    }
+
+    fn replace_with(&mut self, m: u32, root: u64) {
+        let old = self.nodes[root as usize].as_ref().unwrap();
+
+        let arr: js_sys::Array = self
+            .stack
+            .list
+            .drain((self.stack.list.len() - m as usize)..)
+            .collect();
+
+        if let Some(el) = old.dyn_ref::<Element>() {
+            el.replace_with_with_node(&arr).unwrap();
+        } else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
+            el.replace_with_with_node(&arr).unwrap();
+        } else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
+            el.replace_with_with_node(&arr).unwrap();
+        }
+    }
+
+    fn remove(&mut self, root: u64) {
+        let node = self.nodes[root as usize].as_ref().unwrap();
+        if let Some(element) = node.dyn_ref::<Element>() {
+            element.remove();
+        } else {
+            if let Some(parent) = node.parent_node() {
+                parent.remove_child(&node).unwrap();
+            }
+        }
+    }
+
+    fn create_placeholder(&mut self, id: u64) {
+        self.create_element("pre", None, id);
+        self.set_attribute("hidden", "", None, id);
+    }
+
+    fn create_text_node(&mut self, text: &str, id: u64) {
+        let textnode = self
+            .document
+            .create_text_node(text)
+            .dyn_into::<Node>()
+            .unwrap();
+
+        self.stack.push(textnode.clone());
+
+        self.nodes[(id as usize)] = Some(textnode);
+    }
+
+    fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
+        let tag = wasm_bindgen::intern(tag);
+
+        let el = match ns {
+            Some(ns) => self
+                .document
+                .create_element_ns(Some(ns), tag)
+                .unwrap()
+                .dyn_into::<Node>()
+                .unwrap(),
+            None => self
+                .document
+                .create_element(tag)
+                .unwrap()
+                .dyn_into::<Node>()
+                .unwrap(),
+        };
+
+        use smallstr::SmallString;
+        use std::fmt::Write;
+
+        let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
+        write!(s, "{}", id).unwrap();
+
+        let el2 = el.dyn_ref::<Element>().unwrap();
+        el2.set_attribute("dioxus-id", s.as_str()).unwrap();
+
+        self.stack.push(el.clone());
+        self.nodes[(id as usize)] = Some(el);
+    }
+
+    fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) {
+        let event = wasm_bindgen::intern(event);
+
+        // attach the correct attributes to the element
+        // these will be used by accessing the event's target
+        // This ensures we only ever have one handler attached to the root, but decide
+        // dynamically when we want to call a listener.
+
+        let el = self.stack.top();
+
+        let el = el.dyn_ref::<Element>().unwrap();
+
+        el.set_attribute("dioxus-event", event).unwrap();
+
+        // Register the callback to decode
+
+        if let Some(entry) = self.listeners.get_mut(event) {
+            entry.0 += 1;
+        } else {
+            let trigger = self.sender_callback.clone();
+
+            let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
+                // "Result" cannot be received from JS
+                // Instead, we just build and immediately execute a closure that returns result
+                match decode_trigger(event) {
+                    Ok(synthetic_event) => {
+                        let target = event.target().unwrap();
+                        if let Some(node) = target.dyn_ref::<HtmlElement>() {
+                            if let Some(name) = node.get_attribute("dioxus-prevent-default") {
+                                if name == synthetic_event.name
+                                    || name.trim_start_matches("on") == synthetic_event.name
+                                {
+                                    log::trace!("Preventing default");
+                                    event.prevent_default();
+                                }
+                            }
+                        }
+
+                        trigger.as_ref()(SchedulerMsg::Event(synthetic_event))
+                    }
+                    Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
+                };
+            });
+
+            let handler = Closure::wrap(c);
+
+            self.root
+                .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
+                .unwrap();
+
+            // Increment the listeners
+            self.listeners.insert(event.into(), (1, handler));
+        }
+    }
+
+    fn remove_event_listener(&mut self, _event: &str, _root: u64) {
+        todo!()
+    }
+
+    fn set_text(&mut self, text: &str, root: u64) {
+        let el = self.nodes[root as usize].as_ref().unwrap();
+        el.set_text_content(Some(text))
+    }
+
+    fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
+        let node = self.nodes[root as usize].as_ref().unwrap();
+        if ns == Some("style") {
+            if let Some(el) = node.dyn_ref::<Element>() {
+                let el = el.dyn_ref::<HtmlElement>().unwrap();
+                let style_dc: CssStyleDeclaration = el.style();
+                style_dc.set_property(name, value).unwrap();
+            }
+        } else {
+            let fallback = || {
+                let el = node.dyn_ref::<Element>().unwrap();
+                el.set_attribute(name, value).unwrap()
+            };
+            match name {
+                "dangerous_inner_html" => {
+                    if let Some(el) = node.dyn_ref::<Element>() {
+                        el.set_inner_html(value);
+                    }
+                }
+                "value" => {
+                    if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
+                        /*
+                        if the attribute being set is the same as the value of the input, then don't bother setting it.
+                        This is used in controlled components to keep the cursor in the right spot.
+
+                        this logic should be moved into the virtualdom since we have the notion of "volatile"
+                        */
+                        if input.value() != value {
+                            input.set_value(value);
+                        }
+                    } else if let Some(node) = node.dyn_ref::<HtmlTextAreaElement>() {
+                        if name == "value" {
+                            node.set_value(value);
+                        }
+                    } else {
+                        fallback();
+                    }
+                }
+                "checked" => {
+                    if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
+                        match value {
+                            "true" => input.set_checked(true),
+                            "false" => input.set_checked(false),
+                            _ => fallback(),
+                        }
+                    } else {
+                        fallback();
+                    }
+                }
+                "selected" => {
+                    if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
+                        node.set_selected(true);
+                    } else {
+                        fallback();
+                    }
+                }
+                _ => {
+                    // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+                    if value == "false" {
+                        if let Some(el) = node.dyn_ref::<Element>() {
+                            match name {
+                                "allowfullscreen"
+                                | "allowpaymentrequest"
+                                | "async"
+                                | "autofocus"
+                                | "autoplay"
+                                | "checked"
+                                | "controls"
+                                | "default"
+                                | "defer"
+                                | "disabled"
+                                | "formnovalidate"
+                                | "hidden"
+                                | "ismap"
+                                | "itemscope"
+                                | "loop"
+                                | "multiple"
+                                | "muted"
+                                | "nomodule"
+                                | "novalidate"
+                                | "open"
+                                | "playsinline"
+                                | "readonly"
+                                | "required"
+                                | "reversed"
+                                | "selected"
+                                | "truespeed" => {
+                                    let _ = el.remove_attribute(name);
+                                }
+                                _ => {
+                                    let _ = el.set_attribute(name, value);
+                                }
+                            };
+                        }
+                    } else {
+                        fallback();
+                    }
+                }
+            }
+        }
+    }
+
+    fn remove_attribute(&mut self, name: &str, root: u64) {
+        let node = self.nodes[root as usize].as_ref().unwrap();
+        if let Some(node) = node.dyn_ref::<web_sys::Element>() {
+            node.remove_attribute(name).unwrap();
+        }
+        if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
+            // Some attributes are "volatile" and don't work through `removeAttribute`.
+            if name == "value" {
+                node.set_value("");
+            }
+            if name == "checked" {
+                node.set_checked(false);
+            }
+        }
+
+        if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
+            if name == "selected" {
+                node.set_selected(true);
+            }
+        }
+    }
+
+    fn insert_after(&mut self, n: u32, root: u64) {
+        let old = self.nodes[root as usize].as_ref().unwrap();
+
+        let arr: js_sys::Array = self
+            .stack
+            .list
+            .drain((self.stack.list.len() - n as usize)..)
+            .collect();
+
+        if let Some(el) = old.dyn_ref::<Element>() {
+            el.after_with_node(&arr).unwrap();
+        } else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
+            el.after_with_node(&arr).unwrap();
+        } else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
+            el.after_with_node(&arr).unwrap();
+        }
+    }
+
+    fn insert_before(&mut self, n: u32, root: u64) {
+        let anchor = self.nodes[root as usize].as_ref().unwrap();
+
+        if n == 1 {
+            let before = self.stack.pop();
+
+            anchor
+                .parent_node()
+                .unwrap()
+                .insert_before(&before, Some(&anchor))
+                .unwrap();
+        } else {
+            let arr: js_sys::Array = self
+                .stack
+                .list
+                .drain((self.stack.list.len() - n as usize)..)
+                .collect();
+
+            if let Some(el) = anchor.dyn_ref::<Element>() {
+                el.before_with_node(&arr).unwrap();
+            } else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
+                el.before_with_node(&arr).unwrap();
+            } else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
+                el.before_with_node(&arr).unwrap();
+            }
+        }
+    }
+}
+
+#[derive(Debug, Default)]
+struct Stack {
+    list: Vec<Node>,
+}
+
+impl Stack {
+    #[inline]
+    fn with_capacity(cap: usize) -> Self {
+        Stack {
+            list: Vec::with_capacity(cap),
+        }
+    }
+
+    #[inline]
+    fn push(&mut self, node: Node) {
+        self.list.push(node);
+    }
+
+    #[inline]
+    fn pop(&mut self) -> Node {
+        self.list.pop().unwrap()
+    }
+
+    fn top(&self) -> &Node {
+        match self.list.last() {
+            Some(a) => a,
+            None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
+        }
+    }
+}
+
+pub struct DioxusWebsysEvent(web_sys::Event);
+
+// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
+unsafe impl Send for DioxusWebsysEvent {}
+unsafe impl Sync for DioxusWebsysEvent {}
+
+// todo: some of these events are being casted to the wrong event type.
+// We need tests that simulate clicks/etc and make sure every event type works.
+fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send + Sync> {
+    use dioxus_html::on::*;
+    use dioxus_html::KeyCode;
+
+    match event.type_().as_str() {
+        "copy" | "cut" | "paste" => Arc::new(ClipboardData {}),
+        "compositionend" | "compositionstart" | "compositionupdate" => {
+            let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
+            Arc::new(CompositionData {
+                data: evt.data().unwrap_or_default(),
+            })
+        }
+        "keydown" | "keypress" | "keyup" => {
+            let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap();
+            Arc::new(KeyboardData {
+                alt_key: evt.alt_key(),
+                char_code: evt.char_code(),
+                key: evt.key(),
+                key_code: KeyCode::from_raw_code(evt.key_code() as u8),
+                ctrl_key: evt.ctrl_key(),
+                locale: "not implemented".to_string(),
+                location: evt.location() as usize,
+                meta_key: evt.meta_key(),
+                repeat: evt.repeat(),
+                shift_key: evt.shift_key(),
+                which: evt.which() as usize,
+            })
+        }
+        "focus" | "blur" => Arc::new(FocusData {}),
+
+        // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
+        // don't have a good solution with the serialized event problem
+        "change" | "input" | "invalid" | "reset" | "submit" => {
+            let evt: &web_sys::Event = event.dyn_ref().unwrap();
+
+            let target: web_sys::EventTarget = evt.target().unwrap();
+            let value: String = (&target)
+                .dyn_ref()
+                .map(|input: &web_sys::HtmlInputElement| {
+                    // todo: special case more input types
+                    match input.type_().as_str() {
+                        "checkbox" => {
+                           match input.checked() {
+                                true => "true".to_string(),
+                                false => "false".to_string(),
+                            }
+                        },
+                        _ => {
+                            input.value()
+                        }
+                    }
+                })
+                .or_else(|| {
+                    target
+                        .dyn_ref()
+                        .map(|input: &web_sys::HtmlTextAreaElement| input.value())
+                })
+                // select elements are NOT input events - because - why woudn't they be??
+                .or_else(|| {
+                    target
+                        .dyn_ref()
+                        .map(|input: &web_sys::HtmlSelectElement| input.value())
+                })
+                .or_else(|| {
+                    target
+                        .dyn_ref::<web_sys::HtmlElement>()
+                        .unwrap()
+                        .text_content()
+                })
+                .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
+
+            Arc::new(FormData { value })
+        }
+        "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
+        | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
+        | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
+            let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap();
+            Arc::new(MouseData {
+                alt_key: evt.alt_key(),
+                button: evt.button(),
+                buttons: evt.buttons(),
+                client_x: evt.client_x(),
+                client_y: evt.client_y(),
+                ctrl_key: evt.ctrl_key(),
+                meta_key: evt.meta_key(),
+                screen_x: evt.screen_x(),
+                screen_y: evt.screen_y(),
+                shift_key: evt.shift_key(),
+                page_x: evt.page_x(),
+                page_y: evt.page_y(),
+            })
+        }
+        "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
+        | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
+            let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap();
+            Arc::new(PointerData {
+                alt_key: evt.alt_key(),
+                button: evt.button(),
+                buttons: evt.buttons(),
+                client_x: evt.client_x(),
+                client_y: evt.client_y(),
+                ctrl_key: evt.ctrl_key(),
+                meta_key: evt.meta_key(),
+                page_x: evt.page_x(),
+                page_y: evt.page_y(),
+                screen_x: evt.screen_x(),
+                screen_y: evt.screen_y(),
+                shift_key: evt.shift_key(),
+                pointer_id: evt.pointer_id(),
+                width: evt.width(),
+                height: evt.height(),
+                pressure: evt.pressure(),
+                tangential_pressure: evt.tangential_pressure(),
+                tilt_x: evt.tilt_x(),
+                tilt_y: evt.tilt_y(),
+                twist: evt.twist(),
+                pointer_type: evt.pointer_type(),
+                is_primary: evt.is_primary(),
+                // get_modifier_state: evt.get_modifier_state(),
+            })
+        }
+        "select" => Arc::new(SelectionData {}),
+        "touchcancel" | "touchend" | "touchmove" | "touchstart" => {
+            let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap();
+            Arc::new(TouchData {
+                alt_key: evt.alt_key(),
+                ctrl_key: evt.ctrl_key(),
+                meta_key: evt.meta_key(),
+                shift_key: evt.shift_key(),
+            })
+        }
+        "scroll" => Arc::new(()),
+        "wheel" => {
+            let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
+            Arc::new(WheelData {
+                delta_x: evt.delta_x(),
+                delta_y: evt.delta_y(),
+                delta_z: evt.delta_z(),
+                delta_mode: evt.delta_mode(),
+            })
+        }
+        "animationstart" | "animationend" | "animationiteration" => {
+            let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap();
+            Arc::new(AnimationData {
+                elapsed_time: evt.elapsed_time(),
+                animation_name: evt.animation_name(),
+                pseudo_element: evt.pseudo_element(),
+            })
+        }
+        "transitionend" => {
+            let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap();
+            Arc::new(TransitionData {
+                elapsed_time: evt.elapsed_time(),
+                property_name: evt.property_name(),
+                pseudo_element: evt.pseudo_element(),
+            })
+        }
+        "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
+        | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
+        | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
+        | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}),
+        "toggle" => Arc::new(ToggleData {}),
+        _ => Arc::new(()),
+    }
+}
+
+/// This function decodes a websys event and produces an EventTrigger
+/// With the websys implementation, we attach a unique key to the nodes
+fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
+    use anyhow::Context;
+
+    let target = event
+        .target()
+        .expect("missing target")
+        .dyn_into::<Element>()
+        .expect("not a valid element");
+
+    let typ = event.type_();
+
+    let element_id = target
+        .get_attribute("dioxus-id")
+        .context("Could not find element id on event target")?
+        .parse()?;
+
+    Ok(UserEvent {
+        name: event_name_from_typ(&typ),
+        data: virtual_event_from_websys_event(event.clone()),
+        element: Some(ElementId(element_id)),
+        scope_id: None,
+        priority: dioxus_core::EventPriority::Medium,
+    })
+}
+
+pub(crate) fn load_document() -> Document {
+    web_sys::window()
+        .expect("should have access to the Window")
+        .document()
+        .expect("should have access to the Document")
+}
+
+fn event_name_from_typ(typ: &str) -> &'static str {
+    match typ {
+        "copy" => "copy",
+        "cut" => "cut",
+        "paste" => "paste",
+        "compositionend" => "compositionend",
+        "compositionstart" => "compositionstart",
+        "compositionupdate" => "compositionupdate",
+        "keydown" => "keydown",
+        "keypress" => "keypress",
+        "keyup" => "keyup",
+        "focus" => "focus",
+        "blur" => "blur",
+        "change" => "change",
+        "input" => "input",
+        "invalid" => "invalid",
+        "reset" => "reset",
+        "submit" => "submit",
+        "click" => "click",
+        "contextmenu" => "contextmenu",
+        "doubleclick" => "doubleclick",
+        "drag" => "drag",
+        "dragend" => "dragend",
+        "dragenter" => "dragenter",
+        "dragexit" => "dragexit",
+        "dragleave" => "dragleave",
+        "dragover" => "dragover",
+        "dragstart" => "dragstart",
+        "drop" => "drop",
+        "mousedown" => "mousedown",
+        "mouseenter" => "mouseenter",
+        "mouseleave" => "mouseleave",
+        "mousemove" => "mousemove",
+        "mouseout" => "mouseout",
+        "mouseover" => "mouseover",
+        "mouseup" => "mouseup",
+        "pointerdown" => "pointerdown",
+        "pointermove" => "pointermove",
+        "pointerup" => "pointerup",
+        "pointercancel" => "pointercancel",
+        "gotpointercapture" => "gotpointercapture",
+        "lostpointercapture" => "lostpointercapture",
+        "pointerenter" => "pointerenter",
+        "pointerleave" => "pointerleave",
+        "pointerover" => "pointerover",
+        "pointerout" => "pointerout",
+        "select" => "select",
+        "touchcancel" => "touchcancel",
+        "touchend" => "touchend",
+        "touchmove" => "touchmove",
+        "touchstart" => "touchstart",
+        "scroll" => "scroll",
+        "wheel" => "wheel",
+        "animationstart" => "animationstart",
+        "animationend" => "animationend",
+        "animationiteration" => "animationiteration",
+        "transitionend" => "transitionend",
+        "abort" => "abort",
+        "canplay" => "canplay",
+        "canplaythrough" => "canplaythrough",
+        "durationchange" => "durationchange",
+        "emptied" => "emptied",
+        "encrypted" => "encrypted",
+        "ended" => "ended",
+        "error" => "error",
+        "loadeddata" => "loadeddata",
+        "loadedmetadata" => "loadedmetadata",
+        "loadstart" => "loadstart",
+        "pause" => "pause",
+        "play" => "play",
+        "playing" => "playing",
+        "progress" => "progress",
+        "ratechange" => "ratechange",
+        "seeked" => "seeked",
+        "seeking" => "seeking",
+        "stalled" => "stalled",
+        "suspend" => "suspend",
+        "timeupdate" => "timeupdate",
+        "volumechange" => "volumechange",
+        "waiting" => "waiting",
+        "toggle" => "toggle",
+        _ => {
+            panic!("unsupported event type")
+        }
+    }
+}
+
+
+//! This module provides a mirror of the VirtualDOM Element Slab using a Vector.
+
+use std::ops::{Index, IndexMut};
+use web_sys::Node;
+
+pub(crate) struct NodeSlab {
+    nodes: Vec<Option<Node>>,
+}
+
+impl NodeSlab {
+    pub fn new(capacity: usize) -> NodeSlab {
+        let nodes = Vec::with_capacity(capacity);
+        NodeSlab { nodes }
+    }
+}
+impl Index<usize> for NodeSlab {
+    type Output = Option<Node>;
+    fn index(&self, index: usize) -> &Self::Output {
+        &self.nodes[index]
+    }
+}
+
+impl IndexMut<usize> for NodeSlab {
+    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
+        if index >= self.nodes.capacity() * 3 {
+            panic!("Trying to mutate an element way too far out of bounds");
+        }
+
+        if index + 1 > self.nodes.len() {
+            self.nodes.resize_with(index + 1, || None);
+        }
+        &mut self.nodes[index]
+    }
+}
+
+
+#[derive(Debug, Default)]
+struct Stack {
+    list: Vec<Node>,
+}
+
+impl Stack {
+    #[inline]
+    fn with_capacity(cap: usize) -> Self {
+        Stack {
+            list: Vec::with_capacity(cap),
+        }
+    }
+
+    #[inline]
+    fn push(&mut self, node: Node) {
+        self.list.push(node);
+    }
+
+    #[inline]
+    fn pop(&mut self) -> Node {
+        self.list.pop().unwrap()
+    }
+
+    fn top(&self) -> &Node {
+        match self.list.last() {
+            Some(a) => a,
+            None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
+        }
+    }
+}

+ 9 - 4
packages/web/src/rehydrate.rs

@@ -80,7 +80,8 @@ impl WebsysDom {
 
                 *last_node_was_text = true;
 
-                self.nodes[node_id.0] = Some(node);
+                self.interpreter.set_node(node_id.0, node);
+                // self.nodes[node_id.0] = Some(node);
 
                 *cur_place += 1;
             }
@@ -105,7 +106,8 @@ impl WebsysDom {
                     .set_attribute("dioxus-id", s.as_str())
                     .unwrap();
 
-                self.nodes[node_id.0] = Some(node.clone());
+                self.interpreter.set_node(node_id.0, node.clone());
+                // self.nodes[node_id.0] = Some(node.clone());
 
                 *cur_place += 1;
 
@@ -120,9 +122,10 @@ impl WebsysDom {
                 }
 
                 for listener in vel.listeners {
-                    self.new_event_listener(
+                    self.interpreter.NewEventListener(
                         listener.event,
                         listener.mounted_node.get().unwrap().as_u64(),
+                        self.handler.as_ref().unchecked_ref(),
                     );
                 }
 
@@ -142,7 +145,9 @@ impl WebsysDom {
                 let cur_place = place.last_mut().unwrap();
                 let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
 
-                self.nodes[node_id.0] = Some(node);
+                self.interpreter.set_node(node_id.0, node);
+
+                // self.nodes[node_id.0] = Some(node);
 
                 *cur_place += 1;
             }

+ 1 - 2
packages/web/src/ric_raf.rs

@@ -68,8 +68,7 @@ impl RafLoop {
         let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
         let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap();
         let deadline = self.ric_receiver.recv().await.unwrap();
-        let deadline = TimeoutFuture::new(deadline);
-        deadline
+        TimeoutFuture::new(deadline)
     }
 
     pub async fn wait_for_raf(&self) {