Browse Source

added initial documentation

Matheus Giovani 3 years ago
parent
commit
455d101691

+ 18 - 0
packages/docs/package.json

@@ -0,0 +1,18 @@
+{
+  "name": "@pupperjs/docs",
+  "version": "1.0.0",
+  "main": "src/index.js",
+  "repository": "https://github.com/pupperjs/pupperjs/packages/docs",
+  "license": "MIT",
+  "private": true,
+  "scripts": {
+    "watch": "webpack"
+  },
+  "dependencies": {
+    "@pupperjs/webpack-loader": "file:./../webpack-loader"
+  },
+  "devDependencies": {
+    "copy-webpack-plugin": "^11.0.0",
+    "webpack": "^5.73.0"
+  }
+}

+ 18 - 0
packages/docs/src/components/App.pupper

@@ -0,0 +1,18 @@
+import DocsComponent(from="./DocsComponent.pupper")
+import LandingComponent(from="./LandingComponent.pupper")
+
+template
+    div
+        //- Stylesheets
+        link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootswatch@4.5.2/dist/litera/bootstrap.min.css", integrity="sha384-enpDwFISL6M3ZGZ50Tjo8m65q06uLVnyvkFO3rsoW0UC15ATBFz3QEhr3hmxpYsn", crossorigin="anonymous")
+
+        //- If it's loading the docs
+        if isDocs
+            //- Load the docs component
+            DocsComponent
+        else
+            //- Load the landing page component
+            LandingComponent
+
+data
+    isDocs = new URL(window.location.href).hash.includes("docs")

+ 38 - 0
packages/docs/src/components/DocsComponent.pupper

@@ -0,0 +1,38 @@
+import ComponentsComponent(from="./docs/essentials/ComponentsComponent.pupper")
+
+template
+    .m-3
+        .row
+            //- Sidebar
+            .col-12.col-lg-4
+                each section in sections
+                    div
+                        strong=section.title
+
+                        .list-group
+                            each subSection in section.subSections
+                                .list-group-item.list-group-item-action(
+                                    role="button",
+                                    @click="changeToSection(subSection)"
+                                )=subSection.title
+
+            //- Section content
+            .col-12.col-lg-6
+                slot(name="section")
+data
+    sections = [
+        {
+            title: "Essentials",
+            subSections: [
+                {
+                    title: "Components",
+                    component: ComponentsComponent
+                }
+            ]
+        }
+    ]
+
+implementation
+    #changeToSection(section)
+        console.log(this);
+        section.component.mount(this.$slots.section);

+ 23 - 0
packages/docs/src/components/LandingComponent.pupper

@@ -0,0 +1,23 @@
+template
+    .m-3
+        .jumbotron.text-center.bg-transparent.mb-3
+            h1.display-1|pupper.js
+            p.lead|A pug.js styled reactive framework
+
+        .mx-5.px-5
+            h2.display-4
+                div|Simple,
+                div|semantic and
+                div|complete.
+
+            .mt-3
+
+            |pupper.js is a complete tool for creating applications, websites and systems.
+
+            .mt-3
+
+            |Using the syntax from pug.js
+
+            .mt-3
+
+            a.btn.btn-primary(href="#docs")|Documentation

+ 2 - 0
packages/docs/src/components/docs/essentials/ComponentsComponent.pupper

@@ -0,0 +1,2 @@
+template
+    h3.display-4|Components

+ 13 - 0
packages/docs/src/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>pupper.js</title>
+</head>
+<body>
+    <div id="app"></div>
+    <script src="index.js" type="text/javascript"></script>
+</body>
+</html>

+ 3 - 0
packages/docs/src/index.js

@@ -0,0 +1,3 @@
+import App from "./components/App.pupper";
+
+App.mount("#app");

+ 28 - 0
packages/docs/webpack.config.js

@@ -0,0 +1,28 @@
+const CopyPlugin = require("copy-webpack-plugin");
+const path = require("path");
+
+module.exports = {
+    entry: __dirname + "/src/index.js",
+    output: {
+        path: __dirname + "/dist",
+        filename: "index.js",
+        publicPath: "./"
+    },
+    cache: false,
+    watch: true,
+    mode: "development",
+    devtool: "source-map",
+    module: {
+        rules: [
+            {
+                test: /\.pupper$/,
+                use: ["@pupperjs/webpack-loader"]
+            },
+            {
+                test: /\.js$/,
+                enforce: "pre",
+                use: ["source-map-loader"],
+            },
+        ]
+    }
+}

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

@@ -3,6 +3,7 @@ import { Renderer } from "./vdom/Renderer";
 import { Slot } from "./vdom/renderer/Slot";
 
 import type h from "virtual-dom/h";
+import Debugger from "../util/Debugger";
 
 /**
  * Represents a component's data.
@@ -47,14 +48,8 @@ export interface IComponent<
     mounted?: (this: Component) => any;
 }
 
-/**
- * Components also are records, because they carry any type of data.
- */
-export interface Component extends Record<string, any> {
-
-}
-
 export class Component {
+    $container: any;
     public static create<
         TMethods extends Record<string, CallableFunction>,
         TData extends Record<string, any>
@@ -97,19 +92,17 @@ export class Component {
      */
     public renderer = new Renderer(this);
 
+    /**
+     * The component container
+     */
+    public $rendered: Element;
+
     constructor(
         /**
          * The component properties.
          */
         public $component: IComponent<any, any>
     ) {
-        // If has methods
-        if ($component?.methods) {
-            for(let method in $component.methods) {
-                this.$state[method] = $component.methods[method];
-            }
-        }
-
         // If has data
         if ($component?.data) {
             if (typeof $component.data === "function") {
@@ -126,6 +119,13 @@ export class Component {
             }
         }
 
+        // If has methods
+        if ($component?.methods) {
+            for(let method in $component.methods) {
+                this.$state[method] = $component.methods[method].bind(this);
+            }
+        }
+
         // For each generated data
         for(let key in this.$state) {
             // Prepare a descriptor for the base component
@@ -202,41 +202,49 @@ export class Component {
      * Renders the template function into a div tag.
      */
     public async render() {
-        let renderContainer: Element;
-
         if (this.firstRender) {
             this.firstRender = false;
 
-            renderContainer = await this.renderer.render();
-
-            // Find all slots, templates and references
-            const slots = Array.from(renderContainer.querySelectorAll("slot"));
-            const refs = Array.from(renderContainer.querySelectorAll("[ref]"));
+            this.$rendered = await this.renderer.render();
+            this.prepareDOM();
+        }
 
-            // Iterate over all slots
-            for(let slot of slots) {
-                // Replace it with a comment tag
-                const comment = this.replaceWithCommentPlaceholder(slot);
+        return this.$rendered;
+    }
 
-                // 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;
-                }
+    /**
+     * 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;
+        // 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");
-            }
+            // Remove the attribute
+            ref.removeAttribute("ref");
         }
 
-        return renderContainer;
+        Debugger.debug("%O slots: %O", this, slots);
     }
 
     /**
@@ -244,14 +252,25 @@ export class Component {
      * @param target The target element where the element will be mounted.
      * @returns 
      */
-    public async mount(target: HTMLElement | Slot) {
+    public async mount(target: HTMLElement | Slot | string) {
         const rendered = await this.render();
 
         // If it's targeting a slot
         if (target instanceof Slot) {
             target.replaceWith(rendered);
-        } else {
+            this.$container = rendered;
+        } else
+        // If it's targeting a string (selector)
+        if (typeof target === "string") {
+            this.$container = document.querySelector(target);
+            this.$container?.append(rendered);
+        } else
+        // If it's targeint an element
+        if (target instanceof Element) {
+            this.$container = target;
             target.append(rendered);
+        } else {
+            throw new Error("Invalid mounting target " + target);
         }
 
         if ("mounted" in this.$component) {

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

@@ -57,7 +57,7 @@ export class Renderer {
     /**
      * The container that will receive the renderer contents.
      */
-    protected container: Element;
+    public container: Element;
 
     /**
      * The rendering queue.
@@ -73,7 +73,7 @@ export class Renderer {
     private inQueue: boolean;
 
     constructor(
-        protected component: Component
+        public component: Component
     ) {
         this.stateStack.push(
             // Globals
@@ -134,7 +134,7 @@ export class Renderer {
      * Generates a state from the state stack.
      * @returns 
      */
-    protected generateScope() {
+    public generateScope() {
         return this.stateStack.reduce((carrier, curr) => {
             for(let key in curr) {
                 carrier[key] = curr[key];
@@ -154,7 +154,13 @@ export class Renderer {
         const tick = this.nextTick(async () => {
             const vdom = this.component.$component.render({ h });
             const node = Renderer.createNode(vdom, null, this);
+
             this.rendererNode = await walk(node, this.generateScope());
+
+            this.rendererNode.addEventListener("$created", () => {
+                this.component.$rendered = this.rendererNode.element;
+                this.component.prepareDOM();
+            });
         });
 
         await this.waitForTick(tick);
@@ -195,7 +201,7 @@ export class Renderer {
             listeners: []
         });
 
-        setTimeout(() => this.maybeStartQueue());
+        window.requestAnimationFrame(() => this.maybeStartQueue());
 
         return tick;
     }

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

@@ -17,7 +17,7 @@ directive("component", async (node, { expression, scope }) => {
 
             Debugger.warn("component %s resolved to %O", expression, component);
 
-            // Remove the component attribute
+            // Remove the x-component attribute
             node.removeAttribute("x-component");
 
             // Parse all attributes into the component state
@@ -33,10 +33,14 @@ directive("component", async (node, { expression, scope }) => {
             // Set the parent component
             component.$parent = scope.$component as Component;
 
-            Debugger.debug("%s scope is %O", expression, component);
+            console.error(component.renderer.generateScope());
+
+            const rendered = await component.renderer.renderToNode();
+            rendered.setDirty(false);
+            rendered.setChildrenDirty(false);
 
             // Remove the original attribute from the node
-            node.replaceWith(await component.renderer.renderToNode());
+            node.replaceWith(rendered);
         } catch(e) {
             console.warn("pupper.js has failed to create component:");
             console.error(e);

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

@@ -17,7 +17,8 @@ directive("on", async (node, { value, expression, scope }) => {
 
         debug("will handle event \"%s\" to %O", value, evaluate);
 
-        node.addEventListener(value, async ($event: any) => {
+        // Register the event listener
+        node.addEventListener(value as string, async ($event: any) => {
             debug("handled %s event", value);
             
             const evScope = { ...scope, $event };

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

@@ -24,8 +24,15 @@ export class ConditionalNode extends PupperNode {
     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;
+        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) {

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

@@ -15,9 +15,10 @@ interface IProp {
 
 interface IDirective {
     type: TDirectives | null;
-    value: TAttributeVal | null;
+    value?: TAttributeVal | null;
     modifiers: string[];
     expression: string;
+    scope: TScope;
 }
 
 let isDeferringHandlers = false;
@@ -36,12 +37,7 @@ const pupperAttrRegex = /^x-([^:^.]+)\b/;
 
 type TDirectiveCallback = (
     node: TRendererNodes,
-    data: {
-        renderer: Renderer;
-        scope: TScope;
-        expression?: string;
-        value?: string;
-    }
+    data:IDirective
 ) => any;
 
 export function directive(attribute: TDirectives, callback: TDirectiveCallback) {
@@ -140,8 +136,8 @@ function toParsedDirectives(
             value: valueMatch ? valueMatch[1] as any : null,
             modifiers: modifiers.map(i => i.replace(".", "")),
             expression: String(value),
-            original,
-        };
+            original
+        } as any;
     }
 }
 

+ 24 - 1
packages/renderer/src/model/Reactivity.ts

@@ -4,6 +4,7 @@ export type TReactiveObj = Record<string | number | symbol, any>;
 const effects = new Map<TReactiveObj, Record<string | symbol, TEffect[]>>();
 let currentEffect: TEffect = null;
 
+import { Component } from "../core/Component";
 import Debugger from "../util/Debugger";
 
 const debug = Debugger.extend("reactivity");
@@ -48,14 +49,36 @@ export async function effect(effect: TEffect) {
     };
 }
 
+let recursion: any[] = [];
+
 export function reactive(obj: TReactiveObj) {
+    // Ignore if it's not a valid object
+    if (!obj) {
+        return obj;
+    }
+
     for(let property in obj) {
         // Proxy subobjects
-        if ((typeof obj[property] === "object" || Array.isArray(obj[property])) && obj[ProxySymbol] === undefined) {
+        if (
+            // Do not proxy non-objects
+            (typeof obj[property] === "object" || Array.isArray(obj[property])) && 
+            // Do not proxy components
+            !(obj[property] instanceof Component) &&
+            // Do not proxy already proxied objects
+            obj[ProxySymbol] === undefined
+        ) {
+            if (recursion.includes(obj)) {
+                console.warn("reactive recursion detected:", recursion, obj);
+                continue;
+            }
+
+            recursion.push(obj);
             obj[property] = reactive(obj[property]);
         }
     }
 
+    recursion = [];
+
     obj[ProxySymbol] = true;
 
     return new Proxy(obj, {

+ 4 - 0
packages/renderer/src/model/vdom/RendererNode.ts

@@ -655,6 +655,8 @@ export class RendererNode<TNode extends VirtualDOM.VTree = any> {
                 this.element.addEventListener(evt, handler);
             }
         }
+
+        this.element.dispatchEvent(new Event("$created"));
     }
 
     /**
@@ -667,6 +669,8 @@ export class RendererNode<TNode extends VirtualDOM.VTree = any> {
             }
         }
 
+        this.element.dispatchEvent(new Event("$removed"));
+
         this.element = null;
     }