Forráskód Böngészése

fixes issues related to components

Matheus Giovani 2 éve
szülő
commit
953d2aa255

+ 6 - 60
packages/renderer/src/core/Component.ts

@@ -1,9 +1,9 @@
 import { reactive } from "../model/Reactivity";
 import { Renderer } from "./vdom/Renderer";
-import { Slot } from "./vdom/renderer/Slot";
 
 import type h from "virtual-dom/h";
 import Debugger from "../util/Debugger";
+import { SlotNode } from "./vdom/nodes/SlotNode";
 
 /**
  * Represents a component's data.
@@ -70,7 +70,7 @@ export class Component {
     /**
      * Any slots references.
      */
-    public $slots: Record<string, Slot> = {};
+    public $slots: Record<string, SlotNode> = {};
 
     /**
      * Any templates references.
@@ -80,7 +80,7 @@ export class Component {
     /**
      * Any component references.
      */
-    public $refs: Record<string, HTMLElement> = {};
+    public $refs: Record<string, Element> = {};
 
     /**
      * If it's the first time that the component is being rendered.
@@ -180,24 +180,6 @@ export class Component {
         this.$templates[templateName] = template;
     }
 
-    /**
-     * Replaces an element with a comment placeholder element.
-     * @param element The element to be replaced.
-     * @returns 
-     */
-    private replaceWithCommentPlaceholder(element: HTMLElement) {
-        const comment = document.createComment("");
-
-        if (!element.parentElement) {
-            element.replaceWith(comment);
-        } else {
-            element.parentElement.insertBefore(comment, element);
-            element.parentElement.removeChild(element);
-        }
-
-        return comment;
-    }
-
     /**
      * Renders the template function into a div tag.
      */
@@ -206,58 +188,22 @@ export class Component {
             this.firstRender = false;
 
             this.$rendered = await this.renderer.render();
-            this.prepareDOM();
         }
 
         return this.$rendered;
     }
 
-    /**
-     * Prepares the component DOM references.
-     * @todo this is buggy and making the components lose their references.
-     * @todo move this to the vdom parsing instead.
-     */
-    public prepareDOM() {
-        // Find all slots, templates and references
-        const slots = Array.from(this.$rendered.querySelectorAll("slot"));
-        const refs = Array.from(this.$rendered.querySelectorAll("[ref]"));
-
-        // Iterate over all slots
-        for(let slot of slots) {
-            // Replace it with a comment tag
-            const comment = this.replaceWithCommentPlaceholder(slot);
-
-            // If it's a named slot
-            if (slot.hasAttribute("name")) {
-                // Save it
-                this.$slots[slot.getAttribute("name")] = new Slot(comment.childNodes);
-                this.$slots[slot.getAttribute("name")].container = comment;
-            }
-        }
-
-        // Iterate over all references
-        for(let ref of refs) {
-            // Save it
-            this.$refs[ref.getAttribute("ref")] = ref as HTMLElement;
-
-            // Remove the attribute
-            ref.removeAttribute("ref");
-        }
-
-        Debugger.debug("%O slots: %O", this, slots);
-    }
-
     /**
      * Renders and mounts the template into a given element.
      * @param target The target element where the element will be mounted.
      * @returns 
      */
-    public async mount(target: HTMLElement | Slot | string) {
+    public async mount(target: HTMLElement | SlotNode | string) {
         const rendered = await this.render();
 
         // If it's targeting a slot
-        if (target instanceof Slot) {
-            target.replaceWith(rendered);
+        if (target instanceof SlotNode) {
+            target.replace(rendered);
             this.$container = rendered;
         } else
         // If it's targeting a string (selector)

+ 21 - 4
packages/renderer/src/core/vdom/Renderer.ts

@@ -12,18 +12,28 @@ import Debugger from "../../util/Debugger";
 import { ConditionalNode } from "./nodes/ConditionalNode";
 import { LoopNode } from "./nodes/LoopNode";
 import VNode from "virtual-dom/vnode/vnode";
+import { SlotNode } from "./nodes/SlotNode";
+import { ComponentNode } from "./nodes/ComponentNode";
 
 const debug = Debugger.extend("vdom");
 
-export type TRendererNodes = RendererNode | ConditionalNode | LoopNode;
+export type TRendererNodes = RendererNode | ConditionalNode | LoopNode | ComponentNode | SlotNode;
 
 /**
  * Most of the evaluation functions were taken from alpine.js
  * Thanks, alpine.js!
  */
 export class Renderer {
-    vnode: string | VirtualDOM.VTree | (string | VirtualDOM.VTree)[];
-    rendererNode: ConditionalNode | LoopNode | RendererNode<VirtualDOM.VText | VirtualDOM.VComment | VirtualDOM.VNode | VirtualDOM.Widget | VirtualDOM.Thunk>;
+    /**
+     * The resulting virtual node related to this renderer.
+     */
+    public vnode: string | VirtualDOM.VTree | (string | VirtualDOM.VTree)[];
+
+    /**
+     * The converted virtual node.
+     */
+    private rendererNode: RendererNode<VirtualDOM.VText | VirtualDOM.VComment | VirtualDOM.VNode | VirtualDOM.Widget | VirtualDOM.Thunk>;
+
     /**
      * Creates a renderer node from a virtual DOM node.
      * @param node The original virtual DOM node.
@@ -39,8 +49,16 @@ export class Renderer {
                 } else
                 if ("x-for" in node.properties.attrs) {
                     return new LoopNode(node, parent, renderer);
+                } else
+                if ("x-component" in node.properties.attrs) {
+                    return new ComponentNode(node, parent, renderer);
                 }
             }
+
+            // If it's a slot node
+            if (node.tagName.toLowerCase() === "slot") {
+                return new SlotNode(node, parent, renderer);
+            }
         }
 
         return new RendererNode(node, parent, renderer);
@@ -159,7 +177,6 @@ export class Renderer {
 
             this.rendererNode.addEventListener("$created", () => {
                 this.component.$rendered = this.rendererNode.element;
-                this.component.prepareDOM();
             });
         });
 

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

@@ -20,7 +20,7 @@ directive("bind", async (node, { value, expression, scope }) => {
             const evaluated = await evaluate(scope);
 
             // Bind the evaluated value to it
-            node.setAttribute(value, evaluated);
+            node.setAttribute(String(value), evaluated);
         
             debug("binding prop \"%s\" to \"%s\"", value, evaluated);
         

+ 7 - 37
packages/renderer/src/core/vdom/directives/Component.ts

@@ -1,49 +1,19 @@
-import { Component } from "../../../core/Component";
 import Debugger from "../../../util/Debugger";
+import { Component } from "../../../core/Component";
 import { directive } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
-import { effect } from "../../../model/Reactivity";
+import { ComponentNode } from "../nodes/ComponentNode";
 
 /**
  * @directive x-component
  * @description Handles a component.
  */
-directive("component", async (node, { expression, scope }) => {
+directive("component", async (node: ComponentNode, { expression, scope }) => {
     const evaluate = evaluateLater(/*js*/`$component.$component.components?.["${expression}"]`);
+    const component = await evaluate(scope) as Component;
 
-    await effect(async () => {
-        try {
-            const component = await evaluate(scope) as Component;
-
-            Debugger.warn("component %s resolved to %O", expression, component);
-
-            // Remove the x-component attribute
-            node.removeAttribute("x-component");
-
-            // Parse all attributes into the component state
-            const attrs = node.getAttributesAndProps();
-            for(let key in attrs) {
-                component.$state[key] = evaluateLater(attrs[key]);
-
-                if (component.$state[key] instanceof Function) {
-                    component.$state[key] = await component.$state[key](scope);
-                }
-            }
-
-            // Set the parent component
-            component.$parent = scope.$component as Component;
-
-            console.error(component.renderer.generateScope());
-
-            const rendered = await component.renderer.renderToNode();
-            rendered.setDirty(false);
-            rendered.setChildrenDirty(false);
+    Debugger.warn("component %s resolved to %O", expression, component);
 
-            // Remove the original attribute from the node
-            node.replaceWith(rendered);
-        } catch(e) {
-            console.warn("pupper.js has failed to create component:");
-            console.error(e);
-        }
-    });
+    node.setScope(scope);
+    await node.setComponent(component);
 });

+ 48 - 0
packages/renderer/src/core/vdom/nodes/ComponentNode.ts

@@ -0,0 +1,48 @@
+import { evaluateLater } from "../../../model/Evaluator";
+import { Component } from "../../../core/Component";
+import { PupperNode } from "../../../model/vdom/PupperNode";
+import { Renderer } from "../Renderer";
+
+export class ComponentNode extends PupperNode {
+    public component: Component;
+    public scope: Record<string, any>;
+
+    protected rendered: Awaited<ReturnType<Renderer["renderToNode"]>>;
+
+    public setScope(scope: Record<string, any>) {
+        this.scope = scope;
+        return this;
+    }
+
+    public async setComponent(component: Component) {
+        this.component = component;
+
+        // Remove the x-component attribute
+        this.removeAttribute("x-component");
+
+        // Parse all attributes into the component state
+        const attrs = this.getAttributesAndProps();
+        for(let key in attrs) {
+            this.component.$state[key] = evaluateLater(attrs[key]);
+
+            if (this.component.$state[key] instanceof Function) {
+                this.component.$state[key] = await this.component.$state[key](this.scope);
+            }
+        }
+
+        // Set the parent component
+        this.component.$parent = this.scope.$component as Component;
+
+        this.rendered = await this.component.renderer.renderToNode();
+
+        this.rendered.setDirty(false);
+        this.rendered.setChildrenDirty(false);
+
+        return this;
+    }
+
+    public toVNode() {
+        // Remove the original attribute from the node
+        return this.rendered.toVNode();
+    }
+}

+ 0 - 9
packages/renderer/src/core/vdom/nodes/ConditionalNode.ts

@@ -1,6 +1,5 @@
 import { RendererNode } from "../../../model/vdom/RendererNode";
 import { PupperNode } from "../../../model/vdom/PupperNode";
-import { Renderer } from "../Renderer";
 
 export class ConditionalNode extends PupperNode {
     /**
@@ -13,14 +12,6 @@ export class ConditionalNode extends PupperNode {
      */
     declare private alternate: RendererNode[] | undefined;
 
-    constructor(
-        node: VirtualDOM.VNode,
-        parent: RendererNode | null = null,
-        renderer: Renderer
-    ) {
-        super(node, parent, renderer);
-    }
-
     protected initNode() {
         super.initNode();
 

+ 0 - 9
packages/renderer/src/core/vdom/nodes/LoopNode.ts

@@ -1,18 +1,9 @@
 import { PupperNode } from "../../../model/vdom/PupperNode";
 import { RendererNode } from "../../../model/vdom/RendererNode";
-import { Renderer } from "../Renderer";
 
 export class LoopNode extends PupperNode {
     declare public body: RendererNode[] | undefined;
 
-    constructor(
-        node: VirtualDOM.VNode,
-        parent: RendererNode | null = null,
-        renderer: Renderer
-    ) {
-        super(node, parent, renderer);
-    }
-
     public clone() {
         const clone = new LoopNode(this.node, this.parent, this.renderer);
         clone.body = this.body.map((child) => child.clone());

+ 37 - 0
packages/renderer/src/core/vdom/nodes/SlotNode.ts

@@ -0,0 +1,37 @@
+import Debugger from "../../../util/Debugger";
+import { RendererNode } from "../../../model/vdom/RendererNode";
+
+export class SlotNode extends RendererNode<VirtualDOM.VNode> {
+    /**
+     * The slot name.
+     */
+    public name: string;
+
+    public initNode() {
+        super.initNode();
+
+        if (!this.hasAttribute("name")) {
+            Debugger.debug("slot %O has no name", this);
+            return;
+        }
+
+        // Save the slot name
+        this.name = this.getAttribute("name") as string;
+        this.tag = "!";
+
+        // Remove the "name" attribute
+        this.removeAttribute("name");
+
+        // Save the slot reference
+        this.renderer.component.$slots[this.name] = this;
+    }
+
+    /**
+     * Replaces the slot contents with a given element.
+     * @param replacement The element to replace this slot's content.
+     */
+    public replace(replacement: Element) {
+        this.element.replaceWith(replacement);
+        this.element = replacement;
+    }
+}

+ 0 - 24
packages/renderer/src/core/vdom/renderer/Slot.ts

@@ -1,24 +0,0 @@
-export class Slot {
-    /**
-     * The comment holding the slot position.
-     */
-    public container: HTMLElement | Comment;
-
-    constructor(
-        /**
-         * All fallback nodes for this slot.
-         */
-        protected fallback: NodeListOf<Node>
-    ) {
-
-    }
-
-    public createComment() {
-        this.container = document.createComment("!");
-        return this.container;
-    }
-
-    public replaceWith(content: Element) {
-        this.container.replaceWith(content);
-    }
-}

+ 1 - 1
packages/renderer/src/model/vdom/PupperNode.ts

@@ -14,7 +14,7 @@ export class PupperNode extends RendererNode {
         return false;
     }
 
-    public toVNode() {
+    public toVNode(): VirtualDOM.VTree | string | (VirtualDOM.VTree | string)[] {
         let node: (VirtualDOM.VTree | string)[] = [];
 
         this.children.forEach((child) => {

+ 23 - 6
packages/renderer/src/model/vdom/RendererNode.ts

@@ -656,7 +656,11 @@ export class RendererNode<TNode extends VirtualDOM.VTree = any> {
             }
         }
 
-        this.element.dispatchEvent(new Event("$created"));
+        const event = new CustomEvent("$created", {
+            detail: this.element
+        });
+
+        this.element.dispatchEvent(event);
     }
 
     /**
@@ -682,11 +686,6 @@ export class RendererNode<TNode extends VirtualDOM.VTree = any> {
         // If it's a plain string
         if (typeof this.node === "string") {
             return this.node;
-        } else
-        // If it's a comment
-        if (this.tag === "!") {
-            this.node = h.c("") as TNode;
-            return this.node;
         }
 
         // If has no $pupper hook yet
@@ -702,6 +701,24 @@ export class RendererNode<TNode extends VirtualDOM.VTree = any> {
             });
         }
 
+        // If it's a comment
+        if (this.tag === "!") {
+            this.node = h.c("", this.properties) as TNode;
+            return this.node;
+        }
+
+        // If has a "ref" attribute
+        if (this.hasAttribute("ref")) {
+            // Add a hook to set it into the component
+            // and remove the ref attribute
+            const name = this.getAttribute("ref") as string;
+            this.removeAttribute("ref");
+
+            this.addEventListener("$created", () => {
+                this.renderer.component.$refs[name] = this.element;
+            });
+        }
+
         const properties: Record<string, any> = {
             ...this.attributes,
             ...this.properties

+ 0 - 1
packages/renderer/tsconfig.json

@@ -2,7 +2,6 @@
     "extends": "../../tsconfig.json",
     "compilerOptions": {
         "outDir": "./out",
-        "baseUrl": "./src",
         "declarationDir": "./types"
     },
     "include": ["./src/**/*.ts", "./src/typings/**/*.d.ts"],

+ 1 - 1
packages/virtual-dom

@@ -1 +1 @@
-Subproject commit ab5070769e71785251874ea00fde4a0c46fe605a
+Subproject commit 7c64127bb5260eb47df4a54c87ca57a8b4a3e165