Browse Source

fixes and changes a lot of things
nested for loops are not working

Matheus Giovani 2 năm trước cách đây
mục cha
commit
ccf09a7978

+ 34 - 13
packages/compiler/src/core/plugin/hooks/ConditionalHook.ts

@@ -1,3 +1,5 @@
+import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
+import Plugin from "../../Plugin";
 import { Hook } from "../Hook";
 import { ConditionalNode } from "../nodes/ConditionalNode";
 import { TagNode } from "../nodes/TagNode";
@@ -10,26 +12,45 @@ export class ConditionalHook extends Hook {
                 const consequent = node.getThen();
                 const alternate = node.getElse();
 
-                // Replace with an if <div x-if>
+                // Replace with a <$ x-if />
                 const conditional = node.replaceWith({
                     type: "Tag",
-                    name: "template",
+                    name: "$p",
                     attributes: {
                         "x-if": node.getProp("test")
-                    },
-                    children: this.plugin.parseChildren(consequent) as any
+                    }
                 }) as TagNode;
 
-                // <div x-if!>
-                if (node.hasElse()) {
-                    const elseTag = conditional.insertAfter({
-                        type: "Tag",
-                        name: "template",
-                        attributes: {
-                            "x-if": `!(${node.getProp("test")})`
+                // Add the <$ x-if-cond="consequent" /> to it
+                conditional.children.push(
+                    CompilerNode.fromCustomNode(
+                        {
+                            type: "Tag",
+                            name: "$p",
+                            attributes: {
+                                "x-if-cond": "consequent"
+                            },
+                            children: this.plugin.parseChildren(consequent) as any
                         },
-                        children: this.plugin.parseChildren(alternate) as any
-                    });
+                        conditional
+                    )
+                );
+
+                // Add the <$ x-if-cond="consequent" /> to it if needed
+                if (node.hasElse()) {
+                    conditional.children.push(
+                        CompilerNode.fromCustomNode(
+                            {
+                                type: "Tag",
+                                name: "$",
+                                attributes: {
+                                    "x-if-cond": "alternate"
+                                },
+                                children: this.plugin.parseChildren(alternate) as any
+                            },
+                            conditional
+                        )
+                    );
                 }
             }
         }

+ 1 - 1
packages/compiler/src/core/plugin/nodes/EachNode.ts

@@ -42,7 +42,7 @@ export class EachNode extends CompilerNode<Pug.Nodes.EachNode> {
 
         return CompilerNode.parseNodeIntoPugNode({
             type: "Tag",
-            name: "template",
+            name: "$",
             attributes: {
                 "x-for": parsedConditional
             },

+ 2 - 2
packages/compiler/src/core/plugin/phases/PrepareComponentsHook.ts

@@ -317,11 +317,11 @@ export class PrepareComponents extends Hook {
         // Iterate over all children
         node.getChildren().forEach((child) => {
             // Ignore comments
-            if (child.isType("Comment")) {
+            if (child.isComment()) {
                 return;
             }
 
-            // If it's not a tag
+            // If it's not a tag or a comment
             if (!(child instanceof TagNode)) {
                 throw this.plugin.compiler.makeParseError("The implementation tag should only contain methods and events, found a " + child.getType() + ".", {
                     line: child.getLine(),

+ 16 - 0
packages/compiler/src/model/core/nodes/CompilerNode.ts

@@ -16,6 +16,18 @@ export interface IParserNode {
 export type TNodes = PugNodes | CompilerNode | IParserNode;
 
 export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<CompilerNode<any>> {
+    /**
+     * Creates a compiler node from a node in our format.
+     * @param node The node to be parsed.
+     * @returns 
+     */
+    public static fromCustomNode<TNode extends IParserNode, TFinalNode = CompilerNode<TPugNodesWithTypes[TNode["type"]]>>(node: TNode, parent: NodeModel): TFinalNode {
+        return Plugin.createNode(
+            this.parseNodeIntoPugNode(node),
+            parent
+        ) as any as TFinalNode;
+    }
+
     /**
      * Makes a pug attribute node.
      * @param key The attribute name.
@@ -134,6 +146,10 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
         }
     }
 
+    public isComment() {
+        return this.pugNode.type === "BlockComment" || this.pugNode.type === "Comment";
+    }
+
     /**
      * Finds the first children node by a given type.
      * @param type The children node type.

+ 5 - 0
packages/compiler/src/typings/pug.d.ts

@@ -217,6 +217,10 @@ export declare namespace Pug {
             val: string;
             buffer: boolean;
         }
+
+        export declare interface BlockCommentNode extends CommentNode {
+            type: "BlockComment";
+        }
     }
 }
 
@@ -259,6 +263,7 @@ declare module "pug" {
         Pug.Nodes.MixinNode |
         Pug.Nodes.TextNode | 
         Pug.Nodes.CommentNode |
+        Pug.Nodes.BlockCommentNode | 
         Pug.Nodes.CodeNode
     );
 }

+ 10 - 1
packages/renderer/src/core/Component.ts

@@ -149,6 +149,15 @@ export class Component {
         }
     }
 
+    /**
+     * Enqueues a function to be executed in the next queue tick.
+     * @param callback — The callback to be executed.
+     * @returns 
+     */
+    public $nextTick(callback: CallableFunction) {
+        return this.renderer.nextTick(callback);
+    }
+
     /**
      * Registers a single template.
      * @param templateName The template name.
@@ -185,7 +194,7 @@ export class Component {
         if (this.firstRender) {
             this.firstRender = false;
 
-            renderContainer = await this.renderer.renderFirst();
+            renderContainer = await this.renderer.render();
 
             // Find all slots, templates and references
             const slots = Array.from(renderContainer.querySelectorAll("slot"));

+ 49 - 13
packages/renderer/src/core/vdom/Renderer.ts

@@ -3,20 +3,47 @@ import { Component } from "../Component";
 import Pupper from "../..";
 
 import { walk } from "../../model/NodeWalker";
-import { PupperNode } from "./Node";
+import { RendererNode } from "../../model/vdom/RendererNode";
 
 import { diff, patch, create } from "virtual-dom";
 import h from "virtual-dom/h";
 
 import Debugger from "../../util/Debugger";
+import { ConditionalNode } from "./nodes/ConditionalNode";
+import { LoopNode } from "./nodes/LoopNode";
+import VNode from "virtual-dom/vnode/vnode";
 
 const debug = Debugger.extend("vdom");
 
+export type TRendererNodes = RendererNode | ConditionalNode | LoopNode;
+
 /**
  * Most of the evaluation functions were taken from alpine.js
  * Thanks, alpine.js!
  */
 export class Renderer {
+    /**
+     * Creates a renderer node from a virtual DOM node.
+     * @param node The original virtual DOM node.
+     * @param parent The parent node.
+     * @param renderer The renderer related to this node.
+     * @returns 
+     */
+    public static createNode(node: VirtualDOM.VTree | string, parent: RendererNode, renderer: Renderer) {
+        if (node instanceof VNode) {
+            if ("properties" in node && "attrs" in node.properties) {
+                if ("x-if" in node.properties.attrs) {
+                    return new ConditionalNode(node, parent, renderer);
+                } else
+                if ("x-for" in node.properties.attrs) {
+                    return new LoopNode(node, parent, renderer);
+                }
+            }
+        }
+
+        return new RendererNode(node, parent, renderer);
+    }
+
     public diff = diff;
     public patch = patch;
 
@@ -43,11 +70,6 @@ export class Renderer {
      */
     private inQueue: boolean;
 
-    /**
-     * Determines if has a pending render.
-     */
-    private isRenderEnqueued: boolean;
-
     constructor(
         protected component: Component
     ) {
@@ -55,8 +77,16 @@ export class Renderer {
             // Globals
             Pupper.$global,
 
+            // Magics
+            Pupper.$magics,
+
             // Component state
             component.$state,
+
+            // Renderer-related
+            {
+                $component: component
+            }
         );
     }
 
@@ -118,21 +148,27 @@ export class Renderer {
      * Renders the virtual dom for the first time.
      * @returns 
      */
-    public async renderFirst() {
+    public async render() {
         const tick = this.nextTick(async () => {
             debug("first render");
 
             const vdom = this.component.$component.render({ h });
-            const node = new PupperNode(vdom, null, this);
+            const node = Renderer.createNode(vdom, null, this);
             const result = await walk(node, this.generateScope());
+            const vnode = result.toVNode();
 
-            this.container = create(result.toVNode() as VirtualDOM.VNode, {
-                warn: true
-            });
+            try {
+                this.container = create(vnode as VirtualDOM.VNode, {
+                    warn: true
+                });
 
-            this.rendered = true;
+                this.rendered = true;
 
-            debug("first render ended");
+                debug("first render ended");
+            } catch(e) {
+                Debugger.error("an exception ocurred while rendering a component %O", vnode);
+                throw e;
+            }
         });
 
         await this.waitForTick(tick);

+ 32 - 28
packages/renderer/src/core/vdom/directives/Conditional.ts

@@ -1,10 +1,11 @@
+import { RendererNode } from "@/model/vdom/RendererNode";
 import { directive } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
 import { walk } from "../../../model/NodeWalker";
 import { effect } from "../../../model/Reactivity";
-import { PupperNode } from "../Node";
 
 import Debugger from "../../../util/Debugger";
+import { ConditionalNode } from "../nodes/ConditionalNode";
 
 const debug = Debugger.extend("vdom:directives:conditional");
 
@@ -12,19 +13,18 @@ const debug = Debugger.extend("vdom:directives:conditional");
  * @directive x-if
  * @description Conditionally renders a tag's children nodes if the condition is met.
  */
-directive("if", async (node, { expression, scope }) => {
+directive("if", async (node: ConditionalNode, { expression, scope }) => {
     const evaluate = evaluateLater(expression);
 
-    // Save and remove the children
-    const children = node.children;
-    node = node.replaceWithComment();
-    node.setIgnored();
-    node.setRenderable(false);
+    let lastValue: boolean = null;
 
-   let clones: PupperNode[] = [];
-   let lastValue: boolean = null;
+    if (!node.hasConsequence()) {
+        Debugger.error("node %O has no consequence.", node);
+    }
+
+    const removeEffect = await effect(async () => {
+        debug("running");
 
-    await effect(async () => {
         try {
             const value = await evaluate(scope);
 
@@ -36,31 +36,35 @@ directive("if", async (node, { expression, scope }) => {
 
             debug("%s evaluated to %O", expression, value);
 
-            // If already rendered the clones
-            if (clones.length) {
-                clones.forEach((clone) => clone.delete());
-                clones = [];
-            }
+            // Clear the current children
+            node.clearChildren();
+
+            let cloned: RendererNode[];
 
             // If the conditional matched
             if (value) {
-                // Clone them into the DOM
-                clones = await walk(
-                    children.map((child) =>
-                        child.clone()
-                            .setParent(node.parent)
-                            .setDirty(true, false)
-                            .setChildrenDirty(true, false)
-                            .setChildrenIgnored(false)
-                    ), scope);
-                
-                node.insertBefore(...clones);
+                cloned = await walk(node.cloneConsequence(), scope);
+            } else
+            // If has an alternate
+            if (node.hasAlternative()) {
+                cloned = await walk(node.cloneAlternative(), scope);
             }
 
-            node.parent.setDirty();
+            if (cloned) {
+                // Clone it into the DOM
+                node.append(
+                    ...cloned
+                );
+            }
+
+            node.setDirty().setChildrenDirty(true, false);
         } catch(e) {
-            console.warn("[pupper.js] failed to evaluate conditional:");
+            Debugger.error("failed to evaluate conditional:");
             throw e;
         }
+
+        debug("ended");
     });
+
+    node.addEventListener("DOMNodeRemoved", removeEffect);
 });

+ 7 - 9
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 { PupperNode } from "../Node";
+import { RendererNode } from "../../../model/vdom/RendererNode";
 import dom2vdom from "@pupperjs/dom2vdom";
 
 import h from "virtual-dom/h";
@@ -14,7 +14,7 @@ import { VTree } from "virtual-dom";
 directive("html", async (node, { expression, scope }) => {
     const evaluate = evaluateLater(expression);
 
-    let replacedNode: PupperNode = null;
+    let replacement: RendererNode = null;
 
     const escape = node.getAttribute("x-escape");
     node.removeAttribute("x-escape");
@@ -23,19 +23,17 @@ directive("html", async (node, { expression, scope }) => {
         try {
             let content: string | VTree = await evaluate(scope) as string;
             
+            // If doesn't want escaping
             if (!escape) {
+                // Parse it as HTML
                 const evaluatedNode = dom2vdom(content, h) as VirtualDOM.VTree;
                 content = evaluatedNode;
             }
 
-            if (replacedNode) {
-                replacedNode.delete();
-            }
-
-            replacedNode = new PupperNode(content, node.parent, node.renderer);
-            node.insertBefore(replacedNode);
-            node.delete();
+            let insertPosition = replacement !== null ? replacement.getIndex() : node.getIndex();
 
+            replacement = new RendererNode(content, node.parent, node.renderer);
+            node.parent.children[insertPosition].replaceWith(replacement);
             node.parent.setDirty();
         } catch(e) {
             console.warn("[pupper.js] failed to set inner HTML:");

+ 24 - 27
packages/renderer/src/core/vdom/directives/Loop.ts

@@ -1,29 +1,28 @@
+import Debugger from "../../../util/Debugger";
 import { directive } from "../../../model/Directive";
 import { evaluateLater } from "../../../model/Evaluator";
 import { walk } from "../../../model/NodeWalker";
 import { effect } from "../../../model/Reactivity";
 import { IsNumeric, IsObject } from "../../../util/ObjectUtils";
-import { PupperNode } from "../Node";
+import { LoopNode } from "../nodes/LoopNode";
+
+const debug = Debugger.extend("vdom:directives:loop");
 
 /**
  * @directive x-for
  * @description Recursively renders a node's children nodes.
  */
-directive("for", async (node, { expression, scope }) => {
+directive("for", async (node: LoopNode, { expression, scope }) => {
     const loopData = parseForExpression(expression);
     const evaluate = evaluateLater(loopData.items);
 
-    // Save and remove the children
-    const children = node.children;
-    node = node.replaceWithComment();
-    node.setIgnored();
-    node.setRenderable(false);
-
-    let clones: PupperNode[] = [];
+    console.warn(node.body);
 
     const removeEffect = await effect(async () => {        
         let loopScope;
 
+        debug("running");
+
         try {
             let items = await evaluate(scope);
 
@@ -41,11 +40,8 @@ directive("for", async (node, { expression, scope }) => {
                 items = [];
             }
 
-            // Clear the older nodes if needed
-            if (clones.length) {
-                clones.forEach((clone) => clone.delete());
-                clones = [];
-            }
+            // Clear the current children
+            node.clearChildren();
 
             // Iterate over all evaluated items
             for(let index = 0; index < items.length; index++) {
@@ -65,30 +61,31 @@ directive("for", async (node, { expression, scope }) => {
                     loopScope[loopData.collection] = items;
                 }
 
-                for(let child of children) {
+                for(let child of node.body) {
+                    // Clone it
                     child = child.clone()
+                        .setParent(node)
                         .setIgnored(false)
-                        .setParent(node.parent)
-                        .setDirty(true, false)
-                        .setChildrenDirty(true, false)
-
-                        // @todo new added nodes are still being ignored because the comment is ignored
-                        // strange, bug the effect is never triggered for freshly reacted items
                         .setChildrenIgnored(false);
 
-                    node.insertBefore(child);
+                    // Append it to the children
+                    node.appendChild(child);
 
+                    // Walk through it
                     child = await walk(child, loopScope);
-                    clones.push(child);
                 }
             }
 
-            node.parent.setDirty();
+            node.setDirty().setChildrenDirty(true, false);
         } catch(e) {
-            console.warn("[pupper.js] The following information can be useful for debugging:");
-            console.warn("last scope:", loopScope);
-            console.error(e);
+            Debugger.error("failed to evaluate for loop");
+            Debugger.error("the following information can be useful for debugging:");
+            Debugger.error("last scope: %o", loopScope);
+            
+            throw e;
         }
+
+        debug("ended");
     });
 
     node.addEventListener("DOMNodeRemoved", removeEffect);

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

@@ -0,0 +1,68 @@
+import { RendererNode } from "../../../model/vdom/RendererNode";
+import { PupperNode } from "../../../model/vdom/PupperNode";
+import { Renderer } from "../Renderer";
+
+export class ConditionalNode extends PupperNode {
+    /**
+     * The consequence if the condition is met.
+     */
+    declare private consequent: RendererNode[] | undefined;
+
+    /**
+     * The alternative consequence if the condition is not met.
+     */
+    declare private alternate: RendererNode[] | undefined;
+
+    constructor(
+        node: VirtualDOM.VNode,
+        parent: RendererNode | null = null,
+        renderer: Renderer
+    ) {
+        super(node, parent, renderer);
+    }
+
+    protected initNode() {
+        super.initNode();
+
+        this.consequent = this.children.find((child) => child.getAttribute("x-if-cond") === "consequent")?.delete().children;
+        this.alternate = this.children.find((child) => child.getAttribute("x-if-cond") === "alternate")?.delete().children;
+
+        // If has no consequent
+        if (!this.consequent) {
+            throw new Error("Found a conditional node without consequence.");
+        }
+    }
+
+    public clone() {
+        const clone = new ConditionalNode(this.node, this.parent, this.renderer);
+
+        clone.consequent = this.cloneConsequence();
+        clone.alternate = this.cloneAlternative();
+
+        return clone;
+    }
+
+    public hasConsequence() {
+        return this.consequent !== undefined;
+    }
+
+    public getConsequence() {
+        return this.consequent;
+    }
+
+    public hasAlternative() {
+        return this.alternate !== undefined;
+    }
+
+    public getAlternative() {
+        return this.alternate;
+    }
+
+    public cloneConsequence() {
+        return this.consequent.map((child) => child.clone());
+    }
+
+    public cloneAlternative() {
+        return this.alternate?.map((child) => child.clone());
+    }
+}

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

@@ -0,0 +1,28 @@
+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());
+
+        return clone;
+    }
+
+    protected initNode() {
+        super.initNode();
+
+        this.body = this.children.map((child) => child.delete());
+    }
+}

+ 8 - 1
packages/renderer/src/index.ts

@@ -10,6 +10,8 @@ import "./core/vdom/directives/EventHandler";
 import "./core/vdom/directives/HTML";
 import "./core/vdom/directives/Component";
 
+import * as Magics from "./model/Magics";
+
 export default class Pupper {
     /**
      * The default component class
@@ -26,6 +28,11 @@ export default class Pupper {
      */
     public static $store: Record<string, any> = {};
 
+    /**
+     * A handler for all pupper.js magics.
+     */
+    public static $magics: Record<string, any> = Magics;
+
     /**
      * Sets a state in the global store.
      * @param name The state key.
@@ -34,7 +41,7 @@ export default class Pupper {
      */
     public static store(name: string, value?: any) {
         return value !== undefined ? this.$store[name] : this.$store[name] = value;
-    };
+    }
     
     /**
      * The Pupper global state.

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

@@ -1,5 +1,5 @@
-import { PupperNode } from "../core/vdom/Node";
-import { Renderer } from "../core/vdom/Renderer";
+import { RendererNode } from "./vdom/RendererNode";
+import { Renderer, TRendererNodes } from "../core/vdom/Renderer";
 
 export type TScope = Record<string, string | boolean | number>;
 
@@ -34,7 +34,7 @@ const currentHandlerStackKey = Symbol();
 const pupperAttrRegex = /^x-([^:^.]+)\b/;
 
 type TDirectiveCallback = (
-    node: PupperNode,
+    node: TRendererNodes,
     data: {
         renderer: Renderer;
         scope: TScope;
@@ -52,7 +52,7 @@ export function directive(attribute: TDirectives, callback: TDirectiveCallback)
  * @param node The node to be evaluated.
  * @returns 
  */
-export function directives(node: PupperNode, scope: TScope) {
+export function directives(node: RendererNode, scope: TScope) {
     let transformedAttributeMap: Record<string, string> = {};
 
     const attributes = node.getAttributesAndProps();
@@ -80,7 +80,7 @@ export function directives(node: PupperNode, scope: TScope) {
         });
 }
 
-export function getDirectiveHandler(node: PupperNode, directive: IDirective, scope: TScope) {
+export function getDirectiveHandler(node: RendererNode, directive: IDirective, scope: TScope) {
     let noop = async () => {};
     let handler = directiveHandler[directive.type] || noop;
 

+ 8 - 0
packages/renderer/src/model/Magics.ts

@@ -0,0 +1,8 @@
+/**
+ * Executes a callback in the next tick.
+ * @param callback The callback to be executed.
+ * @returns 
+ */
+export function $nextTick(callback: CallableFunction) {
+    return this.$component.renderer.nextTick(callback);
+}

+ 103 - 48
packages/renderer/src/model/NodeWalker.ts

@@ -1,65 +1,110 @@
-import { PupperNode } from "../core/vdom/Node";
+import { RendererNode } from "./vdom/RendererNode";
 import { directives } from "./Directive";
 
 import * as Debugger from "../util/Debugger";
+import { TRendererNodes } from "../core/vdom/Renderer";
 
 export enum ENodeWalkResult {
-    PREVIOUS,
+    REPLACED,
     NEXT,
-    REMOVE
+    REMOVE,
+    SKIP
 }
 
-export async function walk<TNode extends PupperNode | PupperNode[]>(nodes: TNode, scope: any = null): Promise<TNode> {
+interface IWalkResult {
+    result: ENodeWalkResult.NEXT | ENodeWalkResult.REMOVE | ENodeWalkResult.REPLACED,
+    node?: RendererNode | RendererNode[]
+}
+
+interface ISkipWalkResult {
+    result: ENodeWalkResult.SKIP,
+    amount: number
+}
+
+type TWalkResult = ISkipWalkResult | IWalkResult;
+
+let recursionDetector = 0;
+
+/**
+ * Walks through a list of nodes, applying directives to them.
+ * @param nodes The nodes to be walked.
+ * @param scope The scope for this nodes.
+ * @returns 
+ */
+export async function walk<TNode extends TRendererNodes | TRendererNodes[]>(nodes: TNode, scope: any = null): Promise<TNode> {
+    // If it's a single node, walk it
     if (!Array.isArray(nodes)) {
-        return (await walkNode(nodes as PupperNode, scope)).node as TNode;
+        const result = await walkNode(nodes as RendererNode, scope);
+
+        if (result.result === ENodeWalkResult.SKIP) {
+            return nodes;
+        }
+
+        return result.node as TNode;
     }
 
     let count = nodes.length;
     let i = 0;
 
     while(i < count) {
-        const { node, result } = await walkNode(nodes[i], scope);
+        recursionDetector++;
+
+        if (recursionDetector >= 100000) {
+            Debugger.error("pupper.js detected a possible node walking recursion.");
+            break;
+        }
+
+        const currentNode = nodes[i];
+        const walkResult = await walkNode(currentNode, scope);
 
+        // If it's skiping the node
+        if (walkResult.result === ENodeWalkResult.SKIP) {
+            i += Math.max(walkResult.amount, 1);
+        } else
         // If the result is to remove the node
-        if (result === ENodeWalkResult.REMOVE) {
+        if (walkResult.result === ENodeWalkResult.REMOVE) {
             // Remove it and continue
             nodes.splice(i, 1);
             count = nodes.length;
+        } else
+        // If it was replaced
+        if (walkResult.result === ENodeWalkResult.REPLACED) {
+            Debugger.warn("%s %O was replaced with %O", currentNode.tag, currentNode.getAttributesAndProps(), walkResult.node);
 
-            continue;
-        }
+            // Calculate the replacement length
+            const repl = Array.isArray(walkResult.node) ? walkResult.node : [walkResult.node];
+
+            // Replace it from the nodes array
+            nodes.splice(i, 1, ...repl);
 
+            // Update the total count
+            count = nodes.length;
+        } else
         // If it's an array
-        if (Array.isArray(node)) {
+        if (Array.isArray(walkResult.node)) {
             // Append it to the nodes array
-            nodes.splice(i, 1, ...await walk(node, scope));
+            nodes.splice(i, 0, ...walkResult.node);
             count = nodes.length;
+
+            i += walkResult.node.length;
         } else {
-            // If it's going back
-            if (result === ENodeWalkResult.PREVIOUS) {
-                // Parse it again
-                i--;
-                continue;
-            }
-
-            // If the node doesn't exists or is being ignored
-            if (!node.exists() || node.isBeingIgnored()) {
-                i++;
-                continue;
-            }
-
-            nodes[i] = node;
-            i++;          
-        }        
+            // Replace the node with the new one
+            nodes[i++] = walkResult.node;
+        }
     }
 
+    recursionDetector = 0;
+
     return nodes;
 }
 
-async function walkNode(node: PupperNode | undefined, scope: any): Promise<{
-    result: ENodeWalkResult,
-    node?: PupperNode | PupperNode[]
-}> {
+/**
+ * Walks through a single node.
+ * @param node The node to be walked.
+ * @param scope The scope to this node.
+ * @returns 
+ */
+async function walkNode(node: TRendererNodes | undefined, scope: any): Promise<TWalkResult> {
     // If it's an invalid node
     if (!node) {
         // Ignore it
@@ -72,18 +117,17 @@ async function walkNode(node: PupperNode | undefined, scope: any): Promise<{
 
     // Ignore if it's a string
     if (typeof node?.node === "string") {
-        Debugger.warn("node is a plain string");
+        Debugger.warn("\"%s\" is a plain string", node.node);
         Debugger.endGroup();
 
         return {
             node,
             result: ENodeWalkResult.NEXT
         };
-    }
-
+    } else
     // Ignore if it's being ignored
     if (node.isBeingIgnored()) {
-        Debugger.warn("node is being ignored");
+        Debugger.warn("%s is being ignored", node.tag);
         Debugger.endGroup();
 
         return {
@@ -92,32 +136,28 @@ async function walkNode(node: PupperNode | undefined, scope: any): Promise<{
         };
     }
 
+    // Apply all directives for it
     for(let handle of directives(node, scope)) {
         await handle();
     }
 
-    // Set it as non-dirty.
-    node.setDirty(false);
-
     // If node was replaced, stop parsing
     if (node.wasReplaced()) {
-        Debugger.warn("node was replaced with %O", node.getReplacement());
         Debugger.endGroup();
 
         return {
             node: node.getReplacement(),
-            result: ENodeWalkResult.NEXT
+            result: ENodeWalkResult.REPLACED
         };
-    }
-
+    } else
     // If the node was removed, stop parsing
     if (!node.exists()) {
-        Debugger.warn("node was removed");
+        Debugger.warn("%s was removed", node.tag);
         Debugger.endGroup();
 
         return {
-            node,
-            result: ENodeWalkResult.NEXT
+            amount: 1,
+            result: ENodeWalkResult.SKIP
         };
     }
 
@@ -128,10 +168,25 @@ async function walkNode(node: PupperNode | undefined, scope: any): Promise<{
 
     // If it's non-renderable
     if (!node.isRenderable()) {
-        Debugger.warn("node is not renderable");
+        // If it's a $ pupper node
+        if (node.isPupperNode()) {
+            Debugger.warn("found a pupper tag %O, replacing with its children", node);
+            Debugger.endGroup();
+
+            return {
+                amount: node.children.length,
+                result: ENodeWalkResult.SKIP
+            };
+        }
+        
+        Debugger.warn("%s is not renderable", node.tag);
+        Debugger.endGroup();
 
-        // Allow parsing its children but prevent itself from being rendered.
-        return undefined;
+        // Allow parsing its children, but prevent itself from being rendered.
+        return {
+            result: ENodeWalkResult.SKIP,
+            amount: 1
+        };
     }
 
     Debugger.endGroup();

+ 17 - 4
packages/renderer/src/model/Reactivity.ts

@@ -10,17 +10,30 @@ const debug = Debugger.extend("reactivity");
 
 const ProxySymbol = Symbol("$Proxy");
 
+/**
+ * Merges multiple proxies / objects into one.
+ * @param objects All proxies / objects to be merged
+ * @returns 
+ */
+export function mergeProxies(objects: Record<string, any>[]) {
+    return reactive(
+        objects.reduce((carrier, obj) => {
+            for(let key in obj) {
+                carrier[key] = obj[key];
+            }
+
+            return carrier;
+        }, {})
+    );
+}
+
 export async function effect(effect: TEffect) {
     currentEffect = effect;
 
-    debug("processing effect %O", effect);
-
     // Calling the effect immediately will make it
     // be detected and registered at the effects handler.
     await effect();
 
-    debug("effect was processed");
-
     currentEffect = null;
 
     return () => {

+ 0 - 30
packages/renderer/src/model/VirtualDom.ts

@@ -1,30 +0,0 @@
-import h from "virtual-dom/h";
-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: (VirtualDOM.VTree)[]) {
-    const cloned: (VirtualDOM.VTree)[] = [];
-
-    for(let node of nodes) {
-        cloned.push(cloneNode(node));
-    }
-
-    return cloned;
-}
-
-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(String(node));
-    }
-
-    return h(node.tagName, node.properties, node.children);
-}

+ 33 - 0
packages/renderer/src/model/vdom/PupperNode.ts

@@ -0,0 +1,33 @@
+import { Renderer } from "../../core/vdom/Renderer";
+import { RendererNode } from "./RendererNode";
+
+export class PupperNode extends RendererNode {
+    constructor(
+        node: VirtualDOM.VNode,
+        parent: RendererNode | null = null,
+        renderer: Renderer
+    ) {
+        super(node, parent, renderer);
+    }
+
+    public isRenderable(): boolean {
+        return false;
+    }
+
+    public toVNode() {
+        let node: (VirtualDOM.VTree | string)[] = [];
+
+        this.children.forEach((child) => {
+            const result = child.toVNode();
+
+            if (Array.isArray(result)) {
+                node.push(...result);
+                return;
+            }
+
+            node.push(result);
+        })
+        
+        return node;
+    }
+}

+ 210 - 38
packages/renderer/src/core/vdom/Node.ts → packages/renderer/src/model/vdom/RendererNode.ts

@@ -1,4 +1,4 @@
-import { Renderer } from "./Renderer";
+import { Renderer } from "../../core/vdom/Renderer";
 
 import h from "virtual-dom/h";
 import diff from "virtual-dom/diff";
@@ -8,36 +8,80 @@ import VText from "virtual-dom/vnode/vtext";
 
 import Debugger from "../../util/Debugger";
 
+import { LoopNode } from "../../core/vdom/nodes/LoopNode";
+import { ConditionalNode } from "../../core/vdom/nodes/ConditionalNode";
+import VNode from "virtual-dom/vnode/vnode";
+
 const debug = Debugger.extend("vdom:node");
 
-const Hook = (callback: CallableFunction) => {
-    const hook = function() {};
-    hook.prototype.hook = callback;
+interface IHookFn extends Function {
+    hook?: CallableFunction;
+    unhook?: CallableFunction
+}
+
+/**
+ * Creates a new virtual DOM hook.
+ * @param props The hook callbacks.
+ * @returns 
+ */
+function Hook<TProps extends {
+    hook?: CallableFunction,
+    unhook?: CallableFunction
+}>(props: TProps): IHookFn {
+    const hook: IHookFn = function() { };
+
+    hook.prototype.hook = props.hook;
+    hook.prototype.unhook = props.unhook;
 
     // @ts-ignore
     return new hook();
 };
 
-export class PupperNode<TNode extends VirtualDOM.VTree = any> {
-    public children: PupperNode[] = [];
+export class RendererNode<TNode extends VirtualDOM.VTree = any> {
+    public children: RendererNode[] = [];
 
-    public properties: Record<string, string | boolean | number> = {};
+    public properties: Record<string, string | boolean | number | IHookFn> = {};
     public attributes: Record<string, string | boolean | number> = {};
     public eventListeners: Record<string, EventListenerOrEventListenerObject[]> = {};
 
+    /**
+     * The node tag name.
+     */
     public tag: string;
 
+    /**
+     * If the node is being ignored.
+     */
     private ignore: boolean = false;
+
+    /**
+     * If the node is dirty.
+     */
     private dirty: boolean = true;
+
+    /**
+     * If it's currently patching this node.
+     */
     private patching: boolean = false;
+
+    /**
+     * If the node can be rendered.
+     */
     private renderable: boolean = true;
-    public replacedWith: PupperNode[] = null;
 
+    /**
+     * The node that replaced this node, if any.
+     */
+    public replacedWith: RendererNode[] = null;
+
+    /**
+     * The rendered DOM element for this node.
+     */
     public element: Element = null;
 
     constructor(
         public node: TNode | string,
-        public parent: PupperNode = null,
+        public parent: RendererNode | null = null,
         public renderer: Renderer
     ) {
         this.initNode();
@@ -47,7 +91,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Initializes the node data.
      * @returns 
      */
-    private initNode() {
+    protected initNode() {
         if (typeof this.node === "string") {
             return;
         }
@@ -83,11 +127,23 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
             }
 
             if ("children" in this.node) {
-                this.children = this.node.children.map((child) => new PupperNode(child, this, this.renderer));
+                this.children.push(
+                    ...this.node.children.map((child) =>
+                        Renderer.createNode(child, this, this.renderer)
+                    )
+                )
             }
         }
     }
 
+    /**
+     * Checks if it's an internal pupper node.
+     * @returns 
+     */
+    public isPupperNode() {
+        return this.tag === "$";
+    }
+
     /**
      * Checks if this node element has been rendered.
      * @returns 
@@ -177,7 +233,15 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * @returns 
      */
     public isBeingIgnored() {
-        return this.ignore || !this.dirty;
+        return this.ignore;
+    }
+
+    /**
+     * Determines if the node is dirty.
+     * @returns 
+     */
+    public isDirty() {
+        return this.dirty;
     }
 
     /**
@@ -185,7 +249,8 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * @returns 
      */
     public isRenderable() {
-        return this.renderable;
+        // Pupper tags aren't renderable
+        return this.tag !== "$" && this.renderable;
     }
 
     /**
@@ -286,16 +351,16 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * @param nodes The nodes to replace the current one.
      * @returns 
      */
-    public replaceWith<TNode extends PupperNode | VirtualDOM.VTree | string>(...nodes: TNode[]) {
+    public replaceWith<TNode extends RendererNode | VirtualDOM.VTree | string>(...nodes: TNode[]) {
         if (!this.parent) {
             return nodes;
         }
 
         const replacement = nodes.map((node) =>
-            !(node instanceof PupperNode) ?
-                new PupperNode(node, this.parent, this.renderer) :
+            !(node instanceof RendererNode) ?
+                Renderer.createNode(node, this.parent, this.renderer) :
                 node
-        ) as PupperNode[];
+        ) as RendererNode[];
 
         this.parent.children.splice(
             this.getIndex(),
@@ -313,8 +378,8 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Replaces the current node with a comment.
      * @returns 
      */
-    public replaceWithComment() {
-        const comment = new PupperNode(h.c("!"), this.parent, this.renderer);
+    public replaceWithComment(contents: string = "") {
+        const comment = new RendererNode(h.c(contents), this.parent, this.renderer);
 
         this.replaceWith(comment);
 
@@ -367,7 +432,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * @param parent The new node parent.
      * @returns 
      */
-    public setParent(parent: PupperNode) {
+    public setParent(parent: RendererNode) {
         this.parent = parent;
 
         // Update the children parents
@@ -382,7 +447,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Insert a list of nodes before the current node.
      * @param nodes The list of nodes to be inserted.
      */
-    public insertBefore(...nodes: PupperNode[]) {
+    public insertBefore(...nodes: RendererNode[]) {
         this.parent.children.splice(
             this.getIndex() - 1,
             0,
@@ -394,7 +459,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Insert a list of nodes after the current node.
      * @param nodes The list of nodes to be inserted.
      */
-    public insertAfter(...nodes: PupperNode[]) {
+    public insertAfter(...nodes: RendererNode[]) {
         this.parent.children.splice(
             this.getIndex() + 1,
             0,
@@ -406,7 +471,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Appends a node to the children nodes.
      * @param node The node to be appended.
      */
-    public appendChild(node: PupperNode) {
+    public appendChild(node: RendererNode) {
         this.children.push(node);
     }
 
@@ -414,7 +479,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Appends a list of node to the children nodes.
      * @param nodes The nodes to be appended.
      */
-    public append(...nodes: PupperNode[]) {
+    public append(...nodes: RendererNode[]) {
         this.children.push(...nodes);
     }
 
@@ -426,20 +491,36 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
             this.getIndex(),
             1
         );
+
+        return this;
+    }
+
+    /**
+     * Clears the node children nodes.
+     */
+    public clearChildren() {
+        this.children = [];
+        return this;
     }
 
     /**
      * Clones the current node into a new one.
      * @returns 
      */
-    public clone() {
+    public clone(): RendererNode {
+        // If it's a comment
+        if (this.node instanceof VComment) {
+            // Clone it
+            return new RendererNode(h.c(this.node.comment), this.parent, this.renderer);
+        }
+
         const clonedNode = this.isString() ? this.node : h(this.tag, {
             attrs: { ...this.attributes },
             props: { ...this.properties },
             on: {... this.eventListeners }
         }, []);
 
-        const clone = new PupperNode(clonedNode as TNode, this.parent, this.renderer);
+        const clone = Renderer.createNode(clonedNode as TNode, this.parent, this.renderer);
         clone.children = this.children.map((child) => child.clone());
 
         return clone;
@@ -450,7 +531,7 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * @returns 
      */
     public getRoot() {
-        let node: PupperNode = this;
+        let node: RendererNode = this;
 
         while(node.parent !== null) {
             node = node.parent;
@@ -459,6 +540,42 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
         return node;
     }
 
+    /**
+     * Determines if any parent node is in patching state.
+     * @returns 
+     */
+    private hasParentPatching() {
+        let parent = this.parent;
+
+        while(parent !== null) {
+            if (parent.patching) {
+                return true;
+            }
+
+            parent = parent.parent;
+        }
+
+        return false;
+    }
+
+    /**
+     * Retrieves the closest rendered parent element.
+     * @returns 
+     */
+    public getClosestRenderedParent() {
+        let parent = this.parent;
+
+        while(parent !== null) {
+            if (parent.wasRendered()) {
+                return parent;
+            }
+
+            parent = parent.parent;
+        }
+
+        return undefined;
+    }
+
     /**
      * Enqueues a patch to the internal VDOM element.
      */
@@ -469,10 +586,30 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
         }
 
         if (this.wasRendered()) {
+            // Ignore if the parent is already patching, because it will be included
+            if (this.hasParentPatching()) {
+                debug("parent is already patching, will ignore subsequent patches.");
+                return;
+            }
+
             debug("element was rendered, will be patched");
 
             this.patching = true;
             this.renderer.singleNextTick(this.doPatch.bind(this));
+        } else
+        // If already rendered the renderer for the first time
+        if (this.renderer.rendered) {
+            // If has a rendered parent element
+            const renderedParent = this.getClosestRenderedParent();
+
+            if (renderedParent && !renderedParent.patching) {
+                debug("closest parent %O will patch %O", renderedParent, this);
+
+                // Call it to patch the element
+                renderedParent.setDirty();
+            } else {
+                debug("closest parent %O of %O is already patching.", renderedParent, this);
+            }
         }
     }
 
@@ -482,9 +619,18 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
     private doPatch() {
         const diffs = diff(this.node as any, this.toVNode() as any);
 
-        this.element = patch(this.element, diffs);
-        this.patching = false;
-        this.dirty = false;
+        debug("applying patch %O to %O", diffs, this);
+
+        try {
+            this.element = patch(this.element, diffs);
+            this.patching = false;
+            this.dirty = false;
+        } catch(e) {
+            Debugger.error("an exception ocurred while patching node %O:", this);
+            throw e;
+        }
+
+        debug("patched node %O into element %O", this, this.element);
     }
 
     /**
@@ -506,22 +652,30 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
      * Converts the current node into a virtual DOM node.
      * @returns 
      */
-    public toVNode(): VirtualDOM.VTree | string {
+    public toVNode(): VirtualDOM.VTree | string | (VirtualDOM.VTree | string)[] {
+        // 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 $p_create hook yet
+        if (!("$p_create" in this.properties)) {
+            // Create the hook
+            this.properties["$p_create"] = Hook({
+                hook: (node: Element) => {
+                    this.onElementCreated(node);
+                }
+            });
+        }
+
         const properties: Record<string, any> = {
             ...this.attributes,
-            ...this.properties,
-            $p_create: Hook((node: Element) => {
-                this.onElementCreated(node);
-            })
+            ...this.properties
         };
 
         // Rename the "class" attribute
@@ -530,10 +684,28 @@ export class PupperNode<TNode extends VirtualDOM.VTree = any> {
             delete properties.class;
         }
 
+        let finalChild: (VirtualDOM.VTree | string)[] = [];
+
+        // Iterate over the children
+        this.children
+            // Only renderable children
+            .filter((child) => child.isRenderable() || child.isPupperNode())
+            // Into virtual nodes
+            .forEach((child) => {
+                const result = child.toVNode();
+
+                if (Array.isArray(result)) {
+                    finalChild.push(...result);
+                    return;
+                }
+
+                finalChild.push(result);
+            });
+
         this.node = h(
             this.tag,
             properties,
-            this.children.map((child) => child.toVNode())
+            finalChild
         ) as TNode;
 
         return this.node as VirtualDOM.VTree;

+ 55 - 3
packages/renderer/src/util/Debugger.ts

@@ -1,5 +1,42 @@
 const debuggerModule = require("debug");
 
+enum EConsoleLevel {
+    DEBUG,
+    LOG,
+    INFO,
+    WARN,
+    ERROR
+}
+
+let consoleType: EConsoleLevel = null;
+
+debuggerModule.log = (...args: any[]) => {
+    switch(consoleType) {
+        default:
+        case EConsoleLevel.LOG:
+            console.log(...args);
+        break;
+
+        case EConsoleLevel.DEBUG:
+            console.debug(...args);
+        break;
+
+        case EConsoleLevel.INFO:
+            console.info(...args);
+        break;
+
+        case EConsoleLevel.WARN:
+            console.warn(...args);
+        break;
+
+        case EConsoleLevel.ERROR:
+            console.error(...args);
+        break;
+    }
+
+    consoleType = null;
+};
+
 type FLogger = (message: string, ...args: any[]) => void;
 
 export let enabled = localStorage.getItem("pupperjs:debug") === "1";
@@ -27,6 +64,7 @@ export function extend(namespace: string) {
  * @returns 
  */
 export function debug(message: string, ...args: any[]) {
+    consoleType = EConsoleLevel.DEBUG;
     return logger(message, ...args);
 }
 
@@ -37,6 +75,7 @@ export function debug(message: string, ...args: any[]) {
  * @returns 
  */
 export function info(message: string, ...args: any[]) {
+    consoleType = EConsoleLevel.INFO;
     return logger("%c" + message, ...["color: aqua", ...args])
 }
 
@@ -46,8 +85,20 @@ export function info(message: string, ...args: any[]) {
  * @param args Any arguments to be passed to the message sprintf.
  * @returns 
  */
- export function warn(message: string, ...args: any[]) {
-    return logger("%c" + message, ...["color: yellow", ...args])
+export function warn(message: string, ...args: any[]) {
+    consoleType = EConsoleLevel.WARN;
+    return logger(message, ...args)
+}
+
+/**
+ * Prints a debug error message to the console.
+ * @param message The message to be displayed, in sprintf format.
+ * @param args Any arguments to be passed to the message sprintf.
+ * @returns 
+ */
+export function error(message: string, ...args: any[]) {
+    consoleType = EConsoleLevel.ERROR;
+    return logger(message, ...args)
 }
 
 /**
@@ -104,7 +155,8 @@ const Debugger = {
     info,
     debug,
     extend,
-    warn
+    warn,
+    error
 } as const;
 
 export default Debugger;

+ 2 - 12
test/index.js

@@ -1,14 +1,4 @@
 import Template from "./templates/template.pupper";
 
-(async function() {
-    window.component = Template;
-    await Template.mount(document.getElementById("app"));
-
-    Template.puppies.push({
-        id: 3,
-        title: "Wow, a shibe!",
-        description: "Cute shiberino!!!",
-        thumbnail: "https://media.istockphoto.com/photos/happy-shiba-inu-dog-on-yellow-redhaired-japanese-dog-smile-portrait-picture-id1197121742?k=20&m=1197121742&s=170667a&w=0&h=SDkUmO-JcBKWXl7qK2GifsYzVH19D7e6DAjNpAGJP2M=",
-        shibe: true
-    });
-}());
+window.component = Template;
+Template.mount(document.getElementById("app"));

+ 24 - 8
test/templates/template.pupper

@@ -1,11 +1,10 @@
 template
-    .container
+    .container-fluid
         //- Stylesheets
-        link(href="https://getbootstrap.com/docs/4.0/dist/css/bootstrap.min.css", rel="stylesheet")
-        link(href="https://getbootstrap.com/docs/4.0/examples/cover/cover.css", rel="stylesheet")
+        link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootswatch@4.5.2/dist/litera/bootstrap.min.css", integrity="sha384-enpDwFISL6M3ZGZ50Tjo8m65q06uLVnyvkFO3rsoW0UC15ATBFz3QEhr3hmxpYsn", crossorigin="anonymous")
 
         .text-center
-            .cover-container.d-flex.h-100.p-3.mx-auto.flex-column
+            .d-flex.p-3.mx-auto.flex-column
                 header.masthead.mb-auto 
                     .inner
                         h3.masthead-brand=page.title
@@ -16,6 +15,8 @@ template
 
                     p.lead=page.lead
 
+                    hr.my-5
+
                     .row.mt-5.justify-content-around.align-items-center
                         if puppies === undefined || puppies.length === 0
                             ="Oh noe! No puppies to show :("
@@ -23,7 +24,7 @@ template
                             //- Render the puppies and share the onClickPuppy method with it
                             each index, puppy in puppies
                                 .col-5.mb-5
-                                    .puppy.card.px-0(:data-pop="index", :data-id="puppy.id", @click="onClickPuppy(puppy)").text-dark
+                                    .puppy.card.px-0.text-dark(:data-pop="index", :data-id="puppy.id", @click="onClickPuppy(puppy)")
                                         img.card-img-top(:src="puppy.thumbnail", crossorigin="auto")
 
                                         .card-header
@@ -33,12 +34,16 @@ template
                                         .card-body
                                             !=puppy.description
 
+                                            //- Displays a custom message if it's a shibe
                                             if puppy.shibe === true
                                                 p.text-warning="shibbe!!!"
 
+                                            //- Displays a puppy properties
                                             if puppy.properties
-                                                each property in puppy.properties
-                                                    span.badge.badge-info=property
+                                                .row.justify-content-center.mt-3
+                                                    each property in puppy.properties
+                                                        .col
+                                                            span.badge.badge-info.w-100=property
 
                     div 
                         |Testing slots: 
@@ -85,4 +90,15 @@ implementation
 
     when#mounted
         console.log("The component was mounted.");
-        ImportedComponent.mount(this.$slots.slot);
+
+        this.$nextTick(() => {
+            this.puppies.push({
+                id: 3,
+                title: "Wow, a shibe!",
+                description: "Cute shiberino!!!",
+                thumbnail: "https://media.istockphoto.com/photos/happy-shiba-inu-dog-on-yellow-redhaired-japanese-dog-smile-portrait-picture-id1197121742?k=20&m=1197121742&s=170667a&w=0&h=SDkUmO-JcBKWXl7qK2GifsYzVH19D7e6DAjNpAGJP2M=",
+                shibe: true
+            });
+        });
+
+        //ImportedComponent.mount(this.$slots.slot);