Pārlūkot izejas kodu

added event binding

Matheus Giovani 3 gadi atpakaļ
vecāks
revīzija
fbcdb9fe43

+ 8 - 2
modules/webpack/index.js

@@ -11,7 +11,13 @@ const pupper = require("../..");
  * @param {import("../../types/pupper").Compiler.Options} options Any options to be passed to the pupper compiler
  * @returns {String}
  */
-module.exports = (source, options) => {
-    const contents = pupper.compileToStringSync(source, options);
+module.exports = function(source, options) {
+    const contents = pupper.compileToStringSync(source, {
+        ...options,
+        pug: {
+            filename: this.resourcePath
+        }
+    });
+
     return contents;
 };

+ 5 - 3
package.json

@@ -2,13 +2,14 @@
   "name": "@pupperjs/core",
   "version": "1.0.0",
   "description": "A reactive template engine based in pugjs",
-  "main": "out/index.js",
+  "main": "out/",
   "author": "Matheus Giovani <matheus@ad3com.com.br>",
   "license": "AGPL-3.0",
   "private": false,
   "scripts": {
-    "watch": "tsc -w",
-    "test": "webpack",
+    "watch": "npm-run-all -p -r watch-core watch-test",
+    "watch-core": "tsc -watch",
+    "watch-test": "webpack",
     "test-node": "nodemon --ignore test/out test/node"
   },
   "dependencies": {
@@ -22,6 +23,7 @@
     "@types/pug": "^2.0.5",
     "debug": "^4.3.2",
     "js-beautify": "^1.14.0",
+    "npm-run-all": "^4.1.5",
     "source-map-loader": "^3.0.0",
     "tsc": "^2.0.3",
     "typescript": "^4.4.2",

+ 14 - 0
src/core/App.ts

@@ -0,0 +1,14 @@
+export interface AppSettings {
+    /**
+     * The target element related to this application
+     */
+    el?: HTMLElement | string
+}
+
+export class App {
+    constructor(
+        protected settings: AppSettings
+    ) {
+
+    }
+}

+ 2 - 1
src/core/Compiler.ts

@@ -1,7 +1,7 @@
 import pug from "pug";
 import fs from "fs";
 import path from "path";
-import Renderer from "./Renderer";
+import { Renderer } from "./Renderer";
 import Lexer from "./compiler/Lexer";
 import Parser from "./compiler/Parser";
 
@@ -42,6 +42,7 @@ export default class PupperCompiler {
             return new Renderer(
                 pug.compile(template, {
                     name: "pupper",
+                    filename: "pupper.pug",
                     compileDebug: options.debug || false,
                     // Always use self to prevent conflicts with other compilers
                     self: true,

+ 53 - 0
src/core/Component.ts

@@ -0,0 +1,53 @@
+import { Renderer } from "./Renderer";
+import type { CompiledTemplate } from "./Renderer";
+
+export interface ComponentSettings {
+    /**
+     * The template to this component
+     */
+    template: CompiledTemplate;
+
+    /**
+     * The data to be passed to this component
+     */
+    data?: Record<any, any>;
+
+    /**
+     * The component methods
+     */
+    methods?: Record<string, Function>;
+}
+
+export class Component {
+    /**
+     * The renderer related to this component
+     */
+    public renderer: Renderer;
+
+    public data: Record<any, any>;
+    public methods: Record<string, Function>;
+
+    constructor(
+        protected settings: ComponentSettings
+    ) {
+        this.renderer = new Renderer(this.settings.template, this.settings.data);
+        this.data = this.renderer.data;
+        this.methods = this.renderer.methods;
+    }
+
+    /**
+     * Renders the component
+     */
+    public async render() {
+        return this.renderer.render();
+    }
+
+    /**
+     * Renders the component into a HTML element
+     * @param element The HTML element that will receive the element
+     * @returns 
+     */
+    public async renderTo(element: string | HTMLElement) {
+        return this.renderer.renderTo(element);
+    }
+}

+ 50 - 3
src/core/Renderer.ts

@@ -6,6 +6,8 @@ import { Reactive } from "./renderer/Reactive";
 
 const debug = require("debug")("pupperjs:renderer");
 
+export type CompiledTemplate = pug.compileTemplate;
+
 export interface NodeOptions {
     /**
      * Any prefix to be passed to the dot notation
@@ -24,7 +26,9 @@ export enum NodePreparationResult {
     FAILED
 }
 
-export default class PupperRenderer {
+type K = keyof HTMLElementEventMap;
+
+export class Renderer {
     private static SYNTAX_REGEX = /(?: +)?\@p\:(?<command>.+)\((?<property>.+?)\)(?: +)?/;
 
     /**
@@ -37,6 +41,12 @@ export default class PupperRenderer {
      */
     public data: ProxyHandler<Reactive.ReactiveData> = {};
 
+    /**
+     * The methods to be attributed with the elements
+     */
+    // @ts-ignore
+    public methods: Record<string, (this: HTMLElement, ev: HTMLElementEventMap[K]) => any> = {};
+
     /**
      * The DOM element that will receive all children
      */
@@ -147,7 +157,7 @@ export default class PupperRenderer {
      * @param data The new template data
      * @returns The proxied data object
      */
-    public setData<T extends object>(data: T): ProxyHandler<T> {
+    public setData<T extends Record<any, any>>(data: T): ProxyHandler<T> {
         // Prepare the proxy
         const proxy = {
             get(target: Record<any, any>, key: string): any {
@@ -249,7 +259,7 @@ export default class PupperRenderer {
         command = command.trim();
 
         // Parse it
-        const parsed = command.match(PupperRenderer.SYNTAX_REGEX);
+        const parsed = command.match(Renderer.SYNTAX_REGEX);
         
         if (parsed === null) {
             throw new Error("Failed to parse command \"" + command + "\"");
@@ -389,10 +399,47 @@ export default class PupperRenderer {
             // Skip children preparation
             return NodePreparationResult.SKIP_CHILD;
         } else
+        // Check if it's an if
+        if (element.tagName === "P:IF") {
+            const condition = element.getAttribute("condition").match(/\@p\:conditional\((.+?)\)/)[1];
+            const then = element.querySelector("p\\:then")?.innerHTML;
+            const otherwise = element.querySelector("p\\:else")?.innerHTML;
+
+            const comment = document.createComment(" ");
+            element.replaceWith(comment);
+
+            const regex = /\(?(?<var>[\w\."'()]+)(\s*[=!]\s*)?\)?/g;
+
+            let variables: string[] = [];
+            let variable;
+
+            while(variable = regex.exec(condition)) {
+                variables.push(variable[1]);
+            }
+
+            variables.forEach((variable) => {
+                this.reactor.addReactivity(comment, options.pathPrefix + variable, "if", {
+                    condition, then, otherwise
+                });
+            });
+        } else
         // Check if it's an HTML element
         if (element instanceof HTMLElement) {
             // Iterate over all the attributes
             element.getAttributeNames().forEach((attr) => {
+                // Check if it's a bind attribute
+                if (attr.startsWith("@p:bind:")) {
+                    this.reactor.bindEvent(
+                        element,
+                        attr.replace("@p:bind:", ""),
+                        element.getAttribute(attr)
+                    );
+
+                    element.removeAttribute(attr);
+
+                    return;
+                }
+
                 // Check if it doesn't start with our identifier
                 if (element.getAttribute(attr).indexOf("@p:") === -1) {
                     return;

+ 7 - 1
src/core/compiler/Lexer.ts

@@ -2,11 +2,17 @@ import type PugLexer from "pug-lexer";
 import type Token from "./lexer/Token";
 import ForEach from "./lexer/tokens/ForEach";
 import Property from "./lexer/tokens/Property";
+import IfToken from "./lexer/tokens/If";
+import Bind from "./lexer/tokens/Bind";
+import Import from "./lexer/tokens/Import";
 
 export default class Lexer {
     public static LexerRegexes: typeof Token[] = [
         Property,
-        ForEach
+        ForEach,
+        IfToken,
+        Bind,
+        Import
     ];
 
     /**

+ 4 - 4
src/core/compiler/Parser.ts

@@ -17,12 +17,12 @@ export interface PugBlock {
 
 export interface PugNode extends Record<string, any> {
     type: string,
-    start: number,
-    end: number,
+    start?: number,
+    end?: number,
     block?: PugBlock,
-    attributes?: {
+    attrs?: {
         name: string,
-        value: string,
+        val: string,
         mustEscape: boolean
     }[]
 }

+ 20 - 0
src/core/compiler/lexer/tokens/Bind.ts

@@ -0,0 +1,20 @@
+import { PugToken } from "../../Parser";
+import Token from "../Token";
+
+export default class Bind extends Token {
+    public lex(tokens: PugToken[]) {
+        return tokens.map((token, index) => {
+            // We want only attribute tokens
+            if (token.type !== "attribute") {
+                return token;
+            }
+
+            // Check if it's binding an event
+            if (token.mustEscape && token.name.startsWith("@")) {
+                token.name = "@p:bind:" + token.name.substring(1);
+            }
+
+            return token;
+        });
+    }
+};

+ 12 - 6
src/core/compiler/lexer/tokens/ForEach.ts

@@ -9,9 +9,18 @@ export default class ForEach extends Token {
             const node = nodes[index];
 
             // Check if it's a foreach
-            if (node.type === "Tag" && node.name === "foreach" && node.block?.nodes[0].type === "Text") {
-                // Parse the parts
-                const condition: RegExpMatchArray = ("foreach " + node.block?.nodes[0].val).match(ForEach.FOREACH_CONDITION);
+            if (node.type === "Tag" && node.name === "foreach") {
+                let condition: RegExpMatchArray;
+
+                // Check if the next node is a text node
+                if (node.block?.nodes[0].type === "Text") {
+                    condition = ("foreach " + node.block?.nodes[0].val).match(ForEach.FOREACH_CONDITION);
+
+                    // Remove the text from the nodes
+                    node.block.nodes.splice(0, 1);
+                } else {
+                    condition = ("foreach " + node.attrs.map((attr) => attr.name).join(" ")).match(ForEach.FOREACH_CONDITION);
+                }
 
                 // Check if it's an invalid foreach condition
                 if (!condition) {
@@ -23,9 +32,6 @@ export default class ForEach extends Token {
                 // Set the tag name
                 node.name = "p:foreach";
 
-                // Remove the text from it
-                node.block.nodes.splice(0, 1);
-
                 // Setup the attributes for the foreach
                 node.attrs = [
                     {

+ 77 - 0
src/core/compiler/lexer/tokens/If.ts

@@ -0,0 +1,77 @@
+import { PugNode } from "../../Parser";
+import Token from "../Token";
+
+export default class ForEach extends Token {
+    public parse(nodes: PugNode[]) {
+        for(let index = 0; index < nodes.length; index++) {
+            const node = nodes[index];
+
+            // Check if it's a conditional
+            if (node.type === "Conditional") {
+                // Clone it
+                const conditional = { ...node };
+
+                // Replace with a tag
+                node.type = "Tag";
+                node.name = "p:if";
+                node.selfClosing = false;
+                node.attributeBlocks = [];
+                node.isInline = false;
+                node.attrs = [{
+                    name: "condition",
+                    val: `"@p:conditional(${conditional.test})"`,
+                    mustEscape: false
+                }];
+
+                // <p:then>
+                node.block = {
+                    type: "Block",
+                    nodes: [
+                        {
+                            type: "Tag",
+                            name: "p:then",
+                            selfClosing: false,
+                            attrs: [],
+                            attributeBlocks: [],
+                            isInline: false,
+                            block: {
+                                type: "Block",
+                                nodes: this.parse(conditional.consequent.nodes)
+                            }
+                        }
+                    ]
+                };
+
+                // <p:else>
+                if (!!conditional.alternate) {
+                    node.block.nodes.push({
+                        type: "Tag",
+                        name: "p:else",
+                        start: 0,
+                        end: 0,
+                        attributeBlocks: [],
+                        isInline: false,
+                        selfClosing: false,
+                        block: {
+                            type: "Block",
+                            nodes: this.parse(conditional.alternate.nodes)
+                        }
+                    });
+                }
+
+                delete node.test;
+                delete node.consequent;
+                delete node.alternate;
+
+                continue;
+            }
+
+            // Parses the block
+            if (node.block) {
+                node.block.nodes = this.parse(node.block.nodes);
+            }
+        }
+
+        return nodes;
+    }
+};

+ 45 - 0
src/core/compiler/lexer/tokens/Import.ts

@@ -0,0 +1,45 @@
+import { PugNode } from "../../Parser";
+import Token from "../Token";
+
+export default class Import extends Token {
+    private static readonly IMPORT_CONDITION = /import? (?<identifier>.+?) from \"?\'?(?<filename>.+)\"?\'?$/;
+
+    public parse(nodes: PugNode[]) {
+        for(let index = 0; index < nodes.length; index++) {
+            const node = nodes[index];
+
+            // Check if it's a foreach
+            if (node.type === "Tag" && node.name === "import") {
+                const condition: RegExpMatchArray = ("import " + node.attrs.map((attr) => attr.name).join(" ")).match(Import.IMPORT_CONDITION);
+
+                // Check if it's an invalid foreach condition
+                if (!condition) {
+                    throw new TypeError("Invalid import condition. It needs to have an alias and a filename.");
+                }
+
+                const { identifier, filename } = condition.groups;
+
+                // Set the tag name
+                node.type = "Code";
+
+                // Setup the attributes for the foreach
+                node.block = {
+                    type: "Block",
+                    nodes: [
+                        {
+                            type: "Text",
+                            val: `const ${identifier} = require("${filename}");`
+                        }
+                    ]
+                };
+            }
+
+            // Parses the block
+            if (node.block) {
+                node.block.nodes = this.parse(node.block.nodes);
+            }
+        }
+
+        return nodes;
+    }
+};

+ 1 - 1
src/core/renderer/Reactive.ts

@@ -4,7 +4,7 @@ import type Reactor from "./Reactor";
 
 export namespace Reactive {
     export type ReactiveData = Record<string, any>;
-    export type ReactiveTarget = "text" | "html" | "attribute" | "foreach";
+    export type ReactiveTarget = "text" | "html" | "attribute" | "foreach" | "if";
     export type ReactiveCommand = "escape" | "literal" | null;
 
     export type Context = (ProxyHandler<Reactive.ReactiveData> | Record<any, any>);

+ 29 - 3
src/core/renderer/Reactor.ts

@@ -1,4 +1,4 @@
-import type Renderer from "../Renderer";
+import type { Renderer } from "../Renderer";
 import { ObservableChange } from "observable-slim";
 import { Reactive } from "./Reactive";
 
@@ -6,6 +6,7 @@ import ForEachReactor from "./reactors/ForEach";
 import HTMLReactor from "./reactors/HTML";
 import HTMLAttributeReactor from "./reactors/HTMLAttribute";
 import TextReactor from "./reactors/Text";
+import IfReactor from "./reactors/If";
 
 const debug = require("debug")("pupperjs:renderer:reactor");
 
@@ -15,7 +16,8 @@ export default class Reactor {
         foreach: ForEachReactor,
         html: HTMLReactor,
         attribute: HTMLAttributeReactor,
-        text: TextReactor
+        text: TextReactor,
+        if: IfReactor
     };
 
     /**
@@ -83,7 +85,7 @@ export default class Reactor {
             return false;
         }
 
-        this.reactive.some((reactive) => {
+        const result = this.reactive.some((reactive) => {
             if (reactive.test(path)) {
                 debug("%s handled by %s", path, reactive.constructor.name);
 
@@ -94,6 +96,10 @@ export default class Reactor {
 
             return false;
         });
+
+        if (!result) {
+            debug("%s failed to be handled", path);
+        }
     }
 
     /**
@@ -122,4 +128,24 @@ export default class Reactor {
             this.triggerChangeFor(property, options.initialValue);
         }
     }
+
+    /**
+     * Binds an event to an element
+     * @param element The element that will receive the event
+     * @param event The event name
+     * @param method The event method name
+     * @returns 
+     */
+    public bindEvent(element: HTMLElement | Element | Node, event: string, method: string) {
+        const callback = this.renderer.methods[method];
+
+        if (callback === undefined) {
+            debug("method %s was not found when binding %s for %O", method, event, element);
+            return false;
+        }
+
+        element.addEventListener(event, callback);
+
+        return true;
+    }
 }

+ 3 - 2
src/core/renderer/reactors/ForEach.ts

@@ -77,8 +77,9 @@ export default class ForEachReactor extends Reactive.AbstractReactor {
 
         // Check if it's array.length (pushing a new object into the array)
         if (path === this.path + ".length") {
-            path = this.path + "." + (newValue - 1);
-            this.addSingle(path, this.renderer.getLiteralValue(path));
+            path = String(newValue - 1);
+
+            this.addSingle(path, this.renderer.getLiteralValue(this.path + "." + path));
 
             return true;
         }

+ 20 - 0
src/core/renderer/reactors/If.ts

@@ -0,0 +1,20 @@
+import { Reactive } from "../Reactive";
+
+const debug = require("debug")("pupperjs:renderer:reactors:if");
+
+export default class IfReactor extends Reactive.AbstractReactor {
+    public static readonly Type: "if";
+
+    public test(path: string) {
+        console.log(this.path, path);
+        return super.test(path);
+    }
+
+    public handle(path: string, newValue: any) {
+        if (newValue) {
+            
+        }
+
+        console.log(path, newValue);
+    }
+}

+ 14 - 2
src/index.ts

@@ -1,12 +1,24 @@
 import { Compiler } from "./core/Compiler";
 import PupperCompiler from "./core/Compiler";
-import PupperRenderer from "./core/Renderer";
+import { Renderer } from "./core/Renderer";
+import type { compileTemplate } from "pug";
+import { Reactive } from "./core/renderer/Reactive";
 
 class PupperStatic {
     static readonly Compiler = PupperCompiler;
-    static readonly Renderer = PupperRenderer;
+    static readonly Renderer = Renderer;
     static readonly Pupper = import("./pupper");
 
+    /**
+     * Creates a renderer instance
+     * @param template The compiled template function
+     * @param data The reactive data, optional
+     * @returns 
+     */
+    static createRenderer(template: compileTemplate, data?: Reactive.ReactiveData) {
+        return new Renderer(template, data);
+    }
+
     /**
      * Compiles a string
      * @param file The string to be compiled

+ 1 - 1
src/pupper.ts

@@ -1,5 +1,5 @@
 import Compiler from "./core/Compiler";
-import Renderer from "./core/Renderer";
+import { Renderer } from "./core/Renderer";
 
 export {
     Compiler,

+ 33 - 29
test/browser.js

@@ -1,38 +1,42 @@
-const { default: Renderer } = require("../out/core/Renderer");
+const { Component } = require("../out/core/Component");
 
-const template = new Renderer(require("./template.pupper"));
-
-window.data = {
-    page: {
-        title: "pupper.js is awesome!",
-        description: "I use pupper.js because I love puppies!",
-        lead: "Also we love puppers, shiberinos and other doggos too! 🐶"
+const pupper = new Component({
+    template: require("./template.pupper"),
+    methods: {
+        onClickPuppy(e) {
+            alert("You clicked a puppy! :D");
+        }
     },
-    puppies: [
-        {
-            id: 1,
-            title: "A cutie pup",
-            description: "Look at this cutie",
-            thumbnail: "https://placedog.net/800"
+    data: {
+        page: {
+            title: "pupper.js is awesome!",
+            description: "I use pupper.js because I love puppies!",
+            lead: "Also we love puppers, shiberinos and other doggos too! 🐶"
         },
-        {
-            id: 2,
-            title: "Another cute pup",
-            description: "Isn't it a cute doggo?!",
-            thumbnail: "https://placedog.net/400"
-        }
-    ]
-};
-
-window.data = template.setData(data);
-
-window.template = template;
+        puppies: [
+            {
+                id: 1,
+                title: "A cutie pup",
+                description: "Look at this cutie",
+                thumbnail: "https://placedog.net/800"
+            },
+            {
+                id: 2,
+                title: "Another cute pup",
+                description: "Isn't it a cute doggo?!",
+                thumbnail: "https://placedog.net/400"
+            }
+        ]
+    }
+});
 
-template.renderTo(document.getElementById("app"));
+window.component = pupper;
+pupper.renderTo(document.getElementById("app"));
 
-window.data.puppies.push({
+pupper.data.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="
+    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
 });

+ 6 - 1
test/node.js

@@ -3,7 +3,12 @@ const fs = require("fs");
 const beautify = require("js-beautify");
 
 const result = beautify(
-    pupper.compileToStringSync(fs.readFileSync(__dirname + "/template.pupper"))
+    pupper.compileToStringSync(fs.readFileSync(__dirname + "/template.pupper"), {
+        debug: true,
+        pug: {
+            filename: __dirname + "/template.pupper"
+        }
+    })
 );
 
 fs.writeFileSync(__dirname + "/out/node.js", result);

+ 13 - 0
test/puppy.pupper

@@ -0,0 +1,13 @@
+.col-5.mb-5
+    .puppy.card.px-0(data-id={{ puppy.id }}, :click="onClickPuppy").text-dark
+        img.card-img-top(src={- puppy.thumbnail -}, crossorigin="auto")
+
+        .card-header
+            h5.card-title={{ puppy.title }}
+            small.text-muted|Served by pupper.js
+
+        .card-body
+            ={- puppy.description -}
+
+            if (puppy.shibe)
+                p.text-warning|shibe!!!

+ 8 - 3
test/template.pupper

@@ -2,6 +2,8 @@
 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")
 
+//-import(Puppy from "./puppy.pupper")
+
 .text-center
     .cover-container.d-flex.h-100.p-3.mx-auto.flex-column
         header.masthead.mb-auto 
@@ -9,16 +11,16 @@ link(href="https://getbootstrap.com/docs/4.0/examples/cover/cover.css", rel="sty
                 h3.masthead-brand={{ page.title }}
 
         //- Main contents
-        main.inner.cover(role="main")
+        main.inner.cover.my-3(role="main")
             h1.cover-heading={{ page.description }}
 
             p.lead={{ page.lead }}
 
             .row.mt-5.justify-content-around.align-items-center
                 //- Reactive puppies
-                foreach puppy of puppies
+                foreach(puppy of puppies)
                     .col-5.mb-5
-                        .puppy.card.px-0(data-id={{ puppy.id }}).text-dark
+                        .puppy.card.px-0(data-id={{ puppy.id }}, style={ cursor: "pointer" }, @click="onClickPuppy").text-dark
                             img.card-img-top(src={- puppy.thumbnail -}, crossorigin="auto")
 
                             .card-header
@@ -28,6 +30,9 @@ link(href="https://getbootstrap.com/docs/4.0/examples/cover/cover.css", rel="sty
                             .card-body
                                 ={- puppy.description -}
 
+                                if (puppy.shibe)
+                                    p.text-warning|shibe!!!
+
         footer.mastfoot.mt-auto
             .inner
                 p