Ver Fonte

fixes some non-working reactivity functions

Matheus Giovani há 2 anos atrás
pai
commit
ae7a392cbb

+ 3 - 1
packages/renderer/package.json

@@ -11,9 +11,11 @@
     "watch:ts": "tsc -watch"
   },
   "dependencies": {
+    "@types/virtual-dom": "^2.1.1",
+    "dom2hscript": "^0.2.3",
     "morphdom": "^2.6.1",
     "pug": "^3.0.2",
-    "snabbdom": "^3.5.0"
+    "virtual-dom": "^2.1.1"
   },
   "types": "./types/",
   "devDependencies": {

+ 4 - 31
packages/renderer/src/core/Component.ts

@@ -1,4 +1,3 @@
-import { VNode } from "snabbdom";
 import { reactive } from "../model/Reactivity";
 import { Renderer } from "./vdom/Renderer";
 
@@ -31,7 +30,7 @@ export interface IComponent<
     /**
      * The function that renders the template HTML.
      */
-    render?: (...args: any[]) => string | VNode;
+    render?: (...args: any[]) => VirtualDOM.VTree;
 
     /**
      * Any data to be passed to the template.
@@ -177,37 +176,11 @@ export class Component {
         return comment;
     }
 
-    /**
-     * Renders a template and return the rendered child nodes.
-     * @param template The template name to be rendered
-     * @param data The template data
-     * @returns 
-     */
-    public renderTemplate(template: string) {
-        return this.renderStringToTemplate(
-            this.$templates[template](this)
-        ).content.children[0].childNodes;
-    }
-
-    /**
-     * Renders a template string into a template tag with a div with [pup] attribute.
-     * @param string The template string to be rendered.
-     * @returns 
-     */
-    private renderStringToTemplate(string: string): HTMLTemplateElement {
-        const renderContainer = document.createElement("template");
-
-        // @todo this div needs to be removed
-        renderContainer.innerHTML = `<div pup>${string}</div>`;
-
-        return renderContainer;
-    }
-
     /**
      * Renders the template function into a div tag.
      */
     public async render() {
-        let renderContainer: HTMLDivElement;
+        let renderContainer: Element;
 
         if (this.firstRender) {
             this.firstRender = false;
@@ -271,9 +244,9 @@ export class Component {
 
         // If it's targeting a slot
         if (!(target instanceof HTMLElement)) {
-            target.container.replaceWith(...rendered.childNodes);
+            target.container.replaceWith(rendered);
         } else {
-            target.append(...rendered.childNodes);
+            target.append(rendered);
         }
 
         return rendered;

+ 138 - 90
packages/renderer/src/core/vdom/Node.ts

@@ -1,49 +1,60 @@
-import { dir } from "console";
-import { VNode } from "snabbdom";
 import { cloneNode } from "../../model/VirtualDom";
 import { Renderer } from "./Renderer";
 
+import VNode from "virtual-dom/vnode/vnode";
+import VText from "virtual-dom/vnode/vtext";
+
+import h from "virtual-dom/h";
+import diff from "virtual-dom/diff";
+import { patch } from "virtual-dom";
+
 const debug = require("debug")("pupper:vdom:node");
 
-export class Node<TVNode extends Partial<VNode> | string = any> {
-    public children: Node[] = [];
+const Hook = (callback: CallableFunction) => {
+    const hook = function() {};
+    hook.prototype.hook = callback;
+
+    // @ts-ignore
+    return new hook();
+};
+
+export class PupperNode<TNode extends VirtualDOM.VTree = any> {
+    public children: PupperNode[] = [];
 
-    public properties: Record<string, string | boolean | number>;
-    public attributes: Record<string, string | boolean | number>;
-    public eventListeners: Record<string, CallableFunction[]>;
+    public properties: Record<string, string | boolean | number> = {};
+    public attributes: Record<string, string | boolean | number> = {};
+    public eventListeners: Record<string, EventListenerOrEventListenerObject[]> = {};
 
     public tag: string;
 
-    public ignore: boolean = false;
-    public dirty: boolean = true;
-    public invisible: boolean = false;
+    private ignore: boolean = false;
+    private dirty: boolean = true;
+    private patching: boolean;
+
+    public text: string = "";
+    public element: Element = null;
+    key: string;
 
     constructor(
-        protected node: TVNode,
-        public parent: Node = null,
+        protected node: TNode | string,
+        public parent: PupperNode = null,
         public renderer: Renderer
     ) {
         if (typeof node !== "string") {
             // Initialize the properties
-            this.tag = node.sel || "text";
+            this.tag = "tagName" in node ? node.tagName : "text";
 
-            if (node.data) {
-                if ("attrs" in node.data) {
-                    this.attributes = Object.assign({}, node.data.attrs);
-                } else {
-                    this.attributes = {};
+            if ("properties" in node) {
+                if ("attrs" in node.properties) {
+                    this.attributes = Object.assign(this.attributes, node.properties.attrs);
                 }
 
-                if ("props" in node.data) {
-                    this.properties = Object.assign({}, node.data.props);
-                } else {
-                    this.properties = {};
+                if ("props" in node.properties) {
+                    this.properties = Object.assign(this.properties, node.properties.props);
                 }
 
-                if ("on" in node.data) {
-                    this.eventListeners = Object.assign({}, node.data.on as any);
-                } else {
-                    this.eventListeners = {};
+                if ("on" in node.properties) {
+                    this.eventListeners = Object.assign(this.eventListeners, node.properties.on as any);
                 }
             } else {
                 this.attributes = {};
@@ -51,25 +62,49 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
                 this.eventListeners = {};
             }
 
-            this.children = node.children ? node.children.map((child) => new Node(child, this, renderer)) : [];
+            if ("children" in node) {
+                this.children = node.children.map((child) => new PupperNode(child, this, renderer));
+            }
+        } else {
+            this.tag = "text";
+            this.text = node;
         }
     }
 
     /**
-     * Determines if this node is invisible (will be skipped).
-     * @param invisible If it's invisible or not.
+     * Checks if this node element has been rendered.
+     * @returns 
      */
-    public setInvisible(invisible = true) {
-        this.invisible = invisible;
+    public wasRendered() {
+        return this.element !== null;
     }
 
     /**
-     * Determines if this node is dirty (needs to be reparsed) or not.
+     * Sets if this node is dirty (needs to be reparsed) or not.
      * @param dirty If it's dirty or not.
      */
-    public setDirty(dirty: boolean = true) {
+    public setDirty(dirty: boolean = true, autoPatch: boolean = true) {
         this.dirty = dirty;
-        this.renderer.enqueueRender();
+
+        if (dirty && autoPatch) {
+            this.patch();
+        }
+
+        return this;
+    }
+
+    /**
+     * Sets all children to dirty.
+     * @param dirty If it's dirty or not.
+     * @param autoPatch If can automatically call patch()
+     * @returns 
+     */
+    public setChildrenDirty(dirty: boolean = true, autoPatch: boolean = true) {
+        this.children.forEach((child) => {
+            child.setChildrenDirty(dirty, autoPatch);
+        });
+
+        return this;
     }
 
     /**
@@ -81,7 +116,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
     }
 
     /**
-     * Checks if the node is being ignored.
+     * Determines if the node is being ignored.
      * @returns 
      */
     public isBeingIgnored() {
@@ -178,7 +213,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * @param nodes The nodes to replace the current one.
      * @returns 
      */
-    public replaceWith(...nodes: (Node | VNode)[]) {
+    public replaceWith(...nodes: (PupperNode | VirtualDOM.VTree)[]) {
         if (!this.parent) {
             return false;
         }
@@ -186,7 +221,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
         this.parent.children.splice(
             this.getIndex(),
             1,
-            ...nodes.map((node) => !(node instanceof Node) ? new Node(node, this.parent, this.renderer) : node)
+            ...nodes.map((node) => !(node instanceof PupperNode) ? new PupperNode(node, this.parent, this.renderer) : node)
         );
 
         return nodes;
@@ -197,9 +232,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * @returns 
      */
     public replaceWithComment() {
-        const comment = new Node({
-            sel: "!"
-        }, this.parent, this.renderer);
+        const comment = new PupperNode(new VNode("COMMENT", {}, []), this.parent, this.renderer);
 
         this.replaceWith(comment);
 
@@ -211,7 +244,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * @param event The event name to be added.
      * @param listener The event callback.
      */
-    public addEventListener(event: keyof DocumentEventMap | string, listener: CallableFunction) {
+    public addEventListener(event: keyof DocumentEventMap | string, listener: EventListenerOrEventListenerObject) {
         this.eventListeners[event] = this.eventListeners[event] || [];
         this.eventListeners[event].push(listener);
     }
@@ -221,7 +254,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * @param event The event name.
      * @param listener The callback to be removed.
      */
-    public removeEventListener(event: keyof DocumentEventMap | string, listener: CallableFunction) {
+    public removeEventListener(event: keyof DocumentEventMap | string, listener: EventListenerOrEventListenerObject) {
         this.eventListeners[event].splice(
             this.eventListeners[event].indexOf(listener),
             1
@@ -249,7 +282,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * @param parent The new node parent.
      * @returns 
      */
-    public setParent(parent: Node) {
+    public setParent(parent: PupperNode) {
         this.parent = parent;
         return this;
     }
@@ -258,7 +291,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * Insert a list of nodes before the current node.
      * @param nodes The list of nodes to be inserted.
      */
-    public insertBefore(...nodes: Node[]) {
+    public insertBefore(...nodes: PupperNode[]) {
         this.parent.children.splice(
             this.getIndex() - 1,
             0,
@@ -270,7 +303,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * Insert a list of nodes after the current node.
      * @param nodes The list of nodes to be inserted.
      */
-    public insertAfter(...nodes: Node[]) {
+    public insertAfter(...nodes: PupperNode[]) {
         this.parent.children.splice(
             this.getIndex() + 1,
             0,
@@ -282,7 +315,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * Appends a node to the children nodes.
      * @param node The node to be appended.
      */
-    public appendChild(node: Node) {
+    public appendChild(node: PupperNode) {
         this.children.push(node);
     }
 
@@ -290,7 +323,7 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * Appends a list of node to the children nodes.
      * @param nodes The nodes to be appended.
      */
-    public append(...nodes: Node[]) {
+    public append(...nodes: PupperNode[]) {
         this.children.push(...nodes);
     }
 
@@ -309,69 +342,84 @@ export class Node<TVNode extends Partial<VNode> | string = any> {
      * @returns 
      */
     public clone() {
-        return new Node(cloneNode(this.node as VNode), this.parent, this.renderer);
+        return new PupperNode(cloneNode(this.node || this.text), this.parent, this.renderer);
     }
 
     /**
-     * Patches the DOM for this element.
+     * Retrieves the root node.
+     * @returns 
      */
-    public updateDOM(callback?: CallableFunction) {
-        if (typeof this.node === "string") {
+    public getRoot() {
+        let node: PupperNode = this;
+
+        while(node.parent !== null) {
+            node = node.parent;
+        }
+        
+        return node;
+    }
+
+    /**
+     * Enqueues a patch to the internal VDOM element.
+     */
+    public patch() {
+        // Prevent patching if not dirty, being ignored or already patching
+        if (!this.dirty || this.ignore || this.patching) {
             return;
         }
 
-        if (this.renderer.rendered) {
-            this.renderer.update();
+        if (this.wasRendered()) {
+            debug("element was rendered, will be patched");
 
-            if (typeof callback === "function") {
-                this.renderer.nextTick(callback);
-            }
+            this.patching = true;
+            this.renderer.singleNextTick(this.doPatch.bind(this));
         }
     }
 
     /**
-     * Retrieves the root node.
-     * @returns 
+     * Patches the VDOM element in real DOM.
      */
-    public getRoot() {
-        let node: Node = this;
+    private doPatch() {
+        const diffs = diff(this.node as any, this.toVNode());
 
-        while(node.parent !== null) {
-            node = node.parent;
-        }
-        
-        return node;
+        this.element = patch(this.element, diffs);
+        this.patching = false;
     }
 
     /**
-     * Converts this node into a virtual node.
+     * Converts the current node into a virtual DOM node.
      * @returns 
      */
-    public toVirtualNode(): TVNode | VNode {
-        if (typeof this.node === "string") {
-            return {
-                sel: undefined,
-                data: undefined,
-                elm: undefined,
-                children: undefined,
-                key: undefined,
-                text: this.node
-            };
+    public toVNode(): VirtualDOM.VTree {
+        if (this.tag === "text") {
+            this.node = new VText(this.text) as TNode;
+            return this.node as VirtualDOM.VText;
+        }
+
+        const properties: Record<string, any> = {
+            ...this.attributes,
+            ...this.properties,
+            $p_create: Hook((node: Element) => {
+                this.element = node;
+            })
+        };
+
+        // Rename the "class" attribute
+        if (properties.class) {
+            properties.className = properties.class;
+            delete properties.class;
         }
 
-        this.node = {
-            sel: this.tag === "text" ? undefined : this.tag,
-            data: {
-                props: this.properties,
-                attrs: this.attributes,
-                on: this.eventListeners as any
-            },
-            elm: this.node.elm,
-            children: this.children.map((child) => child.toVirtualNode()),
-            key: this.node.key,
-            text: this.node.text
-        } as TVNode;
-
-        return this.node;
+        for(let evt in this.eventListeners) {
+            properties["on" + evt] = this.eventListeners[evt];
+        }
+
+        this.node = h(
+            this.tag,
+            properties,
+            this.children.map((child) => child.toVNode())
+        ) as TNode;
+
+        return this.node as VirtualDOM.VTree;
     }
 }

+ 25 - 63
packages/renderer/src/core/vdom/Renderer.ts

@@ -1,18 +1,12 @@
 import { Component } from "../Component";
-import {
-    h,
-    propsModule,
-    attributesModule,
-    styleModule,
-    eventListenersModule,
-    init,
-    VNode
-} from "snabbdom";
 
 import Pupper from "../..";
 
 import { walk } from "../../model/NodeWalker";
-import { Node } from "./Node";
+import { PupperNode } from "./Node";
+
+import { diff, patch, create } from "virtual-dom";
+import h from "virtual-dom/h";
 
 const debug = require("debug")("pupper:vdom");
 
@@ -21,7 +15,8 @@ const debug = require("debug")("pupper:vdom");
  * Thanks, alpine.js!
  */
 export class Renderer {
-    private patch: ReturnType<typeof init>;
+    public diff = diff;
+    public patch = patch;
 
     /**
      * The stack of states that formulates the context for rendering elements.
@@ -31,12 +26,7 @@ export class Renderer {
     /**
      * The container that will receive the renderer contents.
      */
-    protected container: HTMLDivElement;
-
-    /**
-     * The current VDOM node.
-     */
-    protected currentDOM: VNode;
+    protected container: Element;
 
     /**
      * The rendering queue.
@@ -59,13 +49,6 @@ export class Renderer {
     constructor(
         protected component: Component
     ) {
-        this.patch = init([
-            propsModule,
-            attributesModule,
-            styleModule,
-            eventListenersModule
-        ]);
-
         this.stateStack.push(
             // Globals
             Pupper.$global,
@@ -137,17 +120,17 @@ export class Renderer {
         const tick = this.nextTick(async () => {
             debug("first render");
 
-            const vdom = this.component.$component.render({ h }) as VNode;
-            const node = new Node(vdom, null, this);
-
+            const vdom = this.component.$component.render({ h });
+            const node = new PupperNode(vdom, null, this);
             const result = await walk(node, this.generateScope());
 
-            this.currentDOM = result.toVirtualNode() as VNode;
-
-            this.container = document.createElement("div");
-            this.patch(this.container, this.currentDOM);
+            this.container = create(result.toVNode() as VirtualDOM.VNode, {
+                warn: true
+            });
 
             this.rendered = true;
+
+            debug("first render ended");
         });
 
         await this.waitForTick(tick);
@@ -171,45 +154,24 @@ export class Renderer {
     }
 
     /**
-     * Waits for the given tick or the last added tick to be executed.
-     * @returns 
-     */
-    public waitForTick(tick: number = null) {
-        return new Promise((resolve) => {
-            this.queue[tick !== null ? (tick - 1) : this.queue.length - 1].listeners.push(resolve);
-        });
-    }
-
-    /**
-     * Updates the renderer contents.
+     * Enqueues a function to be executed in the next queue tick only if it hasn't been enqueued yet.
+     * @param callback The callback to be executed.
      */
-    public update() {
-        if (!this.rendered) {
+    public singleNextTick(callback: CallableFunction) {
+        if (this.queue.find((c) => c.callback === callback)) {
             return;
         }
 
-        this.isRenderEnqueued = true;
-
-        return this.nextTick(async () => {
-            const vdom = this.component.$component.render({ h }) as VNode;
-            const node = new Node(vdom, null, this);
-
-            const result = await walk(node, this.generateScope());
-
-            const newDOM = result.toVirtualNode() as VNode;
-
-            this.patch(this.currentDOM, newDOM);
-
-            this.currentDOM = newDOM;
-        });
+        this.nextTick(callback);
     }
 
     /**
-     * Enqueues a render update if the not enqueued yet.
+     * Waits for the given tick or the last added tick to be executed.
+     * @returns 
      */
-    public enqueueRender() {
-        if (!this.isRenderEnqueued) {
-            this.nextTick(() => this.update());
-        }
+    public waitForTick(tick: number = null) {
+        return new Promise((resolve) => {
+            this.queue[tick !== null ? (tick - 1) : this.queue.length - 1].listeners.push(resolve);
+        });
     }
 }

+ 1 - 2
packages/renderer/src/core/vdom/directives/Bind.ts

@@ -1,10 +1,9 @@
-import { directive, mapAttributes, startingWith as replaceWith } from "../../../model/Directive";
+import { directive, mapAttributes, replaceWith } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
 import { effect } from "../../../model/Reactivity";
 
 const debug = require("debug")("pupper:vdom:on");
 
-mapAttributes(replaceWith("@", "x-on:"));
 mapAttributes(replaceWith(":", "x-bind:"));
 
 /**

+ 16 - 9
packages/renderer/src/core/vdom/directives/Conditional.ts

@@ -2,6 +2,7 @@ import { directive } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
 import { walk } from "../../../model/NodeWalker";
 import { effect } from "../../../model/Reactivity";
+import { PupperNode } from "../Node";
 
 const debug = require("debug")("pupper:vdom:directives:conditional");
 
@@ -14,25 +15,31 @@ directive("if", async (node, { expression, scope }) => {
 
     // Save and remove the children
     const children = node.children;
-    const comment = node.replaceWithComment();
+    node = node.replaceWithComment();
+    node.setIgnored();
 
-    await effect(async () => {
-        if (comment.isBeingIgnored()) {
-            return;
-        }
+    let clones: PupperNode[] = [];
 
+    await effect(async () => {
         try {
             const value = await evaluate(scope);
 
             debug("%s evaluated to %O", expression, value);
 
+            // If already rendered the clones
+            if (clones.length) {
+                clones.forEach((clone) => clone.delete());
+                clones = [];
+            }
+
+            // If the conditional matched
             if (value) {
-                comment.insertBefore(
-                    ...await walk(children, scope)
-                );
+                // Clone them into the DOM
+                clones = await walk(children.map((child) => child.clone().setParent(node.parent)), scope);
+                node.insertBefore(...clones);
             }
 
-            comment.parent.setDirty();
+            node.parent.setDirty();
         } catch(e) {
             console.warn("[pupperjs] failed to evaluate conditional:");
             console.error(e);

+ 3 - 1
packages/renderer/src/core/vdom/directives/EventHandler.ts

@@ -1,8 +1,10 @@
-import { directive } from "../../../model/Directive";
+import { directive, mapAttributes, replaceWith } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
 
 const debug = require("debug")("pupper:vdom:on");
 
+mapAttributes(replaceWith("@", "x-on:"));
+
 /**
  * @directive x-on
  * @description Adds an event handler to the node.

+ 2 - 2
packages/renderer/src/core/vdom/directives/HTML.ts

@@ -1,7 +1,7 @@
 import { directive } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
 import { effect } from "../../../model/Reactivity";
-import { Node } from "../Node";
+import { PupperNode } from "../Node";
 
 /**
  * @directive x-html
@@ -15,7 +15,7 @@ directive("html", async (node, { expression, scope }) => {
             const html = await evaluate(scope) as string;
 
             node.appendChild(
-                new Node(html, node.parent, node.renderer)
+                new PupperNode(html, node.parent, node.renderer)
             );
 
             node.removeAttribute("x-html");

+ 20 - 12
packages/renderer/src/core/vdom/directives/Loop.ts

@@ -3,7 +3,7 @@ import { evaluateLater } from "../../../model/Evaluator";
 import { walk } from "../../../model/NodeWalker";
 import { effect } from "../../../model/Reactivity";
 import { IsNumeric, IsObject } from "../../../util/ObjectUtils";
-import { Node } from "../Node";
+import { PupperNode } from "../Node";
 
 /**
  * @directive x-for
@@ -16,9 +16,9 @@ directive("for", async (node, { expression, scope }) => {
     // Save and remove the children
     const children = node.children;
     node = node.replaceWithComment();
-    node.setIgnored(true);
+    node.setIgnored();
 
-    let clonedChildren: Node[] = [];
+    let clones: PupperNode[] = [];
 
     await effect(async () => {        
         let loopScope;
@@ -40,15 +40,20 @@ directive("for", async (node, { expression, scope }) => {
                 items = [];
             }
 
-            // Delete the existing children
-            //node.parent.children = node.parent.children.filter((child) => !clonedChildren.includes(child));
+            // Clear the older nodes if needed
+            if (clones.length) {
+                node.parent.children.splice(
+                    node.getIndex() - clones.length,
+                    clones.length
+                );
+
+                clones = [];
+            }
 
             // Iterate over all evaluated items
             for(let item in items) {
                 loopScope = { ...scope };
 
-                console.log(items[item]);
-                
                 // Push the current item to the state stack
                 if ("item" in loopData) {
                     loopScope[loopData.item] = items[item];
@@ -63,19 +68,22 @@ directive("for", async (node, { expression, scope }) => {
                 }
 
                 for(let child of children) {
-                    child = child.clone().setParent(node.parent);
+                    child = child.clone().setParent(node.parent);                    
                     node.insertBefore(child);
 
-                    await walk(child, loopScope);
-                }                
+                    child = await walk(child, loopScope);
+                    clones.push(child);
+                }
+
+                console.log(node, items[item]);
             }
+
+            node.parent.setDirty();
         } catch(e) {
             console.warn("[pupperjs] The following information can be useful for debugging:");
             console.warn("last scope:", loopScope);
             console.error(e);
         }
-
-        node.parent.setDirty();
     });
 });
 

+ 13 - 7
packages/renderer/src/core/vdom/directives/Text.ts

@@ -1,7 +1,7 @@
 import { directive } from "../../../model/Directive";
 import { maybeEvaluateLater } from "../../../model/Evaluator";
 import { effect } from "../../../model/Reactivity";
-import { Node } from "../Node";
+import { PupperNode } from "../Node";
 
 /**
  * @directive x-text
@@ -10,6 +10,8 @@ import { Node } from "../Node";
 directive("text", async (node, { expression, scope }) => {
     const evaluate = maybeEvaluateLater(expression);
 
+    let replacedNode: PupperNode = null;
+
     await effect(async () => {
         try {
             const text = await evaluate(scope) as string;
@@ -20,13 +22,17 @@ directive("text", async (node, { expression, scope }) => {
             }
 
             if (node.tag === "text") {
-                node.replaceWith(new Node(text, node.parent, node.renderer));
+                node.replaceWith(new PupperNode(text, node.parent, node.renderer));
             } else {
-                node.appendChild(
-                    new Node(text, node, node.renderer)
-                );
-
-                node.removeAttribute("x-text");
+                if (replacedNode) {
+                    replacedNode = replacedNode.replaceWith(
+                        new PupperNode(text, node, node.renderer)
+                    );
+                } else {
+                    replacedNode = new PupperNode(text, node, node.renderer);
+                    node.appendChild(replacedNode);
+                    node.removeAttribute("x-text");
+                }
             }
 
             node.setDirty();

+ 5 - 11
packages/renderer/src/model/Directive.ts

@@ -1,16 +1,10 @@
-import { VNode } from "snabbdom";
-import { Node } from "../core/vdom/Node";
+import { PupperNode } from "../core/vdom/Node";
 import { Renderer } from "../core/vdom/Renderer";
 
 export type TScope = Record<string, string | boolean | number>;
 
 export type TAttributeVal = string | number | boolean;
 
-export interface IDirectiveVNode extends VNode {
-    _x_ignore: boolean;
-    _x_ignoreSelf: boolean;
-}
-
 type TDirectives = typeof directiveOrder[number];
 
 interface IProp {
@@ -40,7 +34,7 @@ const currentHandlerStackKey = Symbol();
 const pupperAttrRegex = /^x-([^:^.]+)\b/;
 
 type TDirectiveCallback = (
-    node: Node,
+    node: PupperNode,
     data: {
         renderer: Renderer;
         scope: TScope;
@@ -58,7 +52,7 @@ export function directive(attribute: TDirectives, callback: TDirectiveCallback)
  * @param node The node to be evaluated.
  * @returns 
  */
-export function directives(node: Node, scope: TScope) {
+export function directives(node: PupperNode, scope: TScope) {
     let transformedAttributeMap: Record<string, string> = {};
 
     const attributes = node.getAttributesAndProps();
@@ -86,7 +80,7 @@ export function directives(node: Node, scope: TScope) {
         });
 }
 
-export function getDirectiveHandler(node: Node, directive: IDirective, scope: TScope) {
+export function getDirectiveHandler(node: PupperNode, directive: IDirective, scope: TScope) {
     let noop = async () => {};
     let handler = directiveHandler[directive.type] || noop;
 
@@ -154,7 +148,7 @@ export function mapAttributes(callback: CallableFunction) {
     attributeTransformers.push(callback);
 }
 
-export function startingWith(subject: string, replacement: string): (prop: IProp) => IProp {
+export function replaceWith(subject: string, replacement: string): (prop: IProp) => IProp {
     return ({ name, value }) => {
         if (name.startsWith(subject)) {
             name = name.replace(subject, replacement);

+ 6 - 9
packages/renderer/src/model/NodeWalker.ts

@@ -1,9 +1,9 @@
-import { Node } from "../core/vdom/Node";
+import { PupperNode } from "../core/vdom/Node";
 import { directives } from "./Directive";
 
-export async function walk<TNode extends Node | Node[]>(nodes: TNode, scope: any = null): Promise<TNode> {
+export async function walk<TNode extends PupperNode | PupperNode[]>(nodes: TNode, scope: any = null): Promise<TNode> {
     if (!Array.isArray(nodes)) {
-        return await node(nodes as Node, scope) as TNode;
+        return await node(nodes as PupperNode, scope) as TNode;
     }
 
     let count = nodes.length;
@@ -37,7 +37,7 @@ export async function walk<TNode extends Node | Node[]>(nodes: TNode, scope: any
     return nodes;
 }
 
-async function node(node: Node | undefined, scope: any) {
+async function node(node: PupperNode | undefined, scope: any) {
     //console.group(node.tag, node.getAttributesAndProps());
 
     // If it's an invalid node
@@ -63,11 +63,8 @@ async function node(node: Node | undefined, scope: any) {
         await handle();
     }
 
-    // If it's invisible
-    if (node.invisible) {
-        //console.groupEnd();
-        return undefined;
-    }
+    // Set it as non-dirty.
+    node.setDirty(false);
 
     // If the node was removed, stop parsing
     if (!node.exists()) {

+ 31 - 6
packages/renderer/src/model/Reactivity.ts

@@ -17,37 +17,62 @@ export async function effect(effect: TEffect) {
 export function reactive(obj: TReactiveObj) {
     return new Proxy(obj, {
         get(target, property) {
-            if (currentEffect === null) {
+            // If detected no current effect
+            // or this property is somehow undefined
+            if (currentEffect === null || target[property] === undefined) {
+                // Ignore
                 return target[property];
             }
 
+            // Ignore functions
+            if (typeof target[property] === "function") {
+                return target[property];
+            }
+
+            // If this target has no effects yet
             if (!effects.has(target)) {
+                // Add a new effect handler to it
                 effects.set(target, {} as any);
             }
 
+            // Retrieves the effects for the current target
             const targetEffects = effects.get(target);
 
+            // If has no effect handler for this property yet
             if (!targetEffects[property]) {
-                targetEffects[property] = [];
+                // Create a new one
+                targetEffects[property] = [
+                    currentEffect
+                ];
+            } else {
+                // Add the bubble to it
+                targetEffects[property].push(currentEffect);
             }
 
-            targetEffects[property].push(currentEffect);
-
             return target[property];
         },
 
         set(target, property, value) {
+            // JavaScript, for some reason, treats "null" as an object
+            if (typeof value === null) {
+                target[property] = null;
+            } else
             // Only objects can be reactive
             if (typeof value === "object") {
                 target[property] = reactive(value);
+            } else {
+                target[property] = value;
             }
 
+            // If has any effects for the given target
             if (effects.has(target)) {
                 const targetEffects = effects.get(target);
+                let propEffects = targetEffects[property];
 
-                if (Array.isArray(targetEffects[property])) {
+                // If it's a valid array
+                if (Array.isArray(propEffects)) {
                     (async () => {
-                        for(let effect of targetEffects[property]) {
+                        for(let effect of propEffects) {
                             await effect();
                         }
                     })();

+ 13 - 18
packages/renderer/src/model/VirtualDom.ts

@@ -1,12 +1,13 @@
-import { VNode } from "snabbdom";
+import VNode from "virtual-dom/vnode/vnode";
+import VText from "virtual-dom/vnode/vtext";
 
 /**
  * Clones a list of nodes.
  * @param nodes The list to be cloned.
  * @returns 
  */
-export function cloneNodes(nodes: (VNode | string)[]) {
-    const cloned: (VNode | string)[] = [];
+export function cloneNodes(nodes: (VirtualDOM.VTree)[]) {
+    const cloned: (VirtualDOM.VTree)[] = [];
 
     for(let node of nodes) {
         cloned.push(cloneNode(node));
@@ -15,21 +16,15 @@ export function cloneNodes(nodes: (VNode | string)[]) {
     return cloned;
 }
 
-export function cloneNode(node: VNode | string): VNode | string {
-    if (typeof node === "string") {
-        return node;
+export function cloneNode(node: VirtualDOM.VTree | string): VirtualDOM.VTree {
+    if (!(node instanceof VNode)) {
+        if (node instanceof VText) {
+            return new VText(node.text);
+        }
+
+        return new VText(node);
     }
 
-    return {
-        children: node.children ? cloneNodes(node.children) : undefined,
-        data: node.data ? {
-            attrs: node.data.attrs ? JSON.parse(JSON.stringify(node.data.attrs)) : undefined,
-            props: node.data.props ? JSON.parse(JSON.stringify(node.data.props)) : undefined,
-            on: node.data.on ? JSON.parse(JSON.stringify(node.data.on)) : undefined
-        } : undefined,
-        elm: undefined,
-        key: node.key || undefined,
-        sel: node.sel || undefined,
-        text: node.text || undefined
-    };
+    // @ts-ignore
+    return new VNode(node.tagName, node.properties, node.children, node.key, node.namespace);
 }