Merge branch 'dev'
jump to
@@ -11,15 +11,15 @@ **H3** is a microframework to build client-side single-page applications (SPAs) in modern JavaScript.
H3 is also: -- **tiny**, less than 4KB minified gzipped. +- **tiny**, less than 4KB minified and gzipped. - **modern**, in the sense that it runs only in modern browsers (latest versions of Chrome, Firefox, Edge & similar). -- **easy** to learn, its API is comprised of only six methods and two properties. +- **easy** to learn, its API is comprised of only seven methods and two properties. ### I'm sold! Where can I get it? Here, look, it's just one file: -<a href="https://raw.githubusercontent.com/h3rald/h3/v0.9.0/h3.js" target="_blank" class="button primary">Download v0.9.0 (Impeccable Iconian)</a> +<a href="https://raw.githubusercontent.com/h3rald/h3/v0.10.0/h3.js" target="_blank" class="button primary">Download v0.10.0 (Jittery Jem'Hadar)</a> <small>Or get the minified version [here](https://raw.githubusercontent.com/h3rald/h3/v0.9.0/h3.min.js).</small>@@ -31,7 +31,7 @@ Here's an example of an extremely minimal SPA created with H3:
```js import h3 from "./h3.js"; -h3.init(() => h3("h1", "Hello, World!")); +h3.init(() => h("h1", "Hello, World!")); ``` This will render a `h1` tag within the document body, containing the text `"Hello, World!"`.
@@ -1,4 +1,6 @@
-const h3 = require("../h3.js").default; +const mod = require("../h3.js"); +const h3 = mod.h3; +const h = mod.h; describe("h3", () => { beforeEach(() => {@@ -12,10 +14,10 @@ window.requestAnimationFrame.mockRestore();
}); it("should support a way to discriminate functions and objects", () => { - const v1 = h3("div", { onclick: () => true }); - const v2 = h3("div", { onclick: () => true }); - const v3 = h3("div", { onclick: () => false }); - const v4 = h3("div"); + const v1 = h("div", { onclick: () => true }); + const v2 = h("div", { onclick: () => true }); + const v3 = h("div", { onclick: () => false }); + const v4 = h("div"); expect(v1.equal(v2)).toEqual(true); expect(v1.equal(v3)).toEqual(false); expect(v4.equal({ type: "div" })).toEqual(false);@@ -24,10 +26,10 @@ expect(v1.equal(null, undefined)).toEqual(false);
}); it("should support the creation of empty virtual node elements", () => { - expect(h3("div")).toEqual({ + expect(h("div")).toEqual({ type: "div", children: [], - attributes: {}, + props: {}, classList: [], data: {}, eventListeners: {},@@ -39,17 +41,17 @@ });
}); it("should throw an error when invalid arguments are supplied", () => { - const empty = () => h3(); - const invalid1st = () => h3(1); - const invalid1st2 = () => h3(1, {}); - const invalid1st3 = () => h3(1, {}, []); - const invalid1st1 = () => h3(() => ({ type: "#text", value: "test" })); - const invalid1st1b = () => h3({ a: 2 }); - const invalid2nd = () => h3("div", 1); - const invalid2nd2 = () => h3("div", true, []); - const invalid2nd3 = () => h3("div", null, []); - const invalidChildren = () => h3("div", ["test", 1, 2]); - const tooManyArgs = () => h3("div", { id: "test" }, "test", "aaa"); + const empty = () => h(); + const invalid1st = () => h(1); + const invalid1st2 = () => h(1, {}); + const invalid1st3 = () => h(1, {}, []); + const invalid1st1 = () => h(() => ({ type: "#text", value: "test" })); + const invalid1st1b = () => h({ a: 2 }); + const invalid2nd = () => h("div", 1); + const invalid2nd2 = () => h("div", true, []); + const invalid2nd3 = () => h("div", null, []); + const invalidChildren = () => h("div", ["test", 1, 2]); + const emptySelector = () => h(""); expect(empty).toThrowError(/No arguments passed/); expect(invalid1st).toThrowError(/Invalid first argument/); expect(invalid1st2).toThrowError(/Invalid first argument/);@@ -60,21 +62,30 @@ expect(invalid2nd).toThrowError(/second argument of a VNode constructor/);
expect(invalid2nd2).toThrowError(/Invalid second argument/); expect(invalid2nd3).toThrowError(/Invalid second argument/); expect(invalidChildren).toThrowError(/not a VNode: 1/); - expect(tooManyArgs).toThrowError(/Too many arguments/); + expect(emptySelector).toThrowError(/Invalid selector/); + }); + + it("should support several child arguments", () => { + let vnode = h("div", { test: "a" }, "a", "b", "c"); + expect(vnode.children.length).toEqual(3); + vnode = h("div", "a", "b", "c"); + expect(vnode.children.length).toEqual(3); + vnode = h("div", "a", "b"); + expect(vnode.children.length).toEqual(2); }); it("should support the creation of elements with a single, non-array child", () => { - const vnode1 = h3("div", () => "test"); - const vnode2 = h3("div", () => h3("span")); + const vnode1 = h("div", () => "test"); + const vnode2 = h("div", () => h("span")); expect(vnode1.children[0].value).toEqual("test"); expect(vnode2.children[0].type).toEqual("span"); }); it("should remove null/false/undefined children", () => { - const v1 = h3("div", [false, "test", undefined, null, ""]); + const v1 = h("div", [false, "test", undefined, null, ""]); expect(v1.children).toEqual([ - h3({ type: "#text", value: "test" }), - h3({ type: "#text", value: "" }), + h({ type: "#text", value: "test" }), + h({ type: "#text", value: "" }), ]); });@@ -85,7 +96,7 @@ children: [
{ type: "#text", children: [], - attributes: {}, + props: {}, classList: [], data: {}, eventListeners: {},@@ -95,7 +106,7 @@ style: undefined,
value: "test", }, ], - attributes: {}, + props: {}, classList: [], data: {}, eventListeners: {},@@ -104,18 +115,18 @@ $html: undefined,
style: undefined, value: undefined, }; - expect(h3("div", "test")).toEqual(result); - const failing = () => h3("***"); + expect(h("div", "test")).toEqual(result); + const failing = () => h("***"); expect(failing).toThrowError(/Invalid selector/); }); it("should support the creation of virtual node elements with classes", () => { - const a = h3("div.a.b.c"); - const b = h3("div", { classList: ["a", "b", "c"] }); + const a = h("div.a.b.c"); + const b = h("div", { classList: ["a", "b", "c"] }); expect(a).toEqual({ type: "div", children: [], - attributes: {}, + props: {}, classList: ["a", "b", "c"], data: {}, eventListeners: {},@@ -128,13 +139,13 @@ });
expect(a).toEqual(b); }); - it("should support the creation of virtual node elements with attributes and classes", () => { - expect(h3("div.test1.test2", { id: "test" })).toEqual({ + it("should support the creation of virtual node elements with props and classes", () => { + expect(h("div.test1.test2", { id: "test" })).toEqual({ type: "div", children: [], classList: ["test1", "test2"], data: {}, - attributes: {}, + props: {}, eventListeners: {}, id: "test", $html: undefined,@@ -145,11 +156,11 @@ });
}); it("should support the creation of virtual node elements with text children and classes", () => { - expect(h3("div.test", ["a", "b"])).toEqual({ + expect(h("div.test", ["a", "b"])).toEqual({ type: "div", children: [ { - attributes: {}, + props: {}, children: [], classList: [], data: {},@@ -161,7 +172,7 @@ type: "#text",
value: "a", }, { - attributes: {}, + props: {}, children: [], classList: [], data: {},@@ -173,7 +184,7 @@ type: "#text",
value: "b", }, ], - attributes: {}, + props: {}, classList: ["test"], data: {}, eventListeners: {},@@ -184,50 +195,50 @@ value: undefined,
}); }); - it("should support the creation of virtual node elements with text children, attributes, and classes", () => { - expect( - h3("div.test", { title: "Test...", id: "test" }, ["a", "b"]) - ).toEqual({ - type: "div", - children: [ - { - attributes: {}, - children: [], - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - type: "#text", - value: "a", - }, - { - attributes: {}, - children: [], - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - type: "#text", - value: "b", - }, - ], - data: {}, - eventListeners: {}, - id: "test", - $html: undefined, - style: undefined, - value: undefined, - attributes: { title: "Test..." }, - classList: ["test"], - }); + it("should support the creation of virtual node elements with text children, props, and classes", () => { + expect(h("div.test", { title: "Test...", id: "test" }, ["a", "b"])).toEqual( + { + type: "div", + children: [ + { + props: {}, + children: [], + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + type: "#text", + value: "a", + }, + { + props: {}, + children: [], + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + type: "#text", + value: "b", + }, + ], + data: {}, + eventListeners: {}, + id: "test", + $html: undefined, + style: undefined, + value: undefined, + props: { title: "Test..." }, + classList: ["test"], + } + ); }); - it("should support the creation of virtual node elements with attributes", () => { - expect(h3("input", { type: "text", value: "AAA" })).toEqual({ + it("should support the creation of virtual node elements with props", () => { + expect(h("input", { type: "text", value: "AAA" })).toEqual({ type: "input", children: [], data: {},@@ -236,14 +247,14 @@ id: undefined,
$html: undefined, style: undefined, value: "AAA", - attributes: { type: "text" }, + props: { type: "text" }, classList: [], }); }); it("should support the creation of virtual node elements with event handlers", () => { const fn = () => true; - expect(h3("button", { onclick: fn })).toEqual({ + expect(h("button", { onclick: fn })).toEqual({ type: "button", children: [], data: {},@@ -254,23 +265,23 @@ id: undefined,
$html: undefined, style: undefined, value: undefined, - attributes: {}, + props: {}, classList: [], }); - expect(() => h3("span", { onclick: "something" })).toThrowError( + expect(() => h("span", { onclick: "something" })).toThrowError( /onclick event is not a function/ ); }); it("should support the creation of virtual node elements with element children and classes", () => { expect( - h3("div.test", ["a", h3("span", ["test1"]), () => h3("span", ["test2"])]) + h("div.test", ["a", h("span", ["test1"]), () => h("span", ["test2"])]) ).toEqual({ - attributes: {}, + props: {}, type: "div", children: [ { - attributes: {}, + props: {}, children: [], classList: [], data: {},@@ -285,7 +296,7 @@ {
type: "span", children: [ { - attributes: {}, + props: {}, children: [], classList: [], data: {},@@ -297,7 +308,7 @@ type: "#text",
value: "test1", }, ], - attributes: {}, + props: {}, classList: [], data: {}, eventListeners: {},@@ -310,7 +321,7 @@ {
type: "span", children: [ { - attributes: {}, + props: {}, children: [], classList: [], data: {},@@ -322,7 +333,7 @@ type: "#text",
value: "test2", }, ], - attributes: {}, + props: {}, classList: [], data: {}, eventListeners: {},@@ -358,7 +369,7 @@ expect(navigateTo).toThrowError(/No application initialized/);
}); it("should provide an init method to initialize a SPA with a single component", async () => { - const c = () => h3("div", "Hello, World!"); + const c = () => h("div", "Hello, World!"); const body = document.body; const appendChild = jest.spyOn(body, "appendChild"); await h3.init(c);@@ -385,7 +396,7 @@ }
}); it("should expose a redraw method", async () => { - const vnode = h3("div"); + const vnode = h("div"); await h3.init(() => vnode); jest.spyOn(vnode, "redraw"); h3.redraw();@@ -397,15 +408,34 @@ expect(vnode.redraw).toHaveBeenCalledTimes(2);
}); it("should not redraw while a other redraw is in progress", async () => { - const vnode = h3("div"); + const vnode = h("div"); await h3.init({ routes: { - "/": () => vnode + "/": () => vnode, }, }); jest.spyOn(vnode, "redraw"); h3.redraw(true); - h3.redraw() + h3.redraw(); expect(vnode.redraw).toHaveBeenCalledTimes(1); + }); + + it("should expose a screen method to define screen-level components with (optional) setup and teardown", async () => { + expect(() => h3.screen({})).toThrowError(/No display property specified/); + expect(() => h3.screen({ setup: 1, display: () => "" })).toThrowError( + /setup property is not a function/ + ); + expect(() => h3.screen({ teardown: 1, display: () => "" })).toThrowError( + /teardown property is not a function/ + ); + let s = h3.screen({ display: () => "test" }); + expect(typeof s).toEqual("function"); + s = h3.screen({ + display: () => "test", + setup: (state) => state, + teardown: (state) => state, + }); + expect(typeof s.setup).toEqual("function"); + expect(typeof s.teardown).toEqual("function"); }); });
@@ -1,4 +1,6 @@
-const h3 = require("../h3.js").default; +const mod = require("../h3.js"); +const h3 = mod.h3; +const h = mod.h; let preStartCalled = false; let postStartCalled = false;@@ -6,129 +8,129 @@ let count = 0;
let result = 0; const setCount = () => { - count = count + 2; - h3.dispatch("count/set", count); + count = count + 2; + h3.dispatch("count/set", count); }; let hash = "#/c2"; const mockLocation = { - get hash() { - return hash; - }, - set hash(value) { - const event = new CustomEvent("hashchange"); - event.oldURL = hash; - event.newURL = value; - hash = value; - window.dispatchEvent(event); - }, + get hash() { + return hash; + }, + set hash(value) { + const event = new CustomEvent("hashchange"); + event.oldURL = hash; + event.newURL = value; + hash = value; + window.dispatchEvent(event); + }, }; const C1 = () => { - const parts = h3.route.parts; - const content = Object.keys(parts).map((key) => - h3("li", `${key}: ${parts[key]}`) - ); - return h3("ul.c1", content); + const parts = h3.route.parts; + const content = Object.keys(parts).map((key) => + h("li", `${key}: ${parts[key]}`) + ); + return h("ul.c1", content); }; const C2 = () => { - const params = h3.route.params; - const content = Object.keys(params).map((key) => - h3("li", `${key}: ${params[key]}`) - ); - return h3("ul.c2", content); + const params = h3.route.params; + const content = Object.keys(params).map((key) => + h("li", `${key}: ${params[key]}`) + ); + return h("ul.c2", content); }; describe("h3 (Router)", () => { - beforeEach(async () => { - const preStart = () => (preStartCalled = true); - const postStart = () => (postStartCalled = true); - await h3.init({ - routes: { - "/c1/:a/:b/:c": C1, - "/c2": C2, - }, - location: mockLocation, - preStart: preStart, - postStart: postStart, + beforeEach(async () => { + const preStart = () => (preStartCalled = true); + const postStart = () => (postStartCalled = true); + await h3.init({ + routes: { + "/c1/:a/:b/:c": C1, + "/c2": C2, + }, + location: mockLocation, + preStart: preStart, + postStart: postStart, + }); }); - }); - it("should support routing configuration at startup", () => { - expect(h3.route.def).toEqual("/c2"); - }); + it("should support routing configuration at startup", () => { + expect(h3.route.def).toEqual("/c2"); + }); - it("should support pre/post start hooks", () => { - expect(preStartCalled).toEqual(true); - expect(postStartCalled).toEqual(true); - }); + it("should support pre/post start hooks", () => { + expect(preStartCalled).toEqual(true); + expect(postStartCalled).toEqual(true); + }); - it("should support the capturing of parts within the current route", (done) => { - const sub = h3.on("$redraw", () => { - expect(document.body.childNodes[0].childNodes[1].textContent).toEqual( - "b: 2" - ); - sub(); - done(); + it("should support the capturing of parts within the current route", (done) => { + const sub = h3.on("$redraw", () => { + expect( + document.body.childNodes[0].childNodes[1].textContent + ).toEqual("b: 2"); + sub(); + done(); + }); + mockLocation.hash = "#/c1/1/2/3"; }); - mockLocation.hash = "#/c1/1/2/3"; - }); - it("should expose a navigateTo method to navigate to another path", (done) => { - const sub = h3.on("$redraw", () => { - expect(document.body.childNodes[0].childNodes[1].textContent).toEqual( - "test2: 2" - ); - sub(); - done(); + it("should expose a navigateTo method to navigate to another path", (done) => { + const sub = h3.on("$redraw", () => { + expect( + document.body.childNodes[0].childNodes[1].textContent + ).toEqual("test2: 2"); + sub(); + done(); + }); + h3.navigateTo("/c2", { test1: 1, test2: 2 }); }); - h3.navigateTo("/c2", { test1: 1, test2: 2 }); - }); - it("should throw an error if no route matches", async () => { - try { - await h3.init({ - element: document.body, - routes: { - "/c1/:a/:b/:c": () => h3("div"), - "/c2": () => h3("div"), - }, - }); - } catch (e) { - expect(e.message).toMatch(/No route matches/); - } - }); + it("should throw an error if no route matches", async () => { + try { + await h3.init({ + element: document.body, + routes: { + "/c1/:a/:b/:c": () => h("div"), + "/c2": () => h("div"), + }, + }); + } catch (e) { + expect(e.message).toMatch(/No route matches/); + } + }); - it("should execute setup and teardown methods", (done) => { - let redraws = 0; - C1.setup = (cstate) => { - cstate.result = cstate.result || 0; - cstate.sub = h3.on("count/set", (state, count) => { - cstate.result = count * count; - }); - }; - C1.teardown = (cstate) => { - cstate.sub(); - result = cstate.result; - return { result: cstate.result }; - }; - const sub = h3.on("$redraw", () => { - redraws++; - setCount(); - setCount(); - if (redraws === 1) { - expect(count).toEqual(4); - expect(result).toEqual(0); - h3.navigateTo("/c2"); - } - if (redraws === 2) { - expect(count).toEqual(8); - expect(result).toEqual(16); - delete C1.setup; - delete C1.teardown; - sub(); - done(); - } + it("should execute setup and teardown methods", (done) => { + let redraws = 0; + C1.setup = (cstate) => { + cstate.result = cstate.result || 0; + cstate.sub = h3.on("count/set", (state, count) => { + cstate.result = count * count; + }); + }; + C1.teardown = (cstate) => { + cstate.sub(); + result = cstate.result; + return { result: cstate.result }; + }; + const sub = h3.on("$redraw", () => { + redraws++; + setCount(); + setCount(); + if (redraws === 1) { + expect(count).toEqual(4); + expect(result).toEqual(0); + h3.navigateTo("/c2"); + } + if (redraws === 2) { + expect(count).toEqual(8); + expect(result).toEqual(16); + delete C1.setup; + delete C1.teardown; + sub(); + done(); + } + }); + h3.navigateTo("/c1/a/b/c"); }); - h3.navigateTo("/c1/a/b/c"); - }); });
@@ -1,37 +1,42 @@
-const h3 = require("../h3.js").default; +const mod = require("../h3.js"); +const h3 = mod.h3; +const h = mod.h; describe("h3 (Store)", () => { - beforeEach(async () => { - const test = () => { - h3.on("$init", () => ({ online: true })); - h3.on("$stop", () => ({ online: false })); - h3.on("online/set", (state, data) => ({ online: data })); - }; - return await h3.init({ modules: [test], routes: { "/": () => h3("div") } }); - }); + beforeEach(async () => { + const test = () => { + h3.on("$init", () => ({ online: true })); + h3.on("$stop", () => ({ online: false })); + h3.on("online/set", (state, data) => ({ online: data })); + }; + return await h3.init({ + modules: [test], + routes: { "/": () => h("div") }, + }); + }); - afterEach(() => { - h3.dispatch("$stop"); - }); + afterEach(() => { + h3.dispatch("$stop"); + }); - it("should expose a method to retrieve the application state", () => { - expect(h3.state.online).toEqual(true); - }); + it("should expose a method to retrieve the application state", () => { + expect(h3.state.online).toEqual(true); + }); - it("should expose a method to dispatch messages", () => { - expect(h3.state.online).toEqual(true); - h3.dispatch("online/set", "YEAH!"); - expect(h3.state.online).toEqual("YEAH!"); - }); + it("should expose a method to dispatch messages", () => { + expect(h3.state.online).toEqual(true); + h3.dispatch("online/set", "YEAH!"); + expect(h3.state.online).toEqual("YEAH!"); + }); - it("should expose a method to subscribe to messages (and also cancel subscriptions)", () => { - const sub = h3.on("online/clear", () => ({ online: undefined })); - h3.dispatch("online/clear"); - expect(h3.state.online).toEqual(undefined); - h3.dispatch("online/set", "reset"); - expect(h3.state.online).toEqual("reset"); - sub(); - h3.dispatch("online/clear"); - expect(h3.state.online).toEqual("reset"); - }); + it("should expose a method to subscribe to messages (and also cancel subscriptions)", () => { + const sub = h3.on("online/clear", () => ({ online: undefined })); + h3.dispatch("online/clear"); + expect(h3.state.online).toEqual(undefined); + h3.dispatch("online/set", "reset"); + expect(h3.state.online).toEqual("reset"); + sub(); + h3.dispatch("online/clear"); + expect(h3.state.online).toEqual("reset"); + }); });
@@ -1,370 +1,400 @@
-const h3 = require("../h3.js").default; +const mod = require("../h3.js"); +const h3 = mod.h3; +const h = mod.h; describe("VNode", () => { - it("should provide a from method to initialize itself from an object", () => { - const fn = () => true; - const obj = { - id: "test", - type: "input", - value: "AAA", - $html: "", - data: { a: "1", b: "2" }, - eventListeners: { click: fn }, - children: [], - attributes: { title: "test" }, - classList: ["a1", "a2"], - style: "padding: 2px", - }; - const vnode1 = h3("br"); - vnode1.from(obj); - const vnode2 = h3("input#test.a1.a2", { - value: "AAA", - $html: "", - data: { a: "1", b: "2" }, - onclick: fn, - title: "test", - style: "padding: 2px", + it("should provide a from method to initialize itself from an object", () => { + const fn = () => true; + const obj = { + id: "test", + type: "input", + value: "AAA", + $html: "", + data: { a: "1", b: "2" }, + eventListeners: { click: fn }, + children: [], + props: { title: "test" }, + classList: ["a1", "a2"], + style: "padding: 2px", + }; + const vnode1 = h("br"); + vnode1.from(obj); + const vnode2 = h("input#test.a1.a2", { + value: "AAA", + $html: "", + data: { a: "1", b: "2" }, + onclick: fn, + title: "test", + style: "padding: 2px", + }); + expect(vnode1).toEqual(vnode2); }); - expect(vnode1).toEqual(vnode2); - }); - it("should provide a render method able to render textual nodes", () => { - const createTextNode = jest.spyOn(document, "createTextNode"); - const vnode = h3({ type: "#text", value: "test" }); - const node = vnode.render(); - expect(createTextNode).toHaveBeenCalledWith("test"); - expect(node.constructor).toEqual(Text); - }); + it("should provide a render method able to render textual nodes", () => { + const createTextNode = jest.spyOn(document, "createTextNode"); + const vnode = h({ type: "#text", value: "test" }); + const node = vnode.render(); + expect(createTextNode).toHaveBeenCalledWith("test"); + expect(node.constructor).toEqual(Text); + }); - it("should provide a render method able to render simple element nodes", () => { - const createElement = jest.spyOn(document, "createElement"); - const vnode = h3("br"); - const node = vnode.render(); - expect(createElement).toHaveBeenCalledWith("br"); - expect(node.constructor).toEqual(HTMLBRElement); - }); + it("should provide a render method able to render simple element nodes", () => { + const createElement = jest.spyOn(document, "createElement"); + const vnode = h("br"); + const node = vnode.render(); + expect(createElement).toHaveBeenCalledWith("br"); + expect(node.constructor).toEqual(HTMLBRElement); + }); - it("should provide a render method able to render element nodes with attributes and classes", () => { - const createElement = jest.spyOn(document, "createElement"); - const vnode = h3("span.test1.test2", { title: "test", falsy: false }); - const node = vnode.render(); - expect(createElement).toHaveBeenCalledWith("span"); - expect(node.constructor).toEqual(HTMLSpanElement); - expect(node.getAttribute("title")).toEqual("test"); - expect(node.classList.value).toEqual("test1 test2"); - }); + it("should provide a render method able to render element nodes with props and classes", () => { + const createElement = jest.spyOn(document, "createElement"); + const vnode = h("span.test1.test2", { title: "test", falsy: false }); + const node = vnode.render(); + expect(createElement).toHaveBeenCalledWith("span"); + expect(node.constructor).toEqual(HTMLSpanElement); + expect(node.getAttribute("title")).toEqual("test"); + expect(node.classList.value).toEqual("test1 test2"); + }); - it("should provide a render method able to render element nodes with children", () => { - const vnode = h3("ul", [h3("li", "test1"), h3("li", "test2")]); - const createElement = jest.spyOn(document, "createElement"); - const node = vnode.render(); - expect(createElement).toHaveBeenCalledWith("ul"); - expect(createElement).toHaveBeenCalledWith("li"); - expect(node.constructor).toEqual(HTMLUListElement); - expect(node.childNodes.length).toEqual(2); - expect(node.childNodes[1].constructor).toEqual(HTMLLIElement); - expect(node.childNodes[0].childNodes[0].data).toEqual("test1"); - }); + it("should provide a render method able to render element nodes with children", () => { + const vnode = h("ul", [h("li", "test1"), h("li", "test2")]); + const createElement = jest.spyOn(document, "createElement"); + const node = vnode.render(); + expect(createElement).toHaveBeenCalledWith("ul"); + expect(createElement).toHaveBeenCalledWith("li"); + expect(node.constructor).toEqual(HTMLUListElement); + expect(node.childNodes.length).toEqual(2); + expect(node.childNodes[1].constructor).toEqual(HTMLLIElement); + expect(node.childNodes[0].childNodes[0].data).toEqual("test1"); + }); - it("should handle boolean attributes when redrawing", () => { - const vnode1 = h3("input", { type: "checkbox", checked: true }); - const node = vnode1.render(); - expect(node.checked).toEqual(true); - const vnode = h3("input", { type: "checkbox", checked: false }); - vnode1.redraw({ node, vnode }); - expect(node.checked).toEqual(false); - }); + it("should handle boolean props when redrawing", () => { + const vnode1 = h("input", { type: "checkbox", checked: true }); + const node = vnode1.render(); + expect(node.checked).toEqual(true); + const vnode = h("input", { type: "checkbox", checked: false }); + vnode1.redraw({ node, vnode }); + expect(node.checked).toEqual(false); + }); - it("should handle non-string attributes as properties and not create attributes", () => { - const v = h3("div", { - test: true, - obj: { a: 1, b: 2 }, - arr: [1, 2, 3], - num: 2.3, - str: "test", - s: "", - title: "testing!", - value: false, + it("should handle non-string props as properties and not create attributes", () => { + const v = h("div", { + test: true, + obj: { a: 1, b: 2 }, + arr: [1, 2, 3], + num: 2.3, + str: "test", + s: "", + title: "testing!", + value: false, + }); + const v2 = h("div", { + test: true, + obj: { a: 1, b: 2 }, + arr: [1, 2, 3], + s: "", + title: "testing!", + value: "true", + }); + const n = v.render(); + expect(n.test).toEqual(true); + expect(n.obj).toEqual({ a: 1, b: 2 }); + expect(n.arr).toEqual([1, 2, 3]); + expect(n.num).toEqual(2.3); + expect(n.str).toEqual("test"); + expect(n.getAttribute("str")).toEqual("test"); + expect(n.s).toEqual(""); + expect(n.getAttribute("s")).toEqual(""); + expect(n.title).toEqual("testing!"); + expect(n.getAttribute("title")).toEqual("testing!"); + expect(n.value).toEqual(undefined); + expect(n.getAttribute("value")).toEqual(null); + v.redraw({ node: n, vnode: v2 }); + expect(n.getAttribute("value")).toEqual("true"); + v2.value = null; + v.redraw({ node: n, vnode: v2 }); + expect(n.getAttribute("value")).toEqual(""); }); - const n = v.render(); - expect(n.test).toEqual(true); - expect(n.obj).toEqual({ a: 1, b: 2 }); - expect(n.arr).toEqual([1, 2, 3]); - expect(n.num).toEqual(2.3); - expect(n.str).toEqual("test"); - expect(n.getAttribute("str")).toEqual("test"); - expect(n.s).toEqual(""); - expect(n.getAttribute("s")).toEqual(null); - expect(n.title).toEqual("testing!"); - expect(n.getAttribute("title")).toEqual("testing!"); - expect(n.value).toEqual(undefined); - expect(n.getAttribute("value")).toEqual(null); - }); - it("should provide a render method able to render element nodes with a value", () => { - const vnode = h3("input", { value: "test" }); - const createElement = jest.spyOn(document, "createElement"); - const node = vnode.render(); - expect(createElement).toHaveBeenCalledWith("input"); - expect(node.constructor).toEqual(HTMLInputElement); - expect(node.value).toEqual("test"); - }); + it("should provide a render method able to render element nodes with a value", () => { + let vnode = h("input", { value: "test" }); + const createElement = jest.spyOn(document, "createElement"); + let node = vnode.render(); + expect(createElement).toHaveBeenCalledWith("input"); + expect(node.constructor).toEqual(HTMLInputElement); + expect(node.value).toEqual("test"); + vnode = h("input", { value: null }); + node = vnode.render(); + expect(node.value).toEqual(""); + vnode = h("test", { value: 123 }); + node = vnode.render(); + expect(node.getAttribute("value")).toEqual("123"); + expect(node.value).toEqual(undefined); + }); - it("should provide a render method able to render element nodes with event handlers", () => { - const handler = () => { - console.log("test"); - }; - const vnode = h3("button", { onclick: handler }); - const button = document.createElement("button"); - const createElement = jest - .spyOn(document, "createElement") - .mockImplementationOnce(() => { - return button; - }); - const addEventListener = jest.spyOn(button, "addEventListener"); - const node = vnode.render(); - expect(createElement).toHaveBeenCalledWith("button"); - expect(node.constructor).toEqual(HTMLButtonElement); - expect(addEventListener).toHaveBeenCalledWith("click", handler); - }); + it("should provide a render method able to render element nodes with event handlers", () => { + const handler = () => { + console.log("test"); + }; + const vnode = h("button", { onclick: handler }); + const button = document.createElement("button"); + const createElement = jest + .spyOn(document, "createElement") + .mockImplementationOnce(() => { + return button; + }); + const addEventListener = jest.spyOn(button, "addEventListener"); + const node = vnode.render(); + expect(createElement).toHaveBeenCalledWith("button"); + expect(node.constructor).toEqual(HTMLButtonElement); + expect(addEventListener).toHaveBeenCalledWith("click", handler); + }); - it("it should provide a render method able to render elements with special attributes", () => { - const vnode = h3("div", { - id: "test", - style: "margin: auto;", - data: { test: "aaa" }, - $html: "<p>Hello!</p>", + it("it should provide a render method able to render elements with special props", () => { + const vnode = h("div", { + id: "test", + style: "margin: auto;", + data: { test: "aaa" }, + $html: "<p>Hello!</p>", + }); + const createElement = jest.spyOn(document, "createElement"); + const node = vnode.render(); + expect(createElement).toHaveBeenCalledWith("div"); + expect(node.constructor).toEqual(HTMLDivElement); + expect(node.style.cssText).toEqual("margin: auto;"); + expect(node.id).toEqual("test"); + expect(node.dataset["test"]).toEqual("aaa"); + expect(node.childNodes[0].textContent).toEqual("Hello!"); }); - const createElement = jest.spyOn(document, "createElement"); - const node = vnode.render(); - expect(createElement).toHaveBeenCalledWith("div"); - expect(node.constructor).toEqual(HTMLDivElement); - expect(node.style.cssText).toEqual("margin: auto;"); - expect(node.id).toEqual("test"); - expect(node.dataset["test"]).toEqual("aaa"); - expect(node.childNodes[0].textContent).toEqual("Hello!"); - }); - it("should provide a redraw method that is able to add new DOM nodes", () => { - const oldvnode = h3("div#test", h3("span")); - const newvnodeNoChildren = h3("div"); - const newvnode = h3("div", [h3("span#a"), h3("span")]); - const node = oldvnode.render(); - const span = node.childNodes[0]; - oldvnode.redraw({ node: node, vnode: newvnodeNoChildren }); - expect(oldvnode.children.length).toEqual(0); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(oldvnode).toEqual(newvnode); - expect(oldvnode.children.length).toEqual(2); - expect(node.childNodes.length).toEqual(2); - expect(node.childNodes[0].id).toEqual("a"); - expect(span).toEqual(node.childNodes[1]); - }); + it("should provide a redraw method that is able to add new DOM nodes", () => { + const oldvnode = h("div#test", h("span")); + const newvnodeNoChildren = h("div"); + const newvnode = h("div", [h("span#a"), h("span")]); + const node = oldvnode.render(); + const span = node.childNodes[0]; + oldvnode.redraw({ node: node, vnode: newvnodeNoChildren }); + expect(oldvnode.children.length).toEqual(0); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(oldvnode.children.length).toEqual(2); + expect(node.childNodes.length).toEqual(2); + expect(node.childNodes[0].id).toEqual("a"); + expect(span).toEqual(node.childNodes[1]); + }); - it("should provide a redraw method that is able to remove existing DOM nodes", () => { - let oldvnode = h3("div", [h3("span#a"), h3("span")]); - let newvnode = h3("div", [h3("span")]); - let node = oldvnode.render(); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(oldvnode).toEqual(newvnode); - expect(oldvnode.children.length).toEqual(1); - expect(node.childNodes.length).toEqual(1); - oldvnode = h3("div.test-children", [h3("span.a"), h3("span.b")]); - node = oldvnode.render(); - newvnode = h3("div.test-children", [h3("div.c")]); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(oldvnode).toEqual(newvnode); - expect(oldvnode.children.length).toEqual(1); - expect(node.childNodes.length).toEqual(1); - expect(oldvnode.children[0].classList[0]).toEqual("c"); - }); + it("should provide a redraw method that is able to remove existing DOM nodes", () => { + let oldvnode = h("div", [h("span#a"), h("span")]); + let newvnode = h("div", [h("span")]); + let node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(oldvnode.children.length).toEqual(1); + expect(node.childNodes.length).toEqual(1); + oldvnode = h("div.test-children", [h("span.a"), h("span.b")]); + node = oldvnode.render(); + newvnode = h("div.test-children", [h("div.c")]); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(oldvnode.children.length).toEqual(1); + expect(node.childNodes.length).toEqual(1); + expect(oldvnode.children[0].classList[0]).toEqual("c"); + }); - it("should provide a redraw method that is able to figure out differences in children", () => { - const oldvnode = h3("div", [h3("span", "a"), h3("span"), h3("span", "b")]); - const newvnode = h3("div", [ - h3("span", "a"), - h3("span", "c"), - h3("span", "b"), - ]); - const node = oldvnode.render(); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(node.childNodes[1].textContent).toEqual("c"); - }); + it("should provide a redraw method that is able to figure out differences in children", () => { + const oldvnode = h("div", [h("span", "a"), h("span"), h("span", "b")]); + const newvnode = h("div", [ + h("span", "a"), + h("span", "c"), + h("span", "b"), + ]); + const node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(node.childNodes[1].textContent).toEqual("c"); + }); - it("should provide a redraw method that is able to figure out differences in existing children", () => { - const oldvnode = h3("div", [ - h3("span.test", "a"), - h3("span.test", "b"), - h3("span.test", "c"), - ]); - const newvnode = h3("div", [ - h3("span.test", "a"), - h3("span.test1", "b"), - h3("span.test", "c"), - ]); - const node = oldvnode.render(); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(node.childNodes[0].classList[0]).toEqual("test"); - expect(node.childNodes[1].classList[0]).toEqual("test1"); - expect(node.childNodes[2].classList[0]).toEqual("test"); - }); + it("should provide a redraw method that is able to figure out differences in existing children", () => { + const oldvnode = h("div", [ + h("span.test", "a"), + h("span.test", "b"), + h("span.test", "c"), + ]); + const newvnode = h("div", [ + h("span.test", "a"), + h("span.test1", "b"), + h("span.test", "c"), + ]); + const node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(node.childNodes[0].classList[0]).toEqual("test"); + expect(node.childNodes[1].classList[0]).toEqual("test1"); + expect(node.childNodes[2].classList[0]).toEqual("test"); + }); - it("should provide a redraw method that is able to update different attributes", () => { - const oldvnode = h3("span", { title: "a", something: "b" }); - const newvnode = h3("span", { title: "b", id: "bbb" }); - const node = oldvnode.render(); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(oldvnode).toEqual(newvnode); - expect(node.getAttribute("title")).toEqual("b"); - expect(node.getAttribute("id")).toEqual("bbb"); - expect(node.hasAttribute("something")).toEqual(false); - }); + it("should provide a redraw method that is able to update different props", () => { + const oldvnode = h("span", { title: "a", something: "b" }); + const newvnode = h("span", { title: "b", id: "bbb" }); + const node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(node.getAttribute("title")).toEqual("b"); + expect(node.getAttribute("id")).toEqual("bbb"); + expect(node.hasAttribute("something")).toEqual(false); + }); - it("should provide a redraw method that is able to update different classes", () => { - const oldvnode = h3("span.a.b", { title: "b" }); - const newvnode = h3("span.a.c", { title: "b" }); - const node = oldvnode.render(); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(oldvnode).toEqual(newvnode); - expect(node.classList.value).toEqual("a c"); - }); + it("should provide a redraw method that is able to update different classes", () => { + const oldvnode = h("span.a.b", { title: "b" }); + const newvnode = h("span.a.c", { title: "b" }); + const node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(node.classList.value).toEqual("a c"); + }); - it("should provide redraw method to detect changed nodes if they have different elements", () => { - const oldvnode = h3("span.c", { title: "b" }); - const newvnode = h3("div.c", { title: "b" }); - const container = document.createElement("div"); - const node = oldvnode.render(); - container.appendChild(node); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(node).not.toEqual(container.childNodes[0]); - expect(node.constructor).toEqual(HTMLSpanElement); - expect(container.childNodes[0].constructor).toEqual(HTMLDivElement); - }); + it("should provide redraw method to detect changed nodes if they have different elements", () => { + const oldvnode = h("span.c", { title: "b" }); + const newvnode = h("div.c", { title: "b" }); + const container = document.createElement("div"); + const node = oldvnode.render(); + container.appendChild(node); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(node).not.toEqual(container.childNodes[0]); + expect(node.constructor).toEqual(HTMLSpanElement); + expect(container.childNodes[0].constructor).toEqual(HTMLDivElement); + }); - it("should provide redraw method to detect position changes in child nodes", () => { - const v1 = h3("ul", [h3("li.a"), h3("li.b"), h3("li.c"), h3("li.d")]); - const v2 = h3("ul", [h3("li.c"), h3("li.b"), h3("li.a"), h3("li.d")]); - const n = v1.render(); - expect(n.childNodes[0].classList[0]).toEqual("a"); - v1.redraw({ node: n, vnode: v2 }); - expect(n.childNodes[0].classList[0]).toEqual("c"); - }); + it("should provide redraw method to detect position changes in child nodes", () => { + const v1 = h("ul", [h("li.a"), h("li.b"), h("li.c"), h("li.d")]); + const v2 = h("ul", [h("li.c"), h("li.b"), h("li.a"), h("li.d")]); + const n = v1.render(); + expect(n.childNodes[0].classList[0]).toEqual("a"); + v1.redraw({ node: n, vnode: v2 }); + expect(n.childNodes[0].classList[0]).toEqual("c"); + }); - it("should provide redraw method to detect changed nodes if they have different node types", () => { - const oldvnode = h3("span.c", { title: "b" }); - const newvnode = h3({ type: "#text", value: "test" }); - const container = document.createElement("div"); - const node = oldvnode.render(); - container.appendChild(node); - expect(node.constructor).toEqual(HTMLSpanElement); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(node).not.toEqual(container.childNodes[0]); - expect(container.childNodes[0].data).toEqual("test"); - }); + it("should optimize insertion and deletions when redrawing if all old/new children exist", () => { + const v = h("div", h("a"), h("d")); + const vnode = h("div", h("a"), h("b"), h("c"), h("d")); + const node = v.render(); + v.redraw({ node, vnode }); + expect(v.children.length).toEqual(4); + }); - it("should provide redraw method to detect changed nodes if they have different text", () => { - const oldvnode = h3({ type: "#text", value: "test1" }); - const newvnode = h3({ type: "#text", value: "test2" }); - const container = document.createElement("div"); - const node = oldvnode.render(); - container.appendChild(node); - expect(node.data).toEqual("test1"); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(container.childNodes[0].data).toEqual("test2"); - }); + it("should provide redraw method to detect changed nodes if they have different node types", () => { + const oldvnode = h("span.c", { title: "b" }); + const newvnode = h({ type: "#text", value: "test" }); + const container = document.createElement("div"); + const node = oldvnode.render(); + container.appendChild(node); + expect(node.constructor).toEqual(HTMLSpanElement); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(node).not.toEqual(container.childNodes[0]); + expect(container.childNodes[0].data).toEqual("test"); + }); - it("should provide redraw method to detect changed nodes and recurse", () => { - const oldvnode = h3("ul.c", { title: "b" }, [ - h3("li#aaa"), - h3("li#bbb"), - h3("li#ccc"), - ]); - const newvnode = h3("ul.c", { title: "b" }, [h3("li#aaa"), h3("li#ccc")]); - const node = oldvnode.render(); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(oldvnode).toEqual(newvnode); - expect(node.childNodes.length).toEqual(2); - expect(node.childNodes[0].getAttribute("id")).toEqual("aaa"); - expect(node.childNodes[1].getAttribute("id")).toEqual("ccc"); - }); + it("should provide redraw method to detect changed nodes if they have different text", () => { + const oldvnode = h({ type: "#text", value: "test1" }); + const newvnode = h({ type: "#text", value: "test2" }); + const container = document.createElement("div"); + const node = oldvnode.render(); + container.appendChild(node); + expect(node.data).toEqual("test1"); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(container.childNodes[0].data).toEqual("test2"); + }); - it("should provide a redraw method able to detect specific changes to style, data, value, attributes, $onrender and eventListeners", () => { - const fn = () => false; - const oldvnode = h3("input", { - style: "margin: auto;", - data: { a: 111, b: 222, d: 444 }, - value: "Test...", - title: "test", - label: "test", - onkeydown: () => true, - onclick: () => true, - onkeypress: () => true, + it("should provide redraw method to detect changed nodes and recurse", () => { + const oldvnode = h("ul.c", { title: "b" }, [ + h("li#aaa"), + h("li#bbb"), + h("li#ccc"), + ]); + const newvnode = h("ul.c", { title: "b" }, [h("li#aaa"), h("li#ccc")]); + const node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(node.childNodes.length).toEqual(2); + expect(node.childNodes[0].getAttribute("id")).toEqual("aaa"); + expect(node.childNodes[1].getAttribute("id")).toEqual("ccc"); }); - const newvnode = h3("input", { - style: false, - data: { a: 111, b: 223, c: 333 }, - title: "test #2", - label: "test", - placeholder: "test", - onkeydown: () => true, - onkeypress: () => false, - $onrender: () => true, - onhover: () => true, + + it("should provide a redraw method able to detect specific changes to style, data, value, props, $onrender and eventListeners", () => { + const fn = () => false; + const oldvnode = h("input", { + style: "margin: auto;", + data: { a: 111, b: 222, d: 444 }, + value: "Test...", + title: "test", + label: "test", + onkeydown: () => true, + onclick: () => true, + onkeypress: () => true, + }); + const newvnode = h("input", { + style: false, + data: { a: 111, b: 223, c: 333 }, + title: "test #2", + label: "test", + placeholder: "test", + onkeydown: () => true, + onkeypress: () => false, + $onrender: () => true, + onhover: () => true, + }); + const container = document.createElement("div"); + const node = oldvnode.render(); + container.appendChild(node); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(node.style.cssText).toEqual(""); + expect(node.dataset["a"]).toEqual("111"); + expect(node.dataset["c"]).toEqual("333"); + expect(node.dataset["b"]).toEqual("223"); + expect(node.dataset["d"]).toEqual(undefined); + expect(node.getAttribute("title")).toEqual("test #2"); + expect(node.getAttribute("placeholder")).toEqual("test"); + expect(node.value).toEqual(""); }); - const container = document.createElement("div"); - const node = oldvnode.render(); - container.appendChild(node); - oldvnode.redraw({ node: node, vnode: newvnode }); - expect(oldvnode).toEqual(newvnode); - expect(node.style.cssText).toEqual(""); - expect(node.dataset["a"]).toEqual("111"); - expect(node.dataset["c"]).toEqual("333"); - expect(node.dataset["b"]).toEqual("223"); - expect(node.dataset["d"]).toEqual(undefined); - expect(node.getAttribute("title")).toEqual("test #2"); - expect(node.getAttribute("placeholder")).toEqual("test"); - expect(node.value).toEqual(""); - }); - it("should provide a redraw method able to detect changes in child content", () => { - const v1 = h3("ul", [h3("li", "a"), h3("li", "b")]); - const n1 = v1.render(); - const v2 = h3("ul", { - $html: "<li>a</li><li>b</li>", - $onrender: (node) => node.classList.add("test"), + it("should provide a redraw method able to detect changes in child content", () => { + const v1 = h("ul", [h("li", "a"), h("li", "b")]); + const n1 = v1.render(); + const v2 = h("ul", { + $html: "<li>a</li><li>b</li>", + $onrender: (node) => node.classList.add("test"), + }); + const v3 = h("ul", [h("li", "a")]); + const v4 = h("ul", [h("li", "b")]); + const n2 = v2.render(); + const n3 = v3.render(); + expect(n2.childNodes[0].childNodes[0].data).toEqual( + n1.childNodes[0].childNodes[0].data + ); + v1.redraw({ node: n1, vnode: v2 }); + expect(n1.classList[0]).toEqual("test"); + expect(v1).toEqual(v2); + v3.redraw({ node: n3, vnode: v4 }); + expect(v3).toEqual(v4); }); - const v3 = h3("ul", [h3("li", "a")]); - const v4 = h3("ul", [h3("li", "b")]); - const n2 = v2.render(); - const n3 = v3.render(); - expect(n2.childNodes[0].childNodes[0].data).toEqual( - n1.childNodes[0].childNodes[0].data - ); - v1.redraw({ node: n1, vnode: v2 }); - expect(n1.classList[0]).toEqual("test"); - expect(v1).toEqual(v2); - v3.redraw({ node: n3, vnode: v4 }); - expect(v3).toEqual(v4); - }); - it("should execute $onrender callbacks whenever a child node is added to the DOM", async () => { - let n; - const $onrender = (node) => { - n = node; - }; - const vn1 = h3("ul", [h3("li")]); - const vn2 = h3("ul", [h3("li"), h3("li.vn2", { $onrender })]); - const n1 = vn1.render(); - vn1.redraw({ node: n1, vnode: vn2 }); - expect(n.classList.value).toEqual("vn2"); - const vn3 = h3("ul", [h3("span.vn3", { $onrender })]); - vn1.redraw({ node: n1, vnode: vn3 }); - expect(n.classList.value).toEqual("vn3"); - const rc = () => h3("div.rc", { $onrender }); - await h3.init(rc); - expect(n.classList.value).toEqual("rc"); - const rc2 = () => vn2; - await h3.init(rc2); - expect(n.classList.value).toEqual("vn2"); - }); + it("should execute $onrender callbacks whenever a child node is added to the DOM", async () => { + let n; + const $onrender = (node) => { + n = node; + }; + const vn1 = h("ul", [h("li")]); + const vn2 = h("ul", [h("li"), h("li.vn2", { $onrender })]); + const n1 = vn1.render(); + vn1.redraw({ node: n1, vnode: vn2 }); + expect(n.classList.value).toEqual("vn2"); + const vn3 = h("ul", [h("span.vn3", { $onrender })]); + vn1.redraw({ node: n1, vnode: vn3 }); + expect(n.classList.value).toEqual("vn3"); + const rc = () => h("div.rc", { $onrender }); + await h3.init(rc); + expect(n.classList.value).toEqual("rc"); + const rc2 = () => vn2; + await h3.init(rc2); + expect(n.classList.value).toEqual("vn2"); + }); });
@@ -7259,14 +7259,11 @@ </li>
<li><a href="#Key-Concepts">Key Concepts</a> <ul> <li><a href="#HyperScript">HyperScript</a></li> - <li><a href="#Components">Components</a></li> + <li><a href="#Component">Component</a></li> + <li><a href="#Router">Router</a></li> + <li><a href="#Screen">Screen</a></li> <li><a href="#Store">Store</a></li> - <li><a href="#Modules">Modules</a></li> - <li><a href="#Router">Router</a> - <ul> - <li><a href="#Route-Components">Route Components</a></li> - </ul> - </li> + <li><a href="#Module">Module</a></li> <li><a href="#How-everything-works...">How everything works...</a></li> </ul> </li>@@ -7274,13 +7271,13 @@ <li><a href="#Tutorial">Tutorial</a>
<ul> <li><a href="#Create-a-simple-HTML-file">Create a simple HTML file</a></li> <li><a href="#Create-a-single-page-application">Create a single-page application</a></li> - <li><a href="#Initialization-and-post-redraw-operations">Initialization and post-redraw operations</a></li> + <li><a href="#Initialization">Initialization</a></li> <li><a href="#Next-steps">Next steps</a></li> </ul> </li> <li><a href="#API">API</a> <ul> - <li><a href="#h3(selector:-string,-attributes:-object,-children:-array)">h3(selector: string, attributes: object, children: array)</a> + <li><a href="#h(selector:-string,-attributes:-object,-children:-array)">h(selector: string, attributes: object, children: array)</a> <ul> <li><a href="#Create-an-element,-with-an-ID,-classes,-attributes-and-children">Create an element, with an ID, classes, attributes and children</a></li> <li><a href="#Create-an-empty-element">Create an empty element</a></li>@@ -7291,6 +7288,7 @@ <li><a href="#Render-child-components">Render child components</a></li>
<li><a href="#Special-attributes">Special attributes</a></li> </ul> </li> + <li><a href="#h3.screen({setup,-display,-teardown})">h3.screen({setup, display, teardown})</a></li> <li><a href="#h3.dispatch(event:-string,-data:-any)">h3.dispatch(event: string, data: any)</a></li> <li><a href="#h3.init(config:-object)">h3.init(config: object)</a></li> <li><a href="#h3.navigateTo(path:-string,-params:-object)">h3.navigateTo(path: string, params: object)</a></li>@@ -7326,9 +7324,9 @@
<p>H3 is also:</p> <ul> -<li><strong>tiny</strong>, less than 4KB minified gzipped.</li> +<li><strong>tiny</strong>, less than 4KB minified and gzipped.</li> <li><strong>modern</strong>, in the sense that it runs only in modern browsers (latest versions of Chrome, Firefox, Edge & similar).</li> -<li><strong>easy</strong> to learn, its API is comprised of only six methods and two properties.</li> +<li><strong>easy</strong> to learn, its API is comprised of only seven methods and two properties.</li> </ul>@@ -7337,7 +7335,7 @@ <h3>I’m sold! Where can I get it?<a href="#document-top" title="Go to top"></a></h3>
<p>Here, look, it’s just one file:</p> -<p><a href="https://raw.githubusercontent.com/h3rald/h3/v0.9.0/h3.js" target="_blank" class="button primary">Download v0.9.0 (Impeccable Iconian)</a></p> +<p><a href="https://raw.githubusercontent.com/h3rald/h3/v0.10.0/h3.js" target="_blank" class="button primary">Download v0.10.0 (Jittery Jem'Hadar)</a></p> <p><small>Or get the minified version <a href="https://raw.githubusercontent.com/h3rald/h3/v0.9.0/h3.min.js">here</a>.</small></p>@@ -7349,7 +7347,7 @@
<p>Here’s an example of an extremely minimal SPA created with H3:</p> <pre><code class="js">import h3 from "./h3.js"; -h3.init(() => h3("h1", "Hello, World!")); +h3.init(() => h("h1", "Hello, World!")); </code></pre> <p>This will render a <code>h1</code> tag within the document body, containing the text <code>"Hello, World!"</code>.</p>@@ -7425,10 +7423,10 @@ <pre><code class="js">// A simple component printing the current date and time
// Pressig the Refresh button causes the application to redraw // And updates the displayed date/dime. const Page = () => { - return h3("main", [ - h3("h1", "Welcome!"), - h3("p", `The current date and time is ${new Date()}`), - h3("button", { + return h("main", [ + h("h1", "Welcome!"), + h("p", `The current date and time is ${new Date()}`), + h("button", { onclick: () => h3.redraw() }, "Refresh") ]);@@ -7449,15 +7447,13 @@ <h3>HyperScript<a href="#document-top" title="Go to top"></a></h3>
<p>H3 uses a <a href="https://openbase.io/js/hyperscript">HyperScript</a>-like syntax to create HTML elements in pure JavaScript. No, you are actually creating Virtual DOM nodes with it but it can be easier to think about them as HTML elements, or better, something that <em>eventually</em> will be rendered as an HTML element.</p> -<p>The main difference between H3’s HyperScript implementation and others is that it uses <strong>h3</strong> as the main constructor to create nodes. HyperScript uses <strong>h</strong>, Mithril uses <strong>m</strong>, …kind of an obvious choice if you ask me. If you don’t like it, you can rename it to <em>piripicchio</em> if you want, and it will <em>still</em> be used in the same way.</p> - <p>How, you ask? Like this:</p> -<pre><code class="js">h3("div.test", [ - h3("ul", [ - h3("li", "This is..."), - h3("li", "...a simple..."), - h3("li", "unordered list.") +<pre><code class="js">h("div.test", [ + h("ul", [ + h("li", "This is..."), + h("li", "...a simple..."), + h("li", "unordered list.") ]) ]); </code></pre>@@ -7475,8 +7471,8 @@ </code></pre>
<p>Simple enough. Yes there are some quirks to it, but check the API or Usage docs for those.</p> -<a name="Components"></a> -<h3>Components<a href="#document-top" title="Go to top"></a></h3> +<a name="Component"></a> +<h3>Component<a href="#document-top" title="Go to top"></a></h3> <p>In H3, a component is a function that returns a Virtual Node or a string (that will be treated as a textual DOM node).</p>@@ -7484,33 +7480,12 @@ <p>Yes that’s it. An example? here:</p>
<pre><code class="js">let count = 0; const CounterButton = () => { - return h3("button", { + return h("button", { onclick: () => count +=1 && h3.redraw() }, `You clicked me ${count} times.`); } </code></pre> -<a name="Store"></a> -<h3>Store<a href="#document-top" title="Go to top"></a></h3> - -<p>H3 essentially uses something very, <em>very</em> similar to <a href="https://github.com/storeon/storeon">Storeon</a> for state management <em>and</em> also as a very simple client-side event dispatcher/subscriber (seriously, it is virtually the same code as Storeon). Typically you’ll only use the default store created by H3 upon initialization, and you’ll use the <code>h3.dispatch()</code> and <code>h3.on()</code> methods to dispatch and subscribe to events.</p> - -<p>The current application state is accessible via the <code>h3.state</code> property.</p> - -<a name="Modules"></a> -<h3>Modules<a href="#document-top" title="Go to top"></a></h3> - -<p>The <code>h3.init()</code> method takes an array of <em>modules</em> that can be used to manipulate the application state when specific events are received. A simple module looks like this:</p> - -<pre><code class="js">const error = () => { - h3.on("$init", () => ({ displayEmptyTodoError: false })); - h3.on("error/clear", (state) => ({ displayEmptyTodoError: false })); - h3.on("error/set", (state) => ({ displayEmptyTodoError: true })); -}; -</code></pre> - -<p>Essentially a module is just a function that typically is meant to run only once to define one or more event subscriptions. Modules are the place where you should handle state changes in your application.</p> - <a name="Router"></a> <h3>Router<a href="#document-top" title="Go to top"></a></h3>@@ -7518,24 +7493,45 @@ <p>H3 comes with a very minimal but fully functional URL fragment router. You create your application routes when initializing your application, and you can navigate to them using ordinary <code>href</code> links or programmatically using the <code>h3.navigateTo</code> method.</p>
<p>The current route is always accessible via the <code>h3.route</code> property.</p> -<a name="Route-Components"></a> -<h4>Route Components<a href="#document-top" title="Go to top"></a></h4> +<a name="Screen"></a> +<h3>Screen<a href="#document-top" title="Go to top"></a></h3> -<p>A route component is a top-level component that handles a route. Unlike ordinary components, route components:</p> +<p>A screen is a top-level component that handles a route. Unlike ordinary components, screens:</p> <ul> -<li>may have a dedicated <em>setup</em> (after the route component is added to the DOM) and <em>teardown</em> phase (after the route component is removed from the DOM and before the new route component is loaded).</li> -<li>may have built-in local state, initialized during setup and (typically) destroyed during teardown. Such state is passed as the first (and only) parameter of the route component when executed.</li> +<li>may have a dedicated <em>setup</em> (after the screen is added to the DOM) and <em>teardown</em> phase (after the screen is removed from the DOM and before the new screen is loaded).</li> +<li>may have built-in local state, initialized during setup and (typically) destroyed during teardown. Such state is passed as the first (and only) parameter of the screen when executed.</li> </ul> -<p>Route components are stll created using ordinary function returning a VNode, but you can optionally define a <strong>setup</strong> and a <strong>teardown</strong> async methods on them (functions are objects in JavaScript after all…) to be executed during each corresponding phase.</p> +<p>Screens are typically created using the <strong>h3.screen</strong> shorthand method, but they can stll created using an ordinary function returning a VNode, but you can optionally define a <strong>setup</strong> and a <strong>teardown</strong> async methods on them (functions are objects in JavaScript after all…) to be executed during each corresponding phase.</p> <p>Note that: * Both the <strong>setup</strong> method take an object as a parameter, representing the component state. Such object will be empty the first time the <strong>setup</strong> method is called for a given component, but it may contain properties not removed during subsequent teardowns. * The <strong>teardown</strong> method can return an object, which will be retained as component state. If however nothing is returned, the component state object is emptied. * Both methods can be asynchronous, in which case H3 will wait for their completion before proceeding.</p> +<a name="Store"></a> +<h3>Store<a href="#document-top" title="Go to top"></a></h3> + +<p>H3 essentially uses something very, <em>very</em> similar to <a href="https://github.com/storeon/storeon">Storeon</a> for state management <em>and</em> also as a very simple client-side event dispatcher/subscriber (seriously, it is virtually the same code as Storeon). Typically you’ll only use the default store created by H3 upon initialization, and you’ll use the <code>h3.dispatch()</code> and <code>h3.on()</code> methods to dispatch and subscribe to events.</p> + +<p>The current application state is accessible via the <code>h3.state</code> property.</p> + +<a name="Module"></a> +<h3>Module<a href="#document-top" title="Go to top"></a></h3> + +<p>The <code>h3.init()</code> method takes an array of <em>modules</em> that can be used to manipulate the application state when specific events are received. A simple module looks like this:</p> + +<pre><code class="js">const error = () => { + h3.on("$init", () => ({ displayEmptyTodoError: false })); + h3.on("error/clear", (state) => ({ displayEmptyTodoError: false })); + h3.on("error/set", (state) => ({ displayEmptyTodoError: true })); +}; +</code></pre> + +<p>Essentially a module is just a function that typically is meant to run only once to define one or more event subscriptions. Modules are the place where you should handle state changes in your application.</p> + <a name="How-everything-works..."></a> <h3>How everything works…<a href="#document-top" title="Go to top"></a></h3>@@ -7551,9 +7547,9 @@ <li>Any <em>Module</em> specified when calling <code>h3.init()</code> is executed.</li>
<li>The <strong>$init</strong> event is dispatched.</li> <li>The <em>preStart</em> function (if specified when calling <code>h3.init()</code>) is executed.</li> <li>The <em>Router</em> is initialized and started.</li> -<li>The <strong>setup()</strong> method of the matching Route Component is called (if any).</li> +<li>The <strong>setup()</strong> method of the matching Screen is called (if any).</li> <li>The <strong>$navigation</strong> event is dispatched.</li> -<li>The <em>Route Component</em> matching the current route and all its child components are rendered for the first time.</li> +<li>The <em>Screen</em> matching the current route and all its child components are rendered for the first time.</li> <li>The <strong>$redraw</strong> event is dispatched.</li> </ol>@@ -7570,12 +7566,12 @@ <p>Similarly, whenever the <code>h3.navigateTo()</code> method is called (typically within a component), or the URL fragment changes:</p>
<ol> <li>The <em>Router</em> processes the new path and determine which component to render based on the routing configuration.</li> -<li>The <strong>teardow()</strong> method of the current Route Component is called (if any).</li> -<li>The <strong>setup()</strong> method of the new matching Route Component is called (if any).</li> +<li>The <strong>teardow()</strong> method of the current Screen is called (if any).</li> +<li>The <strong>setup()</strong> method of the new matching Screen is called (if any).</li> <li>All DOM nodes within the scope of the routing are removed, all components are removed.</li> <li>The <strong>$navigation</strong> event is dispatched.</li> <li>All DOM nodes are removed.</li> -<li>The <em>Route Component</em> matching the new route and all its child components are rendered.</li> +<li>The <em>Screen</em> matching the new route and all its child components are rendered.</li> <li>The <strong>$redraw</strong> event is dispatched.</li> </ol>@@ -7607,15 +7603,18 @@ <p>Start by creating a simple HTML file. Place a script loading the entry point of your application within the <code>body</code> and set its type to <code>module</code>.</p>
<p>This will let you load an ES6 file containing imports to other files… it works in all major browsers, but it doesn’t work in IE (but we don’t care about that, do we?).</p> -<pre><code class="html"><!doctype html> +<pre><code class="html"><!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>H3</title> - <meta name="description" content="A bare-bones client-side web microframework" /> + <meta + name="description" + content="A bare-bones client-side web microframework" + /> <meta name="author" content="Fabio Cevasco" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="shortcut icon" href="favicon.png" type="image/png"> + <link rel="shortcut icon" href="favicon.png" type="image/png" /> <link rel="stylesheet" href="css/mini-default.css" /> <link rel="stylesheet" href="css/prism.css" /> <link rel="stylesheet" href="css/style.css" />@@ -7635,7 +7634,7 @@ <p>Normally you’d have several components, at least one file containing modules to manage the application state, etc. (see the <a href="https://github.com/h3rald/h3/tree/master/docs/example">todo list example</a>), but in this case a single component is sufficient.</p>
<p>Start by importing all the JavaScript modules you need:</p> -<pre><code class="js">import h3 from "./h3.js"; +<pre><code class="js">import { h3, h } from "./h3.js"; import marked from "./vendor/marked.js"; import Prism from "./vendor/prism.js"; </code></pre>@@ -7646,6 +7645,7 @@ <pre><code class="js">const labels = {
overview: "Overview", "quick-start": "Quick Start", "key-concepts": "Key Concepts", + "best-practices": "Best Practices", tutorial: "Tutorial", api: "API", about: "About",@@ -7654,14 +7654,11 @@ </code></pre>
<p>We are going to store the HTML contents of each page in an Object, and we’re going to need a simple function to <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">fetch</a> the Markdown file and render it as HTML:</p> -<pre><code class="js">const pages = {}; - -const fetchPage = async (pages, id, md) => { +<pre><code class="js">const fetchPage = async ({ pages, id, md }) => { if (!pages[id]) { const response = await fetch(md); const text = await response.text(); pages[id] = marked(text); - h3.redraw(); } }; </code></pre>@@ -7670,49 +7667,64 @@ <p>Basically this function is going to be called when you navigate to each page, and it:</p>
<ol> <li>fetches the content of the requested file (<code>md</code>))</li> -<li>renders the Markdown code into HTML using marked, and stores it in the <code>pages</code> object</li> -<li>Triggers a redraw of the application</li> +<li>renders the Markdown code into HTML using the <em>marked</em> library, and stores it in the <code>pages</code> object</li> </ol> -<p>We are gonna use our <code>fetchPage</code> function inside the main component of our app, <code>Page</code>:</p> +<p>We are gonna use our <code>fetchPage</code> function inside the <code>setup</code> of the main (and only) screen of our app, <code>Page</code>:</p> -<pre><code class="js">const Page = () => { - const id = h3.route.path.slice(1); - const ids = Object.keys(labels); - const md = ids.includes(id) ? `md/${id}.md` : `md/overview.md`; - fetchPage(pages, id, md); - return h3("div.page", [ - Header, - h3("div.row", [ - h3("input#drawer-control.drawer", { type: "checkbox" }), - Navigation(id, ids), - Content(pages[id]), - Footer, - ]), - ]); -}; +<pre><code class="js">const Page = h3.screen({ + setup: async (state) => { + state.pages = {}; + state.id = h3.route.path.slice(1); + state.ids = Object.keys(labels); + state.md = state.ids.includes(state.id) + ? `md/${state.id}.md` + : `md/overview.md`; + await fetchPage(state); + }, + display: (state) => { + return h("div.page", [ + Header, + h("div.row", [ + h("input#drawer-control.drawer", { type: "checkbox" }), + Navigation(state.id, state.ids), + Content(state.pages[state.id]), + Footer, + ]), + ]); + }, + teardown: (state) => state, +}); </code></pre> -<p>The main responsibility of this component is to fetch the Markdown content and render the whole page, but note how the rendering different portions of the page are delegated to different components: <code>Header</code>, <code>Navigation</code>, <code>Content</code>, and <code>Footer</code>.</p> +<p>Note that this screen has a <code>setup</code>, a <code>display</code> and a <code>teardown</code> method, both taking <code>state</code> as parameter. In H3, screens are nothing but stateful components that are used to render the whole page of the application, and are therefore typically redered when navigating to a new route.</p> + +<p>The <code>state</code> parameter is nothing but an empty object that can be used to store data that will be accessible to the <code>setup</code>, <code>display</code> and <code>teardown</code> methods, and (typically) will be destroyed when another screen is rendered.</p> + +<p>The <code>setup</code> function allows you to perform some operations that should take place <em>before</em> the screen is rendered. In this case, we want to fetch the page contents (if necessary) beforehand to avoid displaying a spinner while the content is being loaded. Note that the <code>setup</code> method can be asynchronous, and in this case the <code>display</code> method will not be called until all asynchronous operations have been completed (assuming you are <code>await</code>ing them).</p> + +<p>The <code>teardown</code> function in this case only makes sure that the existing screen state (in particular any loaded markdown page) will be passed on to the next screen during navigation (which, in this case, is still the <code>Page</code> screen), so that existing pages will not be fetched again.</p> + +<p>The main responsibility of this screen is to fetch the Markdown content and render the whole page, but note how the rendering different portions of the page are delegated to different components: <code>Header</code>, <code>Navigation</code>, <code>Content</code>, and <code>Footer</code>.</p> <p>The <code>Header</code> and <code>Footer</code> components are very simple: their only job is to render static content:</p> <pre><code class="js">const Header = () => { - return h3("header.row.sticky", [ - h3("a.logo.col-sm-1", { href: "#/" }, [ - h3("img", { alt: "H3", src: "images/h3.svg" }), + return h("header.row.sticky", [ + h("a.logo.col-sm-1", { href: "#/" }, [ + h("img", { alt: "H3", src: "images/h3.svg" }), ]), - h3("div.version.col-sm.col-md", [ - h3("div.version-number", "v0.9.0"), - h3("div.version-label", "“Impeccable Iconian“"), + h("div.version.col-sm.col-md", [ + h("div.version-number", "v0.10.0"), + h("div.version-label", "“Jittery Jem'Hadar“"), ]), - h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), + h("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), ]); }; const Footer = () => { - return h3("footer", [h3("div", "© 2020 Fabio Cevasco")]); + return h("footer", [h("div", "© 2020 Fabio Cevasco")]); }; </code></pre>@@ -7728,23 +7740,24 @@ <p>…and it uses this information to create the site navigation menu dynamically:</p>
<pre><code class="js">const Navigation = (id, ids) => { const menu = ids.map((p) => - h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) + h(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) ); - return h3("nav#navigation.col-md-3", [ - h3("label.drawer-close", { for: "drawer-control" }), + return h("nav#navigation.col-md-3", [ + h("label.drawer-close", { for: "drawer-control" }), ...menu, ]); }; </code></pre> -<p>Finally, the <code>Content</code> component optionally takes a string containing the HTML of the page content to render. If no content is provided, it will display a loading spinner, otherwise it will render the content by using the special <code>$html</code> attribute that can be used to essentially set the <code>innerHTML</code> of an element:</p> +<p>Finally, the <code>Content</code> component takes a string containing the HTML of the page content to render using the special <code>$html</code> attribute that can be used to essentially set the <code>innerHTML</code> property of an element:</p> <pre><code class="js">const Content = (html) => { - const content = html - ? h3("div.content", { $html: html }) - : h3("div.spinner-container", h3("span.spinner")); - return h3("main.col-sm-12.col-md-9", [ - h3("div.card.fluid", h3("div.section", content)), + const content = h("div.content", { $html: html }); + return h("main.col-sm-12.col-md-9", [ + h( + "div.card.fluid", + h("div.section", { $onrender: () => Prism.highlightAll() }, content) + ), ]); }; </code></pre>@@ -7757,27 +7770,24 @@ <p>In a similar way, other well-known pages can easily be mapped to IDs, but it is also important to handle <em>unknown</em> pages (technically I could even pass an URL to a different site containing a malicious markdown page and have it rendered!), and if a page passed in the URL fragment is not present in the <code>labels</code> Object, the Overview page will be rendered instead.</p>
<p>This feature is also handy to automatically load the Overview when no fragment is specified.</p> -<a name="Initialization-and-post-redraw-operations"></a> -<h3>Initialization and post-redraw operations<a href="#document-top" title="Go to top"></a></h3> +<p>What is that weird <code>$onrender</code> property you ask? Well, that’s a H3-specific callback that will be executed whenever the corresponding DOM node is rendered… that’s essentially the perfect place to for executing operations that must be perform when the DOM is fully available, like highlighting our code snippets using <em>Prism</em> in this case.</p> + +<a name="Initialization"></a> +<h3>Initialization<a href="#document-top" title="Go to top"></a></h3> <p>Done? Not quite. We need to initialize the SPA by passing the <code>Page</code> component to the <code>h3.init()</code> method to trigger the first rendering:</p> <pre><code class="js">h3.init(Page); </code></pre> -<p>And that’s it. Noooo wait, what about syntax highlighting? That needs to be applied <em>after</em> the HTML markup is rendered. How can we manage that?</p> - -<p>Easy enough, add a handler to be executed whenever the SPA is redrawn:</p> - -<pre><code class="js">h3.on("$redraw", () => Prism.highlightAll()); -</code></pre> +<p>And that’s it. Now, keep in mind that this is the <em>short</em> version of initialization using a single component and a single route, but still, that’s good enough for our use case.</p> <a name="Next-steps"></a> <h3>Next steps<a href="#document-top" title="Go to top"></a></h3> <p>Made it this far? Good. Wanna know more? Have a look at the code of the <a href="https://github.com/h3rald/h3/tree/master/docs/example">todo list example</a> and try it out <a href="https://h3.js.org/example/index.html">here</a>.</p> -<p>Once you feel more comfortable and you are ready to dive into a more complex application, featuring different routes, route components, forms, confirmation messages, plenty of third-party components etc., have a look at <a href="https://github.com/h3rald/litepad">LitePad</a>. You can see it in action here: <a href="https://litepad.h3rald.com/">litepad.h3rald.com</a>.</p> +<p>Once you feel more comfortable and you are ready to dive into a more complex application, featuring different routes, screens, forms, validation, confirmation messages, plenty of third-party components etc., have a look at <a href="https://github.com/h3rald/litepad">LitePad</a>. You can see it in action here: <a href="https://litepad.h3rald.com/">litepad.h3rald.com</a>.</p> <p>Note: the LitePad online demo will store all its data in localStorage.</p>@@ -7786,10 +7796,10 @@ <h2>API<a href="#document-top" title="Go to top"></a></h2>
<p>The core of the H3 API is comprised of the following six methods and two properties.</p> -<a name="h3(selector:-string,-attributes:-object,-children:-array)"></a> -<h3>h3(selector: string, attributes: object, children: array)<a href="#document-top" title="Go to top"></a></h3> +<a name="h(selector:-string,-attributes:-object,-children:-array)"></a> +<h3>h(selector: string, attributes: object, children: array)<a href="#document-top" title="Go to top"></a></h3> -<p>The <code>h3</code> object is also used as a constructor for Virtual DOM Nodes (VNodes). It can actually take from one to three arguments used to configure the resulting node.</p> +<p>The <code>h</code> function is a constructor for Virtual DOM Nodes (VNodes). It can actually take from one to any number of arguments used to configure the resulting node.</p> <p>The best way to understand how it works is by providing a few different examples. Please note that in each example the corresponding <em>HTML</em> markup is provided, although such markup will actually be generated when the Virtual Node is rendered/redrawn.</p>@@ -7798,9 +7808,13 @@ <h4>Create an element, with an ID, classes, attributes and children<a href="#document-top" title="Go to top"></a></h4>
<p>This is a complete example showing how to create a link with an <code>href</code> attribute, an ID, two classes, and three child nodes.</p> -<pre><code class="js">h3("a#test-link.btn.primary", { - href: "#/test" -}, ["This is a ", h3("em", "test"), "link."]); +<pre><code class="js">h( + "a#test-link.btn.primary", + { + href: "#/test", + }, + ["This is a ", h("em", "test"), "link."] +); </code></pre> <p>↓</p>@@ -7813,7 +7827,7 @@
<a name="Create-an-empty-element"></a> <h4>Create an empty element<a href="#document-top" title="Go to top"></a></h4> -<pre><code class="js">h3("div"); +<pre><code class="js">h("div"); </code></pre> <p>↓</p>@@ -7824,7 +7838,7 @@
<a name="Create-an-element-with-a-textual-child-node"></a> <h4>Create an element with a textual child node<a href="#document-top" title="Go to top"></a></h4> -<pre><code class="js">h3("span", "This is a test"); +<pre><code class="js">h("span", "This is a test"); </code></pre> <p>↓</p>@@ -7835,11 +7849,21 @@
<a name="Create-an-element-with-child-nodes"></a> <h4>Create an element with child nodes<a href="#document-top" title="Go to top"></a></h4> -<pre><code class="js">h3("ol", [ - h3("li", "Do this first."), - h3("li", "Then this."), - h3("li", "And finally this.") +<pre><code class="js">h("ol", [ + h("li", "Do this first."), + h("li", "Then this."), + h("li", "And finally this."), ]); +</code></pre> + +<p><em>or</em></p> + +<pre><code class="js">h( + "ol", + h("li", "Do this first."), + h("li", "Then this."), + h("li", "And finally this.") +); </code></pre> <p>↓</p>@@ -7855,11 +7879,15 @@ <a name="Render-a-component"></a>
<h4>Render a component<a href="#document-top" title="Go to top"></a></h4> <pre><code class="js">const TestComponent = () => { - return h3("button.test", { - onclick: () => alert("Hello!") - }, "Show Alert"); + return h( + "button.test", + { + onclick: () => alert("Hello!"), + }, + "Show Alert" + ); }; -h3(TestComponent); +h(TestComponent); </code></pre> <p>↓</p>@@ -7872,8 +7900,8 @@
<a name="Render-child-components"></a> <h4>Render child components<a href="#document-top" title="Go to top"></a></h4> -<pre><code class="js">const TestLi = (text) => h3("li.test", text); -h3("ul", ["A", "B", "C"].map(TestLi)); +<pre><code class="js">const TestLi = (text) => h("li.test", text); +h("ul", ["A", "B", "C"].map(TestLi)); </code></pre> <p>↓</p>@@ -7901,15 +7929,54 @@ <p>The <code>$html</code> and the <code>$onrender</code> special attributes should be used sparingly, and typically only when interfacing with third-party libraries that need access to the real DOM.</p>
<p>For example, consider the following code snippet that can be used to initialize the <a href="https://github.com/Inscryb/inscryb-markdown-editor">InscrybMDE</a> Markdown editor on a textarea element:</p> -<pre><code class="js">h3("textarea", { +<pre><code class="js">h("textarea", { $onrender: (element) => { const editor = new window.InscrybMDE({ - element + element, }); - } + }, }); </code></pre> +<a name="h3.screen({setup,-display,-teardown})"></a> +<h3>h3.screen({setup, display, teardown})<a href="#document-top" title="Go to top"></a></h3> + +<p>Creates a Screen by providing a (mandatory) <strong>display</strong> function used to render content, an an optional <strong>setup</strong> and <strong>teardown</strong> functions, executed before and after the display function respectively.</p> + +<p>Each of these functions takes a single <strong>screen</strong> parameter, which is initialized as an empty object before the setup, and is (optionally) returned by the teardown function should state be preserved across different screens.</p> + +<p>Consider the following example:</p> + +<pre><code class="js">h3.screen({ + setup: await (state) => { + state.data = state.data || {}; + state.id = h3.route.parts.id || 1; + const url = `http://dummy.restapiexample.com/api/v1/employee/${id}`; + state.data[id] = state.data[id] || await (await fetch(url)).json(); + }, + display(state) => { + const employee = state.data[state.id]; + if (!employee) { + return h("div.error", "Invalid Employee ID."); + } + return h("div.employee", + h("h2", "Employee Profile"), + h("dl", [ + h("dt", "Name:"), + h("dd", data.employee_name), + h("dt", "Salary:"), + h("dd", `${data.employee_salary} €`), + h("dt", "Age:"), + h("dd", data.employee_age), + ]) + ) + }, + teardown: (state) => ({ data: state.data }) +}) +</code></pre> + +<p>This example shows how to implement a simple component that renders an employee profile in the <code>display</code> function, fetches data (if necessary) in the <code>setup</code> function, and preserves data in the <code>teardown</code> function.</p> + <a name="h3.dispatch(event:-string,-data:-any)"></a> <h3>h3.dispatch(event: string, data: any)<a href="#document-top" title="Go to top"></a></h3>@@ -7960,7 +8027,7 @@ <p>Navigates to the specified path. Optionally, it is possibile to specify query string parameters as an object.</p>
<p>The following call causes the application to switch to the following URL: <code>#/posts/?orderBy=date&direction=desc</code>.</p> -<pre><code class="js">h3.navigateTo("/posts/", {orderBy: 'date', direction: 'desc'}); +<pre><code class="js">h3.navigateTo("/posts/", { orderBy: "date", direction: "desc" }); </code></pre> <a name="h3.on(event:-string,-handler:-function)"></a>@@ -8053,12 +8120,12 @@ <p>Special thanks to the following individuals, that made H3 possible:</p>
<ul> <li><strong>Leo Horie</strong>, author of the awesome <a href="https://mithril.js.org/">Mithril</a> framework that inspired me to write the H3 microframework in a moment of need.</li> -<li><strong>Andrey Sitnik</strong>, author of the beatiful <a href="https://evilmartians.com/chronicles/storeon-redux-in-173-bytes">Storeon</a> state management library, that is used (with minor modification) as the H3 store.</li> +<li><strong>Andrey Sitnik</strong>, author of the beatiful <a href="https://evilmartians.com/chronicles/storeon-redux-in-173-bytes">Storeon</a> state management library, that is used (with minor modifications) as the H3 store.</li> </ul> </div> <div id="footer"> - <p><span class="copy"></span> Fabio Cevasco – July 5, 2020</p> + <p><span class="copy"></span> Fabio Cevasco – August 1, 2020</p> <p><span>Powered by</span> <a href="https://h3rald.com/hastyscribe"><span class="hastyscribe"></span></a></p> </div> </div>
@@ -1,16 +1,16 @@
-import h3 from "./h3.js"; +import { h3, h } from "./h3.js"; import modules from "./modules.js"; import SettingsView from "./components/SettingsView.js"; import MainView from "./components/MainView.js"; h3.init({ - modules, - preStart: () => { - h3.dispatch("app/load"); - h3.dispatch("settings/set", h3.state.settings); - }, - routes: { - "/settings": SettingsView, - "/": MainView, - }, + modules, + preStart: () => { + h3.dispatch("app/load"); + h3.dispatch("settings/set", h3.state.settings); + }, + routes: { + "/settings": SettingsView, + "/": MainView, + }, });
@@ -1,44 +1,44 @@
-import h3 from "../h3.js"; +import { h3, h } from "../h3.js"; export default function AddTodoForm() { - const focus = () => document.getElementById("new-todo").focus(); - const addTodo = () => { - if (!newTodo.value) { - h3.dispatch("error/set"); - h3.redraw() - focus(); - return; - } - h3.dispatch("error/clear"); - h3.dispatch("todos/add", { - key: `todo_${Date.now()}__${btoa(newTodo.value)}`, - text: newTodo.value + const focus = () => document.getElementById("new-todo").focus(); + const addTodo = () => { + if (!newTodo.value) { + h3.dispatch("error/set"); + h3.redraw(); + focus(); + return; + } + h3.dispatch("error/clear"); + h3.dispatch("todos/add", { + key: `todo_${Date.now()}__${btoa(newTodo.value)}`, + text: newTodo.value, + }); + newTodo.value = ""; + h3.redraw(); + focus(); + }; + const addTodoOnEnter = (e) => { + if (e.keyCode == 13) { + addTodo(); + e.preventDefault(); + } + }; + const newTodo = h("input", { + id: "new-todo", + placeholder: "What do you want to do?", + oninput: (e) => (newTodo.value = e.target.value), + onkeydown: addTodoOnEnter, }); - newTodo.value = ""; - h3.redraw() - focus(); - }; - const addTodoOnEnter = (e) => { - if (e.keyCode == 13) { - addTodo(); - e.preventDefault(); - } - }; - const newTodo = h3("input", { - id: "new-todo", - placeholder: "What do you want to do?", - oninput: (e) => newTodo.value = e.target.value, - onkeydown: addTodoOnEnter, - }); - return h3("form.add-todo-form", [ - newTodo, - h3( - "span.submit-todo", - { - title: "Add Todo", - onclick: addTodo, - }, - "+" - ), - ]); + return h("form.add-todo-form", [ + newTodo, + h( + "span.submit-todo", + { + title: "Add Todo", + onclick: addTodo, + }, + "+" + ), + ]); }
@@ -1,19 +1,19 @@
-import h3 from "../h3.js"; +import { h3, h } from "../h3.js"; export default function EmptyTodoError(data, actions) { - const emptyTodoErrorClass = h3.state.displayEmptyTodoError ? "" : ".hidden"; - const clearError = () => { - h3.dispatch('error/clear'); - h3.redraw(); - } - return h3(`div#empty-todo-error.error${emptyTodoErrorClass}`, [ - h3("span.error-message", ["Please enter a non-empty todo item."]), - h3( - "span.dismiss-error", - { - onclick: clearError, - }, - "✘" - ), - ]); + const emptyTodoErrorClass = h3.state.displayEmptyTodoError ? "" : ".hidden"; + const clearError = () => { + h3.dispatch("error/clear"); + h3.redraw(); + }; + return h(`div#empty-todo-error.error${emptyTodoErrorClass}`, [ + h("span.error-message", ["Please enter a non-empty todo item."]), + h( + "span.dismiss-error", + { + onclick: clearError, + }, + "✘" + ), + ]); }
@@ -1,15 +1,15 @@
-import h3 from "../h3.js"; +import { h3, h } from "../h3.js"; import AddTodoForm from "./AddTodoForm.js"; import EmptyTodoError from "./EmptyTodoError.js"; import NavigationBar from "./NavigationBar.js"; import TodoList from "./TodoList.js"; export default function () { - const { todos, filter } = h3.state; - h3.dispatch("todos/filter", filter); - h3.dispatch("app/save", { todos: todos, settings: h3.state.settings }); - return h3("div.container", [ - h3("h1", "To Do List"), - h3("main", [AddTodoForm, EmptyTodoError, NavigationBar, TodoList]), - ]); + const { todos, filter } = h3.state; + h3.dispatch("todos/filter", filter); + h3.dispatch("app/save", { todos: todos, settings: h3.state.settings }); + return h("div.container", [ + h("h1", "To Do List"), + h("main", [AddTodoForm, EmptyTodoError, NavigationBar, TodoList]), + ]); }
@@ -1,39 +1,39 @@
-import h3 from "../h3.js"; +import { h3, h } from "../h3.js"; export default function Paginator() { - const hash = window.location.hash; - let { page, pagesize, filteredTodos } = h3.state; - let total = filteredTodos.length; - if (h3.route.params.page) { - page = parseInt(h3.route.params.page); - } - // Recalculate page in case data is filtered. - page = Math.min(Math.ceil(filteredTodos.length / pagesize), page) || 1; - h3.dispatch("pages/set", page); - const pages = Math.ceil(total / pagesize) || 1; - const previousClass = page > 1 ? ".link" : ".disabled"; - const nextClass = page < pages ? ".link" : ".disabled"; - const setPage = (value) => { - const page = h3.state.page; - const newPage = page + value; - h3.dispatch("pages/set", newPage); - h3.navigateTo("/", { page: newPage }); - } - return h3("div.paginator", [ - h3( - `span.previous-page${previousClass}`, - { - onclick: () => setPage(-1), - }, - ["←"] - ), - h3("span.current-page", [`${String(page)}/${String(pages)}`]), - h3( - `span.next-page${nextClass}`, - { - onclick: () => setPage(+1), - }, - ["→"] - ), - ]); + const hash = window.location.hash; + let { page, pagesize, filteredTodos } = h3.state; + let total = filteredTodos.length; + if (h3.route.params.page) { + page = parseInt(h3.route.params.page); + } + // Recalculate page in case data is filtered. + page = Math.min(Math.ceil(filteredTodos.length / pagesize), page) || 1; + h3.dispatch("pages/set", page); + const pages = Math.ceil(total / pagesize) || 1; + const previousClass = page > 1 ? ".link" : ".disabled"; + const nextClass = page < pages ? ".link" : ".disabled"; + const setPage = (value) => { + const page = h3.state.page; + const newPage = page + value; + h3.dispatch("pages/set", newPage); + h3.navigateTo("/", { page: newPage }); + }; + return h("div.paginator", [ + h( + `span.previous-page${previousClass}`, + { + onclick: () => setPage(-1), + }, + ["←"] + ), + h("span.current-page", [`${String(page)}/${String(pages)}`]), + h( + `span.next-page${nextClass}`, + { + onclick: () => setPage(+1), + }, + ["→"] + ), + ]); }
@@ -1,33 +1,33 @@
-import h3 from "../h3.js"; +import { h3, h } from "../h3.js"; export default function () { - const toggleLogging = (e) => { - const value = e.target.checked; - h3.dispatch("settings/set", { logging: value }); - h3.dispatch("app/save"); - }; - return h3("div.settings.container", [ - h3("h1", "Settings"), - h3("div.options", [ - h3("input", { - type: "checkbox", - onclick: toggleLogging, - checked: h3.state.settings.logging, - }), - h3( - "label#options-logging-label", - { - for: "logging", - }, - "Logging" - ), - ]), - h3( - "a.nav-link", - { - onclick: () => h3.navigateTo("/"), - }, - "← Go Back" - ), - ]); + const toggleLogging = (e) => { + const value = e.target.checked; + h3.dispatch("settings/set", { logging: value }); + h3.dispatch("app/save"); + }; + return h("div.settings.container", [ + h("h1", "Settings"), + h("div.options", [ + h("input", { + type: "checkbox", + onclick: toggleLogging, + checked: h3.state.settings.logging, + }), + h( + "label#options-logging-label", + { + for: "logging", + }, + "Logging" + ), + ]), + h( + "a.nav-link", + { + onclick: () => h3.navigateTo("/"), + }, + "← Go Back" + ), + ]); }
@@ -1,19 +1,35 @@
-import h3 from "../h3.js"; +import { h3, h } from "../h3.js"; export default function Todo(data) { - const todoStateClass = data.done ? ".done" : ".todo"; - const toggleTodo = (key) => { - h3.dispatch("todos/toggle", key); - h3.redraw(); - }; - const removeTodo = (key) => { - h3.dispatch("todos/remove", key); - h3.redraw(); - }; - return h3(`div.todo-item`, { data: { key: data.key } }, [ - h3(`div.todo-content${todoStateClass}`, [ - h3("span.todo-text", { onclick: (e) => toggleTodo(e.currentTarget.parentNode.parentNode.dataset.key) }, data.text), - ]), - h3("span.delete-todo", { onclick: (e) => removeTodo(e.currentTarget.parentNode.dataset.key) }, "✘"), - ]); + const todoStateClass = data.done ? ".done" : ".todo"; + const toggleTodo = (key) => { + h3.dispatch("todos/toggle", key); + h3.redraw(); + }; + const removeTodo = (key) => { + h3.dispatch("todos/remove", key); + h3.redraw(); + }; + return h(`div.todo-item`, { data: { key: data.key } }, [ + h(`div.todo-content${todoStateClass}`, [ + h( + "span.todo-text", + { + onclick: (e) => + toggleTodo( + e.currentTarget.parentNode.parentNode.dataset.key + ), + }, + data.text + ), + ]), + h( + "span.delete-todo", + { + onclick: (e) => + removeTodo(e.currentTarget.parentNode.dataset.key), + }, + "✘" + ), + ]); }
@@ -1,13 +1,10 @@
-import h3 from "../h3.js"; +import { h3, h } from "../h3.js"; import Todo from "./Todo.js"; export default function TodoList() { - const { page, pagesize } = h3.state; - const filteredTodos = h3.state.filteredTodos; - const start = (page - 1) * pagesize; - const end = Math.min(start + pagesize, filteredTodos.length); - return h3( - "div.todo-list", - filteredTodos.slice(start, end).map(Todo) - ); + const { page, pagesize } = h3.state; + const filteredTodos = h3.state.filteredTodos; + const start = (page - 1) * pagesize; + const end = Math.min(start + pagesize, filteredTodos.length); + return h("div.todo-list", filteredTodos.slice(start, end).map(Todo)); }
@@ -1,7 +1,7 @@
/** - * H3 v0.9.0 "Impeccable Iconian" + * H3 v0.10.0 "Jittery Jem'Hadar" * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com> - * + * * @license MIT * For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE */@@ -41,8 +41,6 @@ return false;
} } if ([String, Number, Boolean].includes(obj1.constructor)) { - if (obj1 !== obj2) { - } return obj1 === obj2; } if (obj1.constructor === Array) {@@ -60,14 +58,14 @@ return checkProperties(obj1, obj2); // && checkProperties(obj2, obj1);
}; const selectorRegex = /^([a-z][a-z0-9:_=-]*)?(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; - +const [PATCH, INSERT, DELETE] = [-1, -2, -3]; let $onrenderCallbacks = []; // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) { this.type = undefined; - this.attributes = {}; + this.props = {}; this.data = {}; this.id = undefined; this.$html = undefined;@@ -133,25 +131,34 @@ // Not a VNode, assume props object
this.processProperties(data); } } - } else if (args.length === 3) { + } else { let [selector, props, children] = args; + if (args.length > 3) { + children = args.slice(2); + } + children = Array.isArray(children) ? children : [children]; if (typeof selector !== "string") { throw new Error( "[VNode] Invalid first argument passed to VNode constructor." ); } this.processSelector(selector); - if (typeof props !== "object" || props === null) { - throw new Error( - "[VNode] Invalid second argument passed to VNode constructor." - ); + if ( + props instanceof Function || + props instanceof VNode || + typeof props === "string" + ) { + // 2nd argument is a child + children = [props].concat(children); + } else { + if (typeof props !== "object" || props === null) { + throw new Error( + "[VNode] Invalid second argument passed to VNode constructor." + ); + } + this.processProperties(props); } - this.processProperties(props); this.processChildren(children); - } else { - throw new Error( - "[VNode] Too many arguments passed to VNode constructor." - ); } }@@ -166,7 +173,7 @@ this.data = data.data;
this.value = data.value; this.eventListeners = data.eventListeners; this.children = data.children; - this.attributes = data.attributes; + this.props = data.props; this.classList = data.classList; }@@ -185,7 +192,7 @@ this.classList =
attrs.classList && attrs.classList.length > 0 ? attrs.classList : this.classList; - this.attributes = attrs; + this.props = attrs; Object.keys(attrs) .filter((a) => a.startsWith("on") && attrs[a]) .forEach((key) => {@@ -195,19 +202,19 @@ `[VNode] Event handler specified for ${key} event is not a function.`
); } this.eventListeners[key.slice(2)] = attrs[key]; - delete this.attributes[key]; + delete this.props[key]; }); - delete this.attributes.value; - delete this.attributes.$html; - delete this.attributes.$onrender; - delete this.attributes.id; - delete this.attributes.data; - delete this.attributes.style; - delete this.attributes.classList; + delete this.props.value; + delete this.props.$html; + delete this.props.$onrender; + delete this.props.id; + delete this.props.data; + delete this.props.style; + delete this.props.classList; } processSelector(selector) { - if (!selector.match(selectorRegex)) { + if (!selector.match(selectorRegex) || selector.length === 0) { throw new Error(`[VNode] Invalid selector: ${selector}`); } const [, type, id, classes] = selector.match(selectorRegex);@@ -263,17 +270,16 @@ const node = document.createElement(this.type);
if (this.id) { node.id = this.id; } - Object.keys(this.attributes).forEach((attr) => { - // Set attributes (only if non-empty strings) - if (this.attributes[attr] && typeof this.attributes[attr] === "string") { - const a = document.createAttribute(attr); - a.value = this.attributes[attr]; - node.setAttributeNode(a); + Object.keys(this.props).forEach((p) => { + // Set attributes + if (typeof this.props[p] === "boolean") { + this.props[p] ? node.setAttribute(p, "") : node.removeAttribute(p); + } + if (["string", "number"].includes(typeof this.props[p])) { + node.setAttribute(p, this.props[p]); } // Set properties - if (typeof this.attributes[attr] !== "string" || !node[attr]) { - node[attr] = this.attributes[attr]; - } + node[p] = this.props[p]; }); // Event Listeners Object.keys(this.eventListeners).forEach((event) => {@@ -281,7 +287,11 @@ node.addEventListener(event, this.eventListeners[event]);
}); // Value if (this.value) { - node.value = this.value; + if (["textarea", "input"].includes(this.type)) { + node.value = this.value; + } else { + node.setAttribute("value", this.value); + } } // Style if (this.style) {@@ -332,8 +342,12 @@ oldvnode.id = newvnode.id;
} // Value if (oldvnode.value !== newvnode.value) { - node.value = newvnode.value || ""; oldvnode.value = newvnode.value; + if (["textarea", "input"].includes(oldvnode.type)) { + node.value = newvnode.value || ""; + } else { + node.setAttribute("value", newvnode.value || ""); + } } // Classes if (!equal(oldvnode.classList, newvnode.classList)) {@@ -370,27 +384,34 @@ }
}); oldvnode.data = newvnode.data; } - // Attributes - if (!equal(oldvnode.attributes, newvnode.attributes)) { - Object.keys(oldvnode.attributes).forEach((a) => { - if (newvnode.attributes[a] === false) { - node[a] = false; - } - if (!newvnode.attributes[a]) { + // Properties & Attributes + if (!equal(oldvnode.props, newvnode.props)) { + Object.keys(oldvnode.props).forEach((a) => { + node[a] = newvnode.props[a]; + if (typeof newvnode.props[a] === "boolean") { + oldvnode.props[a] = newvnode.props[a]; + newvnode.props[a] + ? node.setAttribute(a, "") + : node.removeAttribute(a); + } else if (!newvnode.props[a]) { + delete oldvnode.props[a]; node.removeAttribute(a); } else if ( - newvnode.attributes[a] && - newvnode.attributes[a] !== oldvnode.attributes[a] + newvnode.props[a] && + newvnode.props[a] !== oldvnode.props[a] ) { - node.setAttribute(a, newvnode.attributes[a]); + oldvnode.props[a] = newvnode.props[a]; + if (["string", "number"].includes(typeof newvnode.props[a])) { + node.setAttribute(a, newvnode.props[a]); + } } }); - Object.keys(newvnode.attributes).forEach((a) => { - if (!oldvnode.attributes[a] && newvnode.attributes[a]) { - node.setAttribute(a, newvnode.attributes[a]); + Object.keys(newvnode.props).forEach((a) => { + if (!oldvnode.props[a] && newvnode.props[a]) { + oldvnode.props[a] = newvnode.props[a]; + node.setAttribute(a, newvnode.props[a]); } }); - oldvnode.attributes = newvnode.attributes; } // Event listeners if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) {@@ -412,113 +433,48 @@ });
oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - let map = []; - let oldNodesFound = 0; - let newNodesFound = 0; - // First look for existing nodes - for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - let found = -1; - for (let index = 0; index < newvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - newNodesFound++; - oldNodesFound++; - break; - } - } - map.push(found); - } - if ( - newNodesFound === oldNodesFound && - newvnode.children.length === oldvnode.children.length - ) { - // something changed but everything else is the same - return map; - } - if (newNodesFound === newvnode.children.length) { - // All children in newvnode exist in oldvnode - // All nodes that are not found must be removed - for (let i = 0; i < map.length; i++) { - if (map[i] === -1) { - map[i] = -3; - } - } - } - if (oldNodesFound === oldvnode.children.length) { - // All children in oldvnode exist in newvnode - // Check where the missing newvnodes children need to be added - for ( - let newIndex = 0; - newIndex < newvnode.children.length; - newIndex++ - ) { - if (!map.includes(newIndex)) { - map.splice(newIndex, 0, -2); - } - } - } - // Check if nodes needs to be removed (if there are fewer children) - if (newvnode.children.length < oldvnode.children.length) { - for (let i = 0; i < map.length; i++) { - if (map[i] === -1 && !newvnode.children[i]) { - map[i] = -3; - } - } - } - return map; - } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; + let resultMap = [...Array(newvnode.children.length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; - for (let i of childMap) { + checkmap: for (const i of childMap) { count++; - let breakFor = false; if (i === count) { // Matching nodes; continue; } switch (i) { - case -1: - // different node, check + case PATCH: { oldvnode.children[count].redraw({ node: node.childNodes[count], vnode: newvnode.children[count], }); - break; - case -2: - // add node + break checkmap; + } + case INSERT: { oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); - breakFor = true; - break; - case -3: - // remove node + break checkmap; + } + case DELETE: { oldvnode.children.splice(count, 1); node.removeChild(node.childNodes[count]); - breakFor = true; - break; - default: - // Node found, move nodes and remap + break checkmap; + } + default: { const vtarget = oldvnode.children.splice(i, 1)[0]; oldvnode.children.splice(count, 0, vtarget); - node.insertBefore(node.childNodes[i], node.childNodes[count]); - breakFor = true; - break; - } - if (breakFor) { - break; + const target = node.removeChild(node.childNodes[i]); + node.insertBefore(target, node.childNodes[count]); + break checkmap; + } } } childMap = mapChildren(oldvnode, newvnode); - resultMap = [...Array(childMap.length).keys()]; + resultMap = [...Array(newvnode.children.length).keys()]; } // $onrender if (!equal(oldvnode.$onrender, newvnode.$onrender)) {@@ -533,6 +489,40 @@ }
} } +const mapChildren = (oldvnode, newvnode) => { + const newList = newvnode.children; + const oldList = oldvnode.children; + let map = []; + for (let nIdx = 0; nIdx < newList.length; nIdx++) { + let op = PATCH; + for (let oIdx = 0; oIdx < oldList.length; oIdx++) { + if (equal(newList[nIdx], oldList[oIdx]) && !map.includes(oIdx)) { + op = oIdx; // Same node found + break; + } + } + if ( + op < 0 && + newList.length >= oldList.length && + map.length >= oldList.length + ) { + op = INSERT; + } + map.push(op); + } + const oldNodesFound = map.filter((c) => c >= 0); + if (oldList.length > newList.length) { + // Remove remaining nodes + [...Array(oldList.length - newList.length).keys()].forEach(() => + map.push(DELETE) + ); + } else if (oldNodesFound.length === oldList.length) { + // All nodes not found are insertions + map = map.map((c) => (c < 0 ? INSERT : c)); + } + return map; +}; + /** * The code of the following class is heavily based on Storeon * Modified according to the terms of the MIT License@@ -643,15 +633,17 @@ if (!this.route) {
throw new Error(`[Router] No route matches '${fragment}'`); } // Old route component teardown + let state = {}; if (oldRoute) { const oldRouteComponent = this.routes[oldRoute.def]; - oldRouteComponent.state = - oldRouteComponent.teardown && - (await oldRouteComponent.teardown(oldRouteComponent.state)); + state = + (oldRouteComponent.teardown && + (await oldRouteComponent.teardown(oldRouteComponent.state))) || + state; } // New route component setup const newRouteComponent = this.routes[this.route.def]; - newRouteComponent.state = {}; + newRouteComponent.state = state; newRouteComponent.setup && (await newRouteComponent.setup(newRouteComponent.state)); // Redrawing...@@ -685,9 +677,12 @@ }
} // High Level API -const h3 = (...args) => { + +export const h = (...args) => { return new VNode(...args); }; + +export const h3 = {}; let store = null; let router = null;@@ -782,6 +777,26 @@ }
redrawing = true; router.redraw(); redrawing = setRedrawing || false; +}; + +h3.screen = ({ setup, display, teardown }) => { + if (!display || typeof display !== "function") { + throw new Error("[h3.screen] No display property specified."); + } + if (setup && typeof setup !== "function") { + throw new Error("[h3.screen] setup property is not a function."); + } + if (teardown && typeof teardown !== "function") { + throw new Error("[h3.screen] teardown property is not a function."); + } + const fn = display; + if (setup) { + fn.setup = setup; + } + if (teardown) { + fn.teardown = teardown; + } + return fn; }; export default h3;
@@ -1,70 +1,72 @@
-import h3 from "./h3.js"; +import { h3, h } from "./h3.js"; const app = () => { - h3.on("app/load", () => { - const storedData = localStorage.getItem("h3_todo_list"); - const { todos, settings } = storedData - ? JSON.parse(storedData) - : { todos: [], settings: {} }; - return { todos, settings }; - }); - h3.on("app/save", (state, data) => { - localStorage.setItem( - "h3_todo_list", - JSON.stringify({ todos: state.todos, settings: state.settings }) - ); - }); + h3.on("app/load", () => { + const storedData = localStorage.getItem("h3_todo_list"); + const { todos, settings } = storedData + ? JSON.parse(storedData) + : { todos: [], settings: {} }; + return { todos, settings }; + }); + h3.on("app/save", (state, data) => { + localStorage.setItem( + "h3_todo_list", + JSON.stringify({ todos: state.todos, settings: state.settings }) + ); + }); }; const settings = () => { - let removeSubscription; - h3.on("$init", () => ({ settings: {} })); - h3.on("settings/set", (state, data) => { - if (data.logging) { - removeSubscription = h3.on("$log", (state, data) => console.log(data)); - } else { - removeSubscription && removeSubscription(); - } - return { settings: data }; - }); + let removeSubscription; + h3.on("$init", () => ({ settings: {} })); + h3.on("settings/set", (state, data) => { + if (data.logging) { + removeSubscription = h3.on("$log", (state, data) => + console.log(data) + ); + } else { + removeSubscription && removeSubscription(); + } + return { settings: data }; + }); }; const todos = () => { - h3.on("$init", () => ({ todos: [], filteredTodos: [], filter: "" })); - h3.on("todos/add", (state, data) => { - let todos = state.todos; - todos.unshift({ - key: data.key, - text: data.text, + h3.on("$init", () => ({ todos: [], filteredTodos: [], filter: "" })); + h3.on("todos/add", (state, data) => { + let todos = state.todos; + todos.unshift({ + key: data.key, + text: data.text, + }); + return { todos }; + }); + h3.on("todos/remove", (state, k) => { + const todos = state.todos.filter(({ key }) => key !== k); + return { todos }; + }); + h3.on("todos/toggle", (state, k) => { + const todos = state.todos; + const todo = state.todos.find(({ key }) => key === k); + todo.done = !todo.done; + return { todos }; + }); + h3.on("todos/filter", (state, filter) => { + const todos = state.todos; + const filteredTodos = todos.filter(({ text }) => text.match(filter)); + return { filteredTodos, filter }; }); - return { todos }; - }); - h3.on("todos/remove", (state, k) => { - const todos = state.todos.filter(({ key }) => key !== k); - return { todos }; - }); - h3.on("todos/toggle", (state, k) => { - const todos = state.todos; - const todo = state.todos.find(({ key }) => key === k); - todo.done = !todo.done; - return { todos }; - }); - h3.on("todos/filter", (state, filter) => { - const todos = state.todos; - const filteredTodos = todos.filter(({ text }) => text.match(filter)); - return { filteredTodos, filter }; - }); }; const error = () => { - h3.on("$init", () => ({ displayEmptyTodoError: false })); - h3.on("error/clear", (state) => ({ displayEmptyTodoError: false })); - h3.on("error/set", (state) => ({ displayEmptyTodoError: true })); + h3.on("$init", () => ({ displayEmptyTodoError: false })); + h3.on("error/clear", (state) => ({ displayEmptyTodoError: false })); + h3.on("error/set", (state) => ({ displayEmptyTodoError: true })); }; const pages = () => { - h3.on("$init", () => ({ pagesize: 10, page: 1 })); - h3.on("pages/set", (state, page) => ({ page })); + h3.on("$init", () => ({ pagesize: 10, page: 1 })); + h3.on("pages/set", (state, page) => ({ page })); }; export default [app, todos, error, pages, settings];
@@ -1,4 +1,4 @@
-import h3 from "./h3.js"; +import { h3, h } from "./h3.js"; import marked from "./vendor/marked.js"; import Prism from "./vendor/prism.js";@@ -6,73 +6,79 @@ const labels = {
overview: "Overview", "quick-start": "Quick Start", "key-concepts": "Key Concepts", + "best-practices": "Best Practices", tutorial: "Tutorial", api: "API", about: "About", }; -const pages = {}; +const Page = h3.screen({ + setup: async (state) => { + state.pages = state.pages || {}; + state.id = h3.route.path.slice(1); + state.ids = Object.keys(labels); + state.md = state.ids.includes(state.id) + ? `md/${state.id}.md` + : `md/overview.md`; + await fetchPage(state); + }, + display: (state) => { + return h("div.page", [ + Header, + h("div.row", [ + h("input#drawer-control.drawer", { type: "checkbox" }), + Navigation(state.id, state.ids), + Content(state.pages[state.id]), + Footer, + ]), + ]); + }, + teardown: (state) => state, +}); -const fetchPage = async (pages, id, md) => { +const fetchPage = async ({ pages, id, md }) => { if (!pages[id]) { const response = await fetch(md); const text = await response.text(); pages[id] = marked(text); - h3.redraw(); } }; -const Page = () => { - const id = h3.route.path.slice(1); - const ids = Object.keys(labels); - const md = ids.includes(id) ? `md/${id}.md` : `md/overview.md`; - fetchPage(pages, id, md); - return h3("div.page", [ - Header, - h3("div.row", [ - h3("input#drawer-control.drawer", { type: "checkbox" }), - Navigation(id, ids), - Content(pages[id]), - Footer, - ]), - ]); -}; - const Header = () => { - return h3("header.row.sticky", [ - h3("a.logo.col-sm-1", { href: "#/" }, [ - h3("img", { alt: "H3", src: "images/h3.svg" }), + return h("header.row.sticky", [ + h("a.logo.col-sm-1", { href: "#/" }, [ + h("img", { alt: "H3", src: "images/h3.svg" }), ]), - h3("div.version.col-sm.col-md", [ - h3("div.version-number", "v0.9.0"), - h3("div.version-label", "“Impeccable Iconian“"), + h("div.version.col-sm.col-md", [ + h("div.version-number", "v0.10.0"), + h("div.version-label", "“Jittery Jem'Hadar“"), ]), - h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), + h("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), ]); }; const Footer = () => { - return h3("footer", [h3("div", "© 2020 Fabio Cevasco")]); + return h("footer", [h("div", "© 2020 Fabio Cevasco")]); }; const Navigation = (id, ids) => { const menu = ids.map((p) => - h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) + h(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) ); - return h3("nav#navigation.col-md-3", [ - h3("label.drawer-close", { for: "drawer-control" }), + return h("nav#navigation.col-md-3", [ + h("label.drawer-close", { for: "drawer-control" }), ...menu, ]); }; const Content = (html) => { - const content = html - ? h3("div.content", { $html: html }) - : h3("div.spinner-container", h3("span.spinner")); - return h3("main.col-sm-12.col-md-9", [ - h3("div.card.fluid", h3("div.section", content)), + const content = h("div.content", { $html: html }); + return h("main.col-sm-12.col-md-9", [ + h( + "div.card.fluid", + h("div.section", { $onrender: () => Prism.highlightAll() }, content) + ), ]); }; h3.init(Page); -h3.on("$redraw", () => Prism.highlightAll());
@@ -1,7 +1,7 @@
/** - * H3 v0.9.0 "Impeccable Iconian" + * H3 v0.10.0 "Jittery Jem'Hadar" * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com> - * + * * @license MIT * For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE */@@ -41,8 +41,6 @@ return false;
} } if ([String, Number, Boolean].includes(obj1.constructor)) { - if (obj1 !== obj2) { - } return obj1 === obj2; } if (obj1.constructor === Array) {@@ -60,14 +58,14 @@ return checkProperties(obj1, obj2); // && checkProperties(obj2, obj1);
}; const selectorRegex = /^([a-z][a-z0-9:_=-]*)?(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; - +const [PATCH, INSERT, DELETE] = [-1, -2, -3]; let $onrenderCallbacks = []; // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) { this.type = undefined; - this.attributes = {}; + this.props = {}; this.data = {}; this.id = undefined; this.$html = undefined;@@ -133,25 +131,34 @@ // Not a VNode, assume props object
this.processProperties(data); } } - } else if (args.length === 3) { + } else { let [selector, props, children] = args; + if (args.length > 3) { + children = args.slice(2); + } + children = Array.isArray(children) ? children : [children]; if (typeof selector !== "string") { throw new Error( "[VNode] Invalid first argument passed to VNode constructor." ); } this.processSelector(selector); - if (typeof props !== "object" || props === null) { - throw new Error( - "[VNode] Invalid second argument passed to VNode constructor." - ); + if ( + props instanceof Function || + props instanceof VNode || + typeof props === "string" + ) { + // 2nd argument is a child + children = [props].concat(children); + } else { + if (typeof props !== "object" || props === null) { + throw new Error( + "[VNode] Invalid second argument passed to VNode constructor." + ); + } + this.processProperties(props); } - this.processProperties(props); this.processChildren(children); - } else { - throw new Error( - "[VNode] Too many arguments passed to VNode constructor." - ); } }@@ -166,7 +173,7 @@ this.data = data.data;
this.value = data.value; this.eventListeners = data.eventListeners; this.children = data.children; - this.attributes = data.attributes; + this.props = data.props; this.classList = data.classList; }@@ -185,7 +192,7 @@ this.classList =
attrs.classList && attrs.classList.length > 0 ? attrs.classList : this.classList; - this.attributes = attrs; + this.props = attrs; Object.keys(attrs) .filter((a) => a.startsWith("on") && attrs[a]) .forEach((key) => {@@ -195,19 +202,19 @@ `[VNode] Event handler specified for ${key} event is not a function.`
); } this.eventListeners[key.slice(2)] = attrs[key]; - delete this.attributes[key]; + delete this.props[key]; }); - delete this.attributes.value; - delete this.attributes.$html; - delete this.attributes.$onrender; - delete this.attributes.id; - delete this.attributes.data; - delete this.attributes.style; - delete this.attributes.classList; + delete this.props.value; + delete this.props.$html; + delete this.props.$onrender; + delete this.props.id; + delete this.props.data; + delete this.props.style; + delete this.props.classList; } processSelector(selector) { - if (!selector.match(selectorRegex)) { + if (!selector.match(selectorRegex) || selector.length === 0) { throw new Error(`[VNode] Invalid selector: ${selector}`); } const [, type, id, classes] = selector.match(selectorRegex);@@ -263,17 +270,16 @@ const node = document.createElement(this.type);
if (this.id) { node.id = this.id; } - Object.keys(this.attributes).forEach((attr) => { - // Set attributes (only if non-empty strings) - if (this.attributes[attr] && typeof this.attributes[attr] === "string") { - const a = document.createAttribute(attr); - a.value = this.attributes[attr]; - node.setAttributeNode(a); + Object.keys(this.props).forEach((p) => { + // Set attributes + if (typeof this.props[p] === "boolean") { + this.props[p] ? node.setAttribute(p, "") : node.removeAttribute(p); + } + if (["string", "number"].includes(typeof this.props[p])) { + node.setAttribute(p, this.props[p]); } // Set properties - if (typeof this.attributes[attr] !== "string" || !node[attr]) { - node[attr] = this.attributes[attr]; - } + node[p] = this.props[p]; }); // Event Listeners Object.keys(this.eventListeners).forEach((event) => {@@ -281,7 +287,11 @@ node.addEventListener(event, this.eventListeners[event]);
}); // Value if (this.value) { - node.value = this.value; + if (["textarea", "input"].includes(this.type)) { + node.value = this.value; + } else { + node.setAttribute("value", this.value); + } } // Style if (this.style) {@@ -332,8 +342,12 @@ oldvnode.id = newvnode.id;
} // Value if (oldvnode.value !== newvnode.value) { - node.value = newvnode.value || ""; oldvnode.value = newvnode.value; + if (["textarea", "input"].includes(oldvnode.type)) { + node.value = newvnode.value || ""; + } else { + node.setAttribute("value", newvnode.value || ""); + } } // Classes if (!equal(oldvnode.classList, newvnode.classList)) {@@ -370,27 +384,34 @@ }
}); oldvnode.data = newvnode.data; } - // Attributes - if (!equal(oldvnode.attributes, newvnode.attributes)) { - Object.keys(oldvnode.attributes).forEach((a) => { - if (newvnode.attributes[a] === false) { - node[a] = false; - } - if (!newvnode.attributes[a]) { + // Properties & Attributes + if (!equal(oldvnode.props, newvnode.props)) { + Object.keys(oldvnode.props).forEach((a) => { + node[a] = newvnode.props[a]; + if (typeof newvnode.props[a] === "boolean") { + oldvnode.props[a] = newvnode.props[a]; + newvnode.props[a] + ? node.setAttribute(a, "") + : node.removeAttribute(a); + } else if (!newvnode.props[a]) { + delete oldvnode.props[a]; node.removeAttribute(a); } else if ( - newvnode.attributes[a] && - newvnode.attributes[a] !== oldvnode.attributes[a] + newvnode.props[a] && + newvnode.props[a] !== oldvnode.props[a] ) { - node.setAttribute(a, newvnode.attributes[a]); + oldvnode.props[a] = newvnode.props[a]; + if (["string", "number"].includes(typeof newvnode.props[a])) { + node.setAttribute(a, newvnode.props[a]); + } } }); - Object.keys(newvnode.attributes).forEach((a) => { - if (!oldvnode.attributes[a] && newvnode.attributes[a]) { - node.setAttribute(a, newvnode.attributes[a]); + Object.keys(newvnode.props).forEach((a) => { + if (!oldvnode.props[a] && newvnode.props[a]) { + oldvnode.props[a] = newvnode.props[a]; + node.setAttribute(a, newvnode.props[a]); } }); - oldvnode.attributes = newvnode.attributes; } // Event listeners if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) {@@ -412,113 +433,48 @@ });
oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - let map = []; - let oldNodesFound = 0; - let newNodesFound = 0; - // First look for existing nodes - for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - let found = -1; - for (let index = 0; index < newvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - newNodesFound++; - oldNodesFound++; - break; - } - } - map.push(found); - } - if ( - newNodesFound === oldNodesFound && - newvnode.children.length === oldvnode.children.length - ) { - // something changed but everything else is the same - return map; - } - if (newNodesFound === newvnode.children.length) { - // All children in newvnode exist in oldvnode - // All nodes that are not found must be removed - for (let i = 0; i < map.length; i++) { - if (map[i] === -1) { - map[i] = -3; - } - } - } - if (oldNodesFound === oldvnode.children.length) { - // All children in oldvnode exist in newvnode - // Check where the missing newvnodes children need to be added - for ( - let newIndex = 0; - newIndex < newvnode.children.length; - newIndex++ - ) { - if (!map.includes(newIndex)) { - map.splice(newIndex, 0, -2); - } - } - } - // Check if nodes needs to be removed (if there are fewer children) - if (newvnode.children.length < oldvnode.children.length) { - for (let i = 0; i < map.length; i++) { - if (map[i] === -1 && !newvnode.children[i]) { - map[i] = -3; - } - } - } - return map; - } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; + let resultMap = [...Array(newvnode.children.length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; - for (let i of childMap) { + checkmap: for (const i of childMap) { count++; - let breakFor = false; if (i === count) { // Matching nodes; continue; } switch (i) { - case -1: - // different node, check + case PATCH: { oldvnode.children[count].redraw({ node: node.childNodes[count], vnode: newvnode.children[count], }); - break; - case -2: - // add node + break checkmap; + } + case INSERT: { oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); - breakFor = true; - break; - case -3: - // remove node + break checkmap; + } + case DELETE: { oldvnode.children.splice(count, 1); node.removeChild(node.childNodes[count]); - breakFor = true; - break; - default: - // Node found, move nodes and remap + break checkmap; + } + default: { const vtarget = oldvnode.children.splice(i, 1)[0]; oldvnode.children.splice(count, 0, vtarget); - node.insertBefore(node.childNodes[i], node.childNodes[count]); - breakFor = true; - break; - } - if (breakFor) { - break; + const target = node.removeChild(node.childNodes[i]); + node.insertBefore(target, node.childNodes[count]); + break checkmap; + } } } childMap = mapChildren(oldvnode, newvnode); - resultMap = [...Array(childMap.length).keys()]; + resultMap = [...Array(newvnode.children.length).keys()]; } // $onrender if (!equal(oldvnode.$onrender, newvnode.$onrender)) {@@ -533,6 +489,40 @@ }
} } +const mapChildren = (oldvnode, newvnode) => { + const newList = newvnode.children; + const oldList = oldvnode.children; + let map = []; + for (let nIdx = 0; nIdx < newList.length; nIdx++) { + let op = PATCH; + for (let oIdx = 0; oIdx < oldList.length; oIdx++) { + if (equal(newList[nIdx], oldList[oIdx]) && !map.includes(oIdx)) { + op = oIdx; // Same node found + break; + } + } + if ( + op < 0 && + newList.length >= oldList.length && + map.length >= oldList.length + ) { + op = INSERT; + } + map.push(op); + } + const oldNodesFound = map.filter((c) => c >= 0); + if (oldList.length > newList.length) { + // Remove remaining nodes + [...Array(oldList.length - newList.length).keys()].forEach(() => + map.push(DELETE) + ); + } else if (oldNodesFound.length === oldList.length) { + // All nodes not found are insertions + map = map.map((c) => (c < 0 ? INSERT : c)); + } + return map; +}; + /** * The code of the following class is heavily based on Storeon * Modified according to the terms of the MIT License@@ -643,15 +633,17 @@ if (!this.route) {
throw new Error(`[Router] No route matches '${fragment}'`); } // Old route component teardown + let state = {}; if (oldRoute) { const oldRouteComponent = this.routes[oldRoute.def]; - oldRouteComponent.state = - oldRouteComponent.teardown && - (await oldRouteComponent.teardown(oldRouteComponent.state)); + state = + (oldRouteComponent.teardown && + (await oldRouteComponent.teardown(oldRouteComponent.state))) || + state; } // New route component setup const newRouteComponent = this.routes[this.route.def]; - newRouteComponent.state = {}; + newRouteComponent.state = state; newRouteComponent.setup && (await newRouteComponent.setup(newRouteComponent.state)); // Redrawing...@@ -685,9 +677,12 @@ }
} // High Level API -const h3 = (...args) => { + +export const h = (...args) => { return new VNode(...args); }; + +export const h3 = {}; let store = null; let router = null;@@ -782,6 +777,26 @@ }
redrawing = true; router.redraw(); redrawing = setRedrawing || false; +}; + +h3.screen = ({ setup, display, teardown }) => { + if (!display || typeof display !== "function") { + throw new Error("[h3.screen] No display property specified."); + } + if (setup && typeof setup !== "function") { + throw new Error("[h3.screen] setup property is not a function."); + } + if (teardown && typeof teardown !== "function") { + throw new Error("[h3.screen] teardown property is not a function."); + } + const fn = display; + if (setup) { + fn.setup = setup; + } + if (teardown) { + fn.teardown = teardown; + } + return fn; }; export default h3;
@@ -39,4 +39,4 @@
Special thanks to the following individuals, that made H3 possible: * **Leo Horie**, author of the awesome [Mithril](https://mithril.js.org/) framework that inspired me to write the H3 microframework in a moment of need. -* **Andrey Sitnik**, author of the beatiful [Storeon](https://evilmartians.com/chronicles/storeon-redux-in-173-bytes) state management library, that is used (with minor modification) as the H3 store.+* **Andrey Sitnik**, author of the beatiful [Storeon](https://evilmartians.com/chronicles/storeon-redux-in-173-bytes) state management library, that is used (with minor modifications) as the H3 store.
@@ -2,21 +2,24 @@ ## API
The core of the H3 API is comprised of the following six methods and two properties. +### h(selector: string, attributes: object, children: array) -### h3(selector: string, attributes: object, children: array) - -The `h3` object is also used as a constructor for Virtual DOM Nodes (VNodes). It can actually take from one to three arguments used to configure the resulting node. +The `h` function is a constructor for Virtual DOM Nodes (VNodes). It can actually take from one to any number of arguments used to configure the resulting node. -The best way to understand how it works is by providing a few different examples. Please note that in each example the corresponding *HTML* markup is provided, although such markup will actually be generated when the Virtual Node is rendered/redrawn. +The best way to understand how it works is by providing a few different examples. Please note that in each example the corresponding _HTML_ markup is provided, although such markup will actually be generated when the Virtual Node is rendered/redrawn. #### Create an element, with an ID, classes, attributes and children This is a complete example showing how to create a link with an `href` attribute, an ID, two classes, and three child nodes. ```js -h3("a#test-link.btn.primary", { - href: "#/test" -}, ["This is a ", h3("em", "test"), "link."]); +h( + "a#test-link.btn.primary", + { + href: "#/test", + }, + ["This is a ", h("em", "test"), "link."] +); ``` ↓@@ -30,7 +33,7 @@
#### Create an empty element ```js -h3("div"); +h("div"); ``` ↓@@ -42,7 +45,7 @@
#### Create an element with a textual child node ```js -h3("span", "This is a test"); +h("span", "This is a test"); ``` ↓@@ -54,13 +57,24 @@
#### Create an element with child nodes ```js -h3("ol", [ - h3("li", "Do this first."), - h3("li", "Then this."), - h3("li", "And finally this.") +h("ol", [ + h("li", "Do this first."), + h("li", "Then this."), + h("li", "And finally this."), ]); ``` +_or_ + +```js +h( + "ol", + h("li", "Do this first."), + h("li", "Then this."), + h("li", "And finally this.") +); +``` + ↓ ```html@@ -75,11 +89,15 @@ #### Render a component
```js const TestComponent = () => { - return h3("button.test", { - onclick: () => alert("Hello!") - }, "Show Alert"); + return h( + "button.test", + { + onclick: () => alert("Hello!"), + }, + "Show Alert" + ); }; -h3(TestComponent); +h(TestComponent); ``` ↓@@ -93,8 +111,8 @@
#### Render child components ```js -const TestLi = (text) => h3("li.test", text); -h3("ul", ["A", "B", "C"].map(TestLi)); +const TestLi = (text) => h("li.test", text); +h("ul", ["A", "B", "C"].map(TestLi)); ``` ↓@@ -109,26 +127,65 @@ ```
#### Special attributes -* Any attribute starting with *on* (e.g. onclick, onkeydown, etc.) will be treated as an event listener. -* The `classList` attribute can be set to a list of classes to apply to the element (as an alternative to using the element selector shorthand). -* The `data` attribute can be set to a simple object containing [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). -* The special `$html` attribute can be used to set the `innerHTML` property of the resulting HTML element. Use only if you know what you are doing! -* The special `$onrender` attribute can be set to a function that will executed every time the VNode is rendered and added to the DOM. +- Any attribute starting with _on_ (e.g. onclick, onkeydown, etc.) will be treated as an event listener. +- The `classList` attribute can be set to a list of classes to apply to the element (as an alternative to using the element selector shorthand). +- The `data` attribute can be set to a simple object containing [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). +- The special `$html` attribute can be used to set the `innerHTML` property of the resulting HTML element. Use only if you know what you are doing! +- The special `$onrender` attribute can be set to a function that will executed every time the VNode is rendered and added to the DOM. -The `$html` and the `$onrender` special attributes should be used sparingly, and typically only when interfacing with third-party libraries that need access to the real DOM. +The `$html` and the `$onrender` special attributes should be used sparingly, and typically only when interfacing with third-party libraries that need access to the real DOM. For example, consider the following code snippet that can be used to initialize the [InscrybMDE](https://github.com/Inscryb/inscryb-markdown-editor) Markdown editor on a textarea element: ```js -h3("textarea", { +h("textarea", { $onrender: (element) => { const editor = new window.InscrybMDE({ - element + element, }); - } + }, }); ``` +### h3.screen({setup, display, teardown}) + +Creates a Screen by providing a (mandatory) **display** function used to render content, an an optional **setup** and **teardown** functions, executed before and after the display function respectively. + +Each of these functions takes a single **screen** parameter, which is initialized as an empty object before the setup, and is (optionally) returned by the teardown function should state be preserved across different screens. + +Consider the following example: + +```js +h3.screen({ + setup: await (state) => { + state.data = state.data || {}; + state.id = h3.route.parts.id || 1; + const url = `http://dummy.restapiexample.com/api/v1/employee/${id}`; + state.data[id] = state.data[id] || await (await fetch(url)).json(); + }, + display(state) => { + const employee = state.data[state.id]; + if (!employee) { + return h("div.error", "Invalid Employee ID."); + } + return h("div.employee", + h("h2", "Employee Profile"), + h("dl", [ + h("dt", "Name:"), + h("dd", data.employee_name), + h("dt", "Salary:"), + h("dd", `${data.employee_salary} €`), + h("dt", "Age:"), + h("dd", data.employee_age), + ]) + ) + }, + teardown: (state) => ({ data: state.data }) +}) +``` + +This example shows how to implement a simple component that renders an employee profile in the `display` function, fetches data (if necessary) in the `setup` function, and preserves data in the `teardown` function. + ### h3.dispatch(event: string, data: any) Dispatches a event and optionally some data. Messages are typically handled centrally by modules.@@ -139,20 +196,20 @@ ```
A event name can be any string, but keep in mind that the following names (and typically any name starting with `$`) are reserved for framework use: -* `$init` — Dispatched when the application is initialized. Useful to initialize application state. -* `$redraw` — Dispatched after an application redraw is triggered. -* `$navigation` — Dispatched after a navigation occurs. -* `$log` — Dispatched after *any* event (except `$log` iself) is dispatched. Very useful for debugging. +- `$init` — Dispatched when the application is initialized. Useful to initialize application state. +- `$redraw` — Dispatched after an application redraw is triggered. +- `$navigation` — Dispatched after a navigation occurs. +- `$log` — Dispatched after _any_ event (except `$log` iself) is dispatched. Very useful for debugging. ### h3.init(config: object) The initialization method of every H3 application. You _must_ call this method once to initialize your application by providing a component to render or configuration object with the following properties: -* **element** (Element) — The DOM Element to which the Application will be attached (default: `document.body`). -* **modules** (Array) — An array of functions used to handle the application state that will be executed once before starting the application. -* **routes** (Object) — An object containing routing definitions, using paths as keys and components as values. Routing paths can contain named parts like `:name` or `:id` which will populate the `parts` property of the current route (`h3.route`). -* **preStart** (Function) — An optional function to be executed before the application is first rendered. -* **postStart** (Function) — An optional function to be executed after the application is first rendered. +- **element** (Element) — The DOM Element to which the Application will be attached (default: `document.body`). +- **modules** (Array) — An array of functions used to handle the application state that will be executed once before starting the application. +- **routes** (Object) — An object containing routing definitions, using paths as keys and components as values. Routing paths can contain named parts like `:name` or `:id` which will populate the `parts` property of the current route (`h3.route`). +- **preStart** (Function) — An optional function to be executed before the application is first rendered. +- **postStart** (Function) — An optional function to be executed after the application is first rendered. This is an example of a simple routing configuration:@@ -173,14 +230,14 @@
The following call causes the application to switch to the following URL: `#/posts/?orderBy=date&direction=desc`. ```js -h3.navigateTo("/posts/", {orderBy: 'date', direction: 'desc'}); +h3.navigateTo("/posts/", { orderBy: "date", direction: "desc" }); ``` ### h3.on(event: string, handler: function) Subscribes to the specified event and executes the specified handler function whenever the event is dispatched. Returns a function that can be used to delete the subscription. -Subscriptions should be typically managed in modules rather than in components: a component gets rendered several times and subscriptions *must* be properly cleaned up to avoid memory leaks. +Subscriptions should be typically managed in modules rather than in components: a component gets rendered several times and subscriptions _must_ be properly cleaned up to avoid memory leaks. Example:@@ -193,18 +250,18 @@ ```
### h3.redraw() -Triggers an application redraw. Unlike most frameworks, in H3 redraws *must* be triggered explicitly. Just call this method whenever you want something to change and components to re-render. +Triggers an application redraw. Unlike most frameworks, in H3 redraws _must_ be triggered explicitly. Just call this method whenever you want something to change and components to re-render. ### h3.route An read-only property containing current route (Route object). A Route object has the following properties: -* **path** — The current path (fragment without #) without query string parameters, e.g. `/posts/134` -* **def** — The matching route definition, e.g. `/posts/:id` -* **query** — The query string, if present, e.g. `?comments=yes` -* **part** — An object containing the values of the parts defined in the route, e.g. `{id: "134"}` -* **params** — An object containing the query string parameters, e.g. `{comments: "yet"}` +- **path** — The current path (fragment without #) without query string parameters, e.g. `/posts/134` +- **def** — The matching route definition, e.g. `/posts/:id` +- **query** — The query string, if present, e.g. `?comments=yes` +- **part** — An object containing the values of the parts defined in the route, e.g. `{id: "134"}` +- **params** — An object containing the query string parameters, e.g. `{comments: "yet"}` ### h3.state -A read-only property containing the current application state. The state is a plain object, but its properties should only be modified using event subscription handlers. +A read-only property containing the current application state. The state is a plain object, but its properties should only be modified using event subscription handlers.
@@ -0,0 +1,36 @@
+## Best Practices + +This page lists some common tips and best practices to get the most out of H3. Some of these may sound counter-intuitive (especially if you are using to frameworks advocating absolute data immutability), but they work well with H3 because of the way it is designed. + +### Embrace Mutability + +No, that's not a mistake. Although you should understand [why immutability is important](https://stackoverflow.com/questions/34385243/why-is-immutability-so-important-or-needed-in-javascript), you shouldn't force yourself to use it in all situations. Instead, you should go through [this article](https://desalasworks.com/article/immutability-in-javascript-a-contrarian-view/) and try to understand also a contrarian view of immutability. + +In H3, changes only occur when needed. Most notably, when re-rendering the Virtual DOM tree of the application will be *mutated in place*, but only where necessary. Functions as well are considered equal if their source code (i.e. string representation) is equal. While this can cause problems in some situations if you are not aware of it, it can be beneficial and actually simplify things most of the time. + +When managing state, if something is different you should typically _just change it_, unless it's shared across the whole application through the Store, in which case (but only in that case) you should try to manage change without side effects and following basic immutability rules. As a rule of thumb, Modules should manage shared application state in an immutable way. + +### Components + +* Avoid nesting component definitions, to avoid creating stale closures. Only define components within other components if you are not relying on changes affecting variables defined in the outer component within the inner component. +* Component should only mutate their own local state. +* Pay attention when relying on captured variables in event handlers, as they may become stale. In certain situations, you can pass data to event handlers through the DOM instead. + * Add identifiers to the real DOM and use `event.currentTarget` in event handlers. + * Use dataset to store identfiers in the real DOM. + + +### Screens + +* Use screens for complex, stateful components orchestrating nested "dumb" components. +* Store event subscription destructors in the screen state and call them in the `teardown` method. +* Return an object from the `teardown` method to preserve route state across screens, only if needed. +* Use the `setup` method to define local, screen-level state that will be accessible in the `display` method. + +### State Management + +* Application state should be stored in the H3 store and should not be mutated. +* Use mutable objects for non-stored local state. +* Use screen state to share data among complex components hierarchies. +* Define separate model classes for complex objects. +* Move complex data manipulation logic to model classes. +* Use url parts and params as an easy way to keep references to recreate application state.
@@ -8,16 +8,14 @@ ### HyperScript
H3 uses a [HyperScript](https://openbase.io/js/hyperscript)-like syntax to create HTML elements in pure JavaScript. No, you are actually creating Virtual DOM nodes with it but it can be easier to think about them as HTML elements, or better, something that *eventually* will be rendered as an HTML element. -The main difference between H3's HyperScript implementation and others is that it uses **h3** as the main constructor to create nodes. HyperScript uses **h**, Mithril uses **m**, ...kind of an obvious choice if you ask me. If you don't like it, you can rename it to *piripicchio* if you want, and it will *still* be used in the same way. - How, you ask? Like this: ```js -h3("div.test", [ - h3("ul", [ - h3("li", "This is..."), - h3("li", "...a simple..."), - h3("li", "unordered list.") +h("div.test", [ + h("ul", [ + h("li", "This is..."), + h("li", "...a simple..."), + h("li", "unordered list.") ]) ]); ```@@ -36,7 +34,7 @@ ```
Simple enough. Yes there are some quirks to it, but check the API or Usage docs for those. -### Components +### Component In H3, a component is a function that returns a Virtual Node or a string (that will be treated as a textual DOM node).@@ -45,19 +43,40 @@
```js let count = 0; const CounterButton = () => { - return h3("button", { + return h("button", { onclick: () => count +=1 && h3.redraw() }, `You clicked me ${count} times.`); } ``` +### Router + +H3 comes with a very minimal but fully functional URL fragment router. You create your application routes when initializing your application, and you can navigate to them using ordinary `href` links or programmatically using the `h3.navigateTo` method. + +The current route is always accessible via the `h3.route` property. + + +### Screen + +A screen is a top-level component that handles a route. Unlike ordinary components, screens: + +* may have a dedicated *setup* (after the screen is added to the DOM) and *teardown* phase (after the screen is removed from the DOM and before the new screen is loaded). +* may have built-in local state, initialized during setup and (typically) destroyed during teardown. Such state is passed as the first (and only) parameter of the screen when executed. + +Screens are typically created using the **h3.screen** shorthand method, but they can stll created using an ordinary function returning a VNode, but you can optionally define a **setup** and a **teardown** async methods on them (functions are objects in JavaScript after all...) to be executed during each corresponding phase. + +Note that: +* Both the **setup** method take an object as a parameter, representing the component state. Such object will be empty the first time the **setup** method is called for a given component, but it may contain properties not removed during subsequent teardowns. +* The **teardown** method can return an object, which will be retained as component state. If however nothing is returned, the component state object is emptied. +* Both methods can be asynchronous, in which case H3 will wait for their completion before proceeding. + ### Store H3 essentially uses something very, *very* similar to [Storeon](https://github.com/storeon/storeon) for state management *and* also as a very simple client-side event dispatcher/subscriber (seriously, it is virtually the same code as Storeon). Typically you'll only use the default store created by H3 upon initialization, and you'll use the `h3.dispatch()` and `h3.on()` methods to dispatch and subscribe to events. The current application state is accessible via the `h3.state` property. -### Modules +### Module The `h3.init()` method takes an array of *modules* that can be used to manipulate the application state when specific events are received. A simple module looks like this:@@ -71,27 +90,6 @@ ```
Essentially a module is just a function that typically is meant to run only once to define one or more event subscriptions. Modules are the place where you should handle state changes in your application. -### Router - -H3 comes with a very minimal but fully functional URL fragment router. You create your application routes when initializing your application, and you can navigate to them using ordinary `href` links or programmatically using the `h3.navigateTo` method. - -The current route is always accessible via the `h3.route` property. - - -#### Route Components - -A route component is a top-level component that handles a route. Unlike ordinary components, route components: - -* may have a dedicated *setup* (after the route component is added to the DOM) and *teardown* phase (after the route component is removed from the DOM and before the new route component is loaded). -* may have built-in local state, initialized during setup and (typically) destroyed during teardown. Such state is passed as the first (and only) parameter of the route component when executed. - -Route components are stll created using ordinary function returning a VNode, but you can optionally define a **setup** and a **teardown** async methods on them (functions are objects in JavaScript after all...) to be executed during each corresponding phase. - -Note that: -* Both the **setup** method take an object as a parameter, representing the component state. Such object will be empty the first time the **setup** method is called for a given component, but it may contain properties not removed during subsequent teardowns. -* The **teardown** method can return an object, which will be retained as component state. If however nothing is returned, the component state object is emptied. -* Both methods can be asynchronous, in which case H3 will wait for their completion before proceeding. - ### How everything works... The following sequence diagram summarizes how H3 works, from its initialization to the redraw and navigation phases.@@ -105,9 +103,9 @@ 2. Any *Module* specified when calling `h3.init()` is executed.
3. The **$init** event is dispatched. 4. The *preStart* function (if specified when calling `h3.init()`) is executed. 5. The *Router* is initialized and started. -6. The **setup()** method of the matching Route Component is called (if any). +6. The **setup()** method of the matching Screen is called (if any). 8. The **$navigation** event is dispatched. -9. The *Route Component* matching the current route and all its child components are rendered for the first time. +9. The *Screen* matching the current route and all its child components are rendered for the first time. 10. The **$redraw** event is dispatched. Then, whenever the `h3.redraw()` method is called (typically within a component):@@ -118,12 +116,12 @@
Similarly, whenever the `h3.navigateTo()` method is called (typically within a component), or the URL fragment changes: 1. The *Router* processes the new path and determine which component to render based on the routing configuration. -2. The **teardow()** method of the current Route Component is called (if any). -3. The **setup()** method of the new matching Route Component is called (if any). +2. The **teardow()** method of the current Screen is called (if any). +3. The **setup()** method of the new matching Screen is called (if any). 4. All DOM nodes within the scope of the routing are removed, all components are removed. 6. The **$navigation** event is dispatched. 7. All DOM nodes are removed. -8. The *Route Component* matching the new route and all its child components are rendered. +8. The *Screen* matching the new route and all its child components are rendered. 10. The **$redraw** event is dispatched. And that's it. The whole idea is to make the system extremely *simple* and *predictable* — which means everything should be very easy to debug, too.
@@ -4,15 +4,15 @@ **H3** is a microframework to build client-side single-page applications (SPAs) in modern JavaScript.
H3 is also: -- **tiny**, less than 4KB minified gzipped. +- **tiny**, less than 4KB minified and gzipped. - **modern**, in the sense that it runs only in modern browsers (latest versions of Chrome, Firefox, Edge & similar). -- **easy** to learn, its API is comprised of only six methods and two properties. +- **easy** to learn, its API is comprised of only seven methods and two properties. ### I'm sold! Where can I get it? Here, look, it's just one file: -<a href="https://raw.githubusercontent.com/h3rald/h3/v0.9.0/h3.js" target="_blank" class="button primary">Download v0.9.0 (Impeccable Iconian)</a> +<a href="https://raw.githubusercontent.com/h3rald/h3/v0.10.0/h3.js" target="_blank" class="button primary">Download v0.10.0 (Jittery Jem'Hadar)</a> <small>Or get the minified version [here](https://raw.githubusercontent.com/h3rald/h3/v0.9.0/h3.min.js).</small>@@ -24,7 +24,7 @@ Here's an example of an extremely minimal SPA created with H3:
```js import h3 from "./h3.js"; -h3.init(() => h3("h1", "Hello, World!")); +h3.init(() => h("h1", "Hello, World!")); ``` This will render a `h1` tag within the document body, containing the text `"Hello, World!"`.
@@ -43,10 +43,10 @@ // A simple component printing the current date and time
// Pressig the Refresh button causes the application to redraw // And updates the displayed date/dime. const Page = () => { - return h3("main", [ - h3("h1", "Welcome!"), - h3("p", `The current date and time is ${new Date()}`), - h3("button", { + return h("main", [ + h("h1", "Welcome!"), + h("p", `The current date and time is ${new Date()}`), + h("button", { onclick: () => h3.redraw() }, "Refresh") ]);
@@ -1,12 +1,12 @@
-## Tutorial +## Tutorial As a (meta) explanation of how to use H3, let's have a look at how the [H3 web site](https://h3.js.org) itself was created. The idea was to build a simple web site to display the documentation of the H3 microframework, so it must be able to: -* Provide a simple way to navigate through page. -* Render markdown content (via [marked.js](https://marked.js.org/#/README.md#README.md)) -* Apply syntax highlighting (via [Prism.js](https://prismjs.com/)) +- Provide a simple way to navigate through page. +- Render markdown content (via [marked.js](https://marked.js.org/#/README.md#README.md)) +- Apply syntax highlighting (via [Prism.js](https://prismjs.com/)) As far as look and feel is concerned, I wanted something minimal but functional, so [mini.css](https://minicss.org/) was more than enough.@@ -14,20 +14,23 @@ The full source of the site is available [here](https://github.com/h3rald/h3/tree/master/docs).
### Create a simple HTML file -Start by creating a simple HTML file. Place a script loading the entry point of your application within the `body` and set its type to `module`. +Start by creating a simple HTML file. Place a script loading the entry point of your application within the `body` and set its type to `module`. This will let you load an ES6 file containing imports to other files... it works in all major browsers, but it doesn't work in IE (but we don't care about that, do we?). ```html -<!doctype html> +<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>H3</title> - <meta name="description" content="A bare-bones client-side web microframework" /> + <meta + name="description" + content="A bare-bones client-side web microframework" + /> <meta name="author" content="Fabio Cevasco" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="shortcut icon" href="favicon.png" type="image/png"> + <link rel="shortcut icon" href="favicon.png" type="image/png" /> <link rel="stylesheet" href="css/mini-default.css" /> <link rel="stylesheet" href="css/prism.css" /> <link rel="stylesheet" href="css/style.css" />@@ -47,7 +50,7 @@
Start by importing all the JavaScript modules you need: ```js -import h3 from "./h3.js"; +import { h3, h } from "./h3.js"; import marked from "./vendor/marked.js"; import Prism from "./vendor/prism.js"; ```@@ -59,6 +62,7 @@ const labels = {
overview: "Overview", "quick-start": "Quick Start", "key-concepts": "Key Concepts", + "best-practices": "Best Practices", tutorial: "Tutorial", api: "API", about: "About",@@ -67,16 +71,12 @@ ```
We are going to store the HTML contents of each page in an Object, and we're going to need a simple function to [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) the Markdown file and render it as HTML: - ```js -const pages = {}; - -const fetchPage = async (pages, id, md) => { +const fetchPage = async ({ pages, id, md }) => { if (!pages[id]) { const response = await fetch(md); const text = await response.text(); pages[id] = marked(text); - h3.redraw(); } }; ```@@ -84,80 +84,96 @@
Basically this function is going to be called when you navigate to each page, and it: 1. fetches the content of the requested file (`md`)) -2. renders the Markdown code into HTML using marked, and stores it in the `pages` object -3. Triggers a redraw of the application +2. renders the Markdown code into HTML using the _marked_ library, and stores it in the `pages` object -We are gonna use our `fetchPage` function inside the main component of our app, `Page`: +We are gonna use our `fetchPage` function inside the `setup` of the main (and only) screen of our app, `Page`: ```js -const Page = () => { - const id = h3.route.path.slice(1); - const ids = Object.keys(labels); - const md = ids.includes(id) ? `md/${id}.md` : `md/overview.md`; - fetchPage(pages, id, md); - return h3("div.page", [ - Header, - h3("div.row", [ - h3("input#drawer-control.drawer", { type: "checkbox" }), - Navigation(id, ids), - Content(pages[id]), - Footer, - ]), - ]); -}; +const Page = h3.screen({ + setup: async (state) => { + state.pages = {}; + state.id = h3.route.path.slice(1); + state.ids = Object.keys(labels); + state.md = state.ids.includes(state.id) + ? `md/${state.id}.md` + : `md/overview.md`; + await fetchPage(state); + }, + display: (state) => { + return h("div.page", [ + Header, + h("div.row", [ + h("input#drawer-control.drawer", { type: "checkbox" }), + Navigation(state.id, state.ids), + Content(state.pages[state.id]), + Footer, + ]), + ]); + }, + teardown: (state) => state, +}); ``` -The main responsibility of this component is to fetch the Markdown content and render the whole page, but note how the rendering different portions of the page are delegated to different components: `Header`, `Navigation`, `Content`, and `Footer`. +Note that this screen has a `setup`, a `display` and a `teardown` method, both taking `state` as parameter. In H3, screens are nothing but stateful components that are used to render the whole page of the application, and are therefore typically redered when navigating to a new route. + +The `state` parameter is nothing but an empty object that can be used to store data that will be accessible to the `setup`, `display` and `teardown` methods, and (typically) will be destroyed when another screen is rendered. + +The `setup` function allows you to perform some operations that should take place _before_ the screen is rendered. In this case, we want to fetch the page contents (if necessary) beforehand to avoid displaying a spinner while the content is being loaded. Note that the `setup` method can be asynchronous, and in this case the `display` method will not be called until all asynchronous operations have been completed (assuming you are `await`ing them). + +The `teardown` function in this case only makes sure that the existing screen state (in particular any loaded markdown page) will be passed on to the next screen during navigation (which, in this case, is still the `Page` screen), so that existing pages will not be fetched again. + +The main responsibility of this screen is to fetch the Markdown content and render the whole page, but note how the rendering different portions of the page are delegated to different components: `Header`, `Navigation`, `Content`, and `Footer`. The `Header` and `Footer` components are very simple: their only job is to render static content: ```js const Header = () => { - return h3("header.row.sticky", [ - h3("a.logo.col-sm-1", { href: "#/" }, [ - h3("img", { alt: "H3", src: "images/h3.svg" }), + return h("header.row.sticky", [ + h("a.logo.col-sm-1", { href: "#/" }, [ + h("img", { alt: "H3", src: "images/h3.svg" }), ]), - h3("div.version.col-sm.col-md", [ - h3("div.version-number", "v0.9.0"), - h3("div.version-label", "“Impeccable Iconian“"), + h("div.version.col-sm.col-md", [ + h("div.version-number", "v0.10.0"), + h("div.version-label", "“Jittery Jem'Hadar“"), ]), - h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), + h("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), ]); }; const Footer = () => { - return h3("footer", [h3("div", "© 2020 Fabio Cevasco")]); + return h("footer", [h("div", "© 2020 Fabio Cevasco")]); }; ``` The `Navigation` component is more interesting, as it takes two parameters: -* The ID of the current page -* The list of page IDs +- The ID of the current page +- The list of page IDs ...and it uses this information to create the site navigation menu dynamically: ```js const Navigation = (id, ids) => { const menu = ids.map((p) => - h3(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) + h(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) ); - return h3("nav#navigation.col-md-3", [ - h3("label.drawer-close", { for: "drawer-control" }), + return h("nav#navigation.col-md-3", [ + h("label.drawer-close", { for: "drawer-control" }), ...menu, ]); }; ``` -Finally, the `Content` component optionally takes a string containing the HTML of the page content to render. If no content is provided, it will display a loading spinner, otherwise it will render the content by using the special `$html` attribute that can be used to essentially set the `innerHTML` of an element: +Finally, the `Content` component takes a string containing the HTML of the page content to render using the special `$html` attribute that can be used to essentially set the `innerHTML` property of an element: ```js const Content = (html) => { - const content = html - ? h3("div.content", { $html: html }) - : h3("div.spinner-container", h3("span.spinner")); - return h3("main.col-sm-12.col-md-9", [ - h3("div.card.fluid", h3("div.section", content)), + const content = h("div.content", { $html: html }); + return h("main.col-sm-12.col-md-9", [ + h( + "div.card.fluid", + h("div.section", { $onrender: () => Prism.highlightAll() }, content) + ), ]); }; ```@@ -170,7 +186,9 @@ In a similar way, other well-known pages can easily be mapped to IDs, but it is also important to handle _unknown_ pages (technically I could even pass an URL to a different site containing a malicious markdown page and have it rendered!), and if a page passed in the URL fragment is not present in the `labels` Object, the Overview page will be rendered instead.
This feature is also handy to automatically load the Overview when no fragment is specified. -### Initialization and post-redraw operations +What is that weird `$onrender` property you ask? Well, that's a H3-specific callback that will be executed whenever the corresponding DOM node is rendered... that's essentially the perfect place to for executing operations that must be perform when the DOM is fully available, like highlighting our code snippets using _Prism_ in this case. + +### Initialization Done? Not quite. We need to initialize the SPA by passing the `Page` component to the `h3.init()` method to trigger the first rendering:@@ -178,18 +196,12 @@ ```js
h3.init(Page); ``` -And that's it. Noooo wait, what about syntax highlighting? That needs to be applied _after_ the HTML markup is rendered. How can we manage that? - -Easy enough, add a handler to be executed whenever the SPA is redrawn: - -```js -h3.on("$redraw", () => Prism.highlightAll()); -``` +And that's it. Now, keep in mind that this is the _short_ version of initialization using a single component and a single route, but still, that's good enough for our use case. ### Next steps Made it this far? Good. Wanna know more? Have a look at the code of the [todo list example](https://github.com/h3rald/h3/tree/master/docs/example) and try it out [here](https://h3.js.org/example/index.html). -Once you feel more comfortable and you are ready to dive into a more complex application, featuring different routes, route components, forms, confirmation messages, plenty of third-party components etc., have a look at [LitePad](https://github.com/h3rald/litepad). You can see it in action here: [litepad.h3rald.com](https://litepad.h3rald.com/). +Once you feel more comfortable and you are ready to dive into a more complex application, featuring different routes, screens, forms, validation, confirmation messages, plenty of third-party components etc., have a look at [LitePad](https://github.com/h3rald/litepad). You can see it in action here: [litepad.h3rald.com](https://litepad.h3rald.com/). -Note: the LitePad online demo will store all its data in localStorage.+Note: the LitePad online demo will store all its data in localStorage.
@@ -1,7 +1,7 @@
/** - * H3 v0.9.0 "Impeccable Iconian" + * H3 v0.10.0 "Jittery Jem'Hadar" * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com> - * + * * @license MIT * For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE */@@ -41,8 +41,6 @@ return false;
} } if ([String, Number, Boolean].includes(obj1.constructor)) { - if (obj1 !== obj2) { - } return obj1 === obj2; } if (obj1.constructor === Array) {@@ -56,18 +54,18 @@ }
} return true; } - return checkProperties(obj1, obj2); // && checkProperties(obj2, obj1); + return checkProperties(obj1, obj2); }; const selectorRegex = /^([a-z][a-z0-9:_=-]*)?(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; - +const [PATCH, INSERT, DELETE] = [-1, -2, -3]; let $onrenderCallbacks = []; -// Virtual Node Implementation with HyperScript-like syntax +// Virtual DOM implementation with HyperScript syntax class VNode { constructor(...args) { this.type = undefined; - this.attributes = {}; + this.props = {}; this.data = {}; this.id = undefined; this.$html = undefined;@@ -133,25 +131,34 @@ // Not a VNode, assume props object
this.processProperties(data); } } - } else if (args.length === 3) { + } else { let [selector, props, children] = args; + if (args.length > 3) { + children = args.slice(2); + } + children = Array.isArray(children) ? children : [children]; if (typeof selector !== "string") { throw new Error( "[VNode] Invalid first argument passed to VNode constructor." ); } this.processSelector(selector); - if (typeof props !== "object" || props === null) { - throw new Error( - "[VNode] Invalid second argument passed to VNode constructor." - ); + if ( + props instanceof Function || + props instanceof VNode || + typeof props === "string" + ) { + // 2nd argument is a child + children = [props].concat(children); + } else { + if (typeof props !== "object" || props === null) { + throw new Error( + "[VNode] Invalid second argument passed to VNode constructor." + ); + } + this.processProperties(props); } - this.processProperties(props); this.processChildren(children); - } else { - throw new Error( - "[VNode] Too many arguments passed to VNode constructor." - ); } }@@ -166,7 +173,7 @@ this.data = data.data;
this.value = data.value; this.eventListeners = data.eventListeners; this.children = data.children; - this.attributes = data.attributes; + this.props = data.props; this.classList = data.classList; }@@ -185,7 +192,7 @@ this.classList =
attrs.classList && attrs.classList.length > 0 ? attrs.classList : this.classList; - this.attributes = attrs; + this.props = attrs; Object.keys(attrs) .filter((a) => a.startsWith("on") && attrs[a]) .forEach((key) => {@@ -195,19 +202,19 @@ `[VNode] Event handler specified for ${key} event is not a function.`
); } this.eventListeners[key.slice(2)] = attrs[key]; - delete this.attributes[key]; + delete this.props[key]; }); - delete this.attributes.value; - delete this.attributes.$html; - delete this.attributes.$onrender; - delete this.attributes.id; - delete this.attributes.data; - delete this.attributes.style; - delete this.attributes.classList; + delete this.props.value; + delete this.props.$html; + delete this.props.$onrender; + delete this.props.id; + delete this.props.data; + delete this.props.style; + delete this.props.classList; } processSelector(selector) { - if (!selector.match(selectorRegex)) { + if (!selector.match(selectorRegex) || selector.length === 0) { throw new Error(`[VNode] Invalid selector: ${selector}`); } const [, type, id, classes] = selector.match(selectorRegex);@@ -263,17 +270,16 @@ const node = document.createElement(this.type);
if (this.id) { node.id = this.id; } - Object.keys(this.attributes).forEach((attr) => { - // Set attributes (only if non-empty strings) - if (this.attributes[attr] && typeof this.attributes[attr] === "string") { - const a = document.createAttribute(attr); - a.value = this.attributes[attr]; - node.setAttributeNode(a); + Object.keys(this.props).forEach((p) => { + // Set attributes + if (typeof this.props[p] === "boolean") { + this.props[p] ? node.setAttribute(p, "") : node.removeAttribute(p); + } + if (["string", "number"].includes(typeof this.props[p])) { + node.setAttribute(p, this.props[p]); } // Set properties - if (typeof this.attributes[attr] !== "string" || !node[attr]) { - node[attr] = this.attributes[attr]; - } + node[p] = this.props[p]; }); // Event Listeners Object.keys(this.eventListeners).forEach((event) => {@@ -281,7 +287,11 @@ node.addEventListener(event, this.eventListeners[event]);
}); // Value if (this.value) { - node.value = this.value; + if (["textarea", "input"].includes(this.type)) { + node.value = this.value; + } else { + node.setAttribute("value", this.value); + } } // Style if (this.style) {@@ -332,8 +342,12 @@ oldvnode.id = newvnode.id;
} // Value if (oldvnode.value !== newvnode.value) { - node.value = newvnode.value || ""; oldvnode.value = newvnode.value; + if (["textarea", "input"].includes(oldvnode.type)) { + node.value = newvnode.value || ""; + } else { + node.setAttribute("value", newvnode.value || ""); + } } // Classes if (!equal(oldvnode.classList, newvnode.classList)) {@@ -370,27 +384,34 @@ }
}); oldvnode.data = newvnode.data; } - // Attributes - if (!equal(oldvnode.attributes, newvnode.attributes)) { - Object.keys(oldvnode.attributes).forEach((a) => { - if (newvnode.attributes[a] === false) { - node[a] = false; - } - if (!newvnode.attributes[a]) { + // Properties & Attributes + if (!equal(oldvnode.props, newvnode.props)) { + Object.keys(oldvnode.props).forEach((a) => { + node[a] = newvnode.props[a]; + if (typeof newvnode.props[a] === "boolean") { + oldvnode.props[a] = newvnode.props[a]; + newvnode.props[a] + ? node.setAttribute(a, "") + : node.removeAttribute(a); + } else if (!newvnode.props[a]) { + delete oldvnode.props[a]; node.removeAttribute(a); } else if ( - newvnode.attributes[a] && - newvnode.attributes[a] !== oldvnode.attributes[a] + newvnode.props[a] && + newvnode.props[a] !== oldvnode.props[a] ) { - node.setAttribute(a, newvnode.attributes[a]); + oldvnode.props[a] = newvnode.props[a]; + if (["string", "number"].includes(typeof newvnode.props[a])) { + node.setAttribute(a, newvnode.props[a]); + } } }); - Object.keys(newvnode.attributes).forEach((a) => { - if (!oldvnode.attributes[a] && newvnode.attributes[a]) { - node.setAttribute(a, newvnode.attributes[a]); + Object.keys(newvnode.props).forEach((a) => { + if (!oldvnode.props[a] && newvnode.props[a]) { + oldvnode.props[a] = newvnode.props[a]; + node.setAttribute(a, newvnode.props[a]); } }); - oldvnode.attributes = newvnode.attributes; } // Event listeners if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) {@@ -412,113 +433,48 @@ });
oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - let map = []; - let oldNodesFound = 0; - let newNodesFound = 0; - // First look for existing nodes - for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - let found = -1; - for (let index = 0; index < newvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - newNodesFound++; - oldNodesFound++; - break; - } - } - map.push(found); - } - if ( - newNodesFound === oldNodesFound && - newvnode.children.length === oldvnode.children.length - ) { - // something changed but everything else is the same - return map; - } - if (newNodesFound === newvnode.children.length) { - // All children in newvnode exist in oldvnode - // All nodes that are not found must be removed - for (let i = 0; i < map.length; i++) { - if (map[i] === -1) { - map[i] = -3; - } - } - } - if (oldNodesFound === oldvnode.children.length) { - // All children in oldvnode exist in newvnode - // Check where the missing newvnodes children need to be added - for ( - let newIndex = 0; - newIndex < newvnode.children.length; - newIndex++ - ) { - if (!map.includes(newIndex)) { - map.splice(newIndex, 0, -2); - } - } - } - // Check if nodes needs to be removed (if there are fewer children) - if (newvnode.children.length < oldvnode.children.length) { - for (let i = 0; i < map.length; i++) { - if (map[i] === -1 && !newvnode.children[i]) { - map[i] = -3; - } - } - } - return map; - } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; + let resultMap = [...Array(newvnode.children.length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; - for (let i of childMap) { + checkmap: for (const i of childMap) { count++; - let breakFor = false; if (i === count) { // Matching nodes; continue; } switch (i) { - case -1: - // different node, check + case PATCH: { oldvnode.children[count].redraw({ node: node.childNodes[count], vnode: newvnode.children[count], }); - break; - case -2: - // add node + break checkmap; + } + case INSERT: { oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); - breakFor = true; - break; - case -3: - // remove node + break checkmap; + } + case DELETE: { oldvnode.children.splice(count, 1); node.removeChild(node.childNodes[count]); - breakFor = true; - break; - default: - // Node found, move nodes and remap + break checkmap; + } + default: { const vtarget = oldvnode.children.splice(i, 1)[0]; oldvnode.children.splice(count, 0, vtarget); - node.insertBefore(node.childNodes[i], node.childNodes[count]); - breakFor = true; - break; - } - if (breakFor) { - break; + const target = node.removeChild(node.childNodes[i]); + node.insertBefore(target, node.childNodes[count]); + break checkmap; + } } } childMap = mapChildren(oldvnode, newvnode); - resultMap = [...Array(childMap.length).keys()]; + resultMap = [...Array(newvnode.children.length).keys()]; } // $onrender if (!equal(oldvnode.$onrender, newvnode.$onrender)) {@@ -533,6 +489,40 @@ }
} } +const mapChildren = (oldvnode, newvnode) => { + const newList = newvnode.children; + const oldList = oldvnode.children; + let map = []; + for (let nIdx = 0; nIdx < newList.length; nIdx++) { + let op = PATCH; + for (let oIdx = 0; oIdx < oldList.length; oIdx++) { + if (equal(newList[nIdx], oldList[oIdx]) && !map.includes(oIdx)) { + op = oIdx; // Same node found + break; + } + } + if ( + op < 0 && + newList.length >= oldList.length && + map.length >= oldList.length + ) { + op = INSERT; + } + map.push(op); + } + const oldNodesFound = map.filter((c) => c >= 0); + if (oldList.length > newList.length) { + // Remove remaining nodes + [...Array(oldList.length - newList.length).keys()].forEach(() => + map.push(DELETE) + ); + } else if (oldNodesFound.length === oldList.length) { + // All nodes not found are insertions + map = map.map((c) => (c < 0 ? INSERT : c)); + } + return map; +}; + /** * The code of the following class is heavily based on Storeon * Modified according to the terms of the MIT License@@ -643,15 +633,17 @@ if (!this.route) {
throw new Error(`[Router] No route matches '${fragment}'`); } // Old route component teardown + let state = {}; if (oldRoute) { const oldRouteComponent = this.routes[oldRoute.def]; - oldRouteComponent.state = - oldRouteComponent.teardown && - (await oldRouteComponent.teardown(oldRouteComponent.state)); + state = + (oldRouteComponent.teardown && + (await oldRouteComponent.teardown(oldRouteComponent.state))) || + state; } // New route component setup const newRouteComponent = this.routes[this.route.def]; - newRouteComponent.state = {}; + newRouteComponent.state = state; newRouteComponent.setup && (await newRouteComponent.setup(newRouteComponent.state)); // Redrawing...@@ -685,9 +677,12 @@ }
} // High Level API -const h3 = (...args) => { + +export const h = (...args) => { return new VNode(...args); }; + +export const h3 = {}; let store = null; let router = null;@@ -782,6 +777,26 @@ }
redrawing = true; router.redraw(); redrawing = setRedrawing || false; +}; + +h3.screen = ({ setup, display, teardown }) => { + if (!display || typeof display !== "function") { + throw new Error("[h3.screen] No display property specified."); + } + if (setup && typeof setup !== "function") { + throw new Error("[h3.screen] setup property is not a function."); + } + if (teardown && typeof teardown !== "function") { + throw new Error("[h3.screen] teardown property is not a function."); + } + const fn = display; + if (setup) { + fn.setup = setup; + } + if (teardown) { + fn.teardown = teardown; + } + return fn; }; export default h3;
@@ -1,1 +1,1 @@
-{"version":3,"sources":["0"],"names":["checkProperties","obj1","obj2","key","equal","undefined","constructor","toString","String","Number","Boolean","includes","Array","length","i","selectorRegex","$onrenderCallbacks","VNode","[object Object]","args","this","type","attributes","data","id","$html","$onrender","style","value","children","classList","eventListeners","Error","vnode","processSelector","from","processVNodeObject","selector","isArray","Function","processChildren","processProperties","props","a","b","attrs","Object","keys","filter","startsWith","forEach","slice","match","classes","split","arg","map","c","document","createTextNode","node","createElement","attr","createAttribute","setAttributeNode","event","addEventListener","cssText","add","dataset","cnode","render","appendChild","push","innerHTML","newvnode","oldvnode","renderedNode","parentNode","replaceChild","mapChildren","oldNodesFound","newNodesFound","oldIndex","found","index","newIndex","splice","remove","setAttribute","removeAttribute","removeEventListener","childMap","resultMap","count","breakFor","redraw","childNodes","insertBefore","removeChild","vtarget","Store","events","state","dispatch","cb","Route","path","def","query","parts","params","p","name","decodeURIComponent","Router","element","routes","store","location","window","route","processPath","async","oldRoute","fragment","newURL","hash","replace","rawQuery","pathParts","routeParts","rP","pP","oldRouteComponent","teardown","newRouteComponent","setup","redrawing","firstChild","setRedraw","cbk","scrollTo","encodeURIComponent","join","h3","router","init","config","modules","preStart","postStart","/","body","Element","Promise","resolve","then","start","navigateTo","defineProperty","get","on","setRedrawing"],"mappings":"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","file":"h3.js"}+{"version":3,"sources":["0"],"names":["checkProperties","obj1","obj2","key","equal","undefined","constructor","toString","String","Number","Boolean","includes","Array","length","i","selectorRegex","PATCH","INSERT","DELETE","$onrenderCallbacks","VNode","[object Object]","args","this","type","props","data","id","$html","$onrender","style","value","children","classList","eventListeners","Error","vnode","processSelector","from","processVNodeObject","selector","isArray","Function","processChildren","processProperties","slice","concat","a","b","attrs","Object","keys","filter","startsWith","forEach","match","classes","split","arg","map","c","document","createTextNode","node","createElement","p","setAttribute","removeAttribute","event","addEventListener","cssText","add","dataset","cnode","render","appendChild","push","innerHTML","newvnode","oldvnode","renderedNode","parentNode","replaceChild","remove","removeEventListener","childMap","mapChildren","resultMap","count","checkmap","redraw","childNodes","splice","insertBefore","removeChild","vtarget","target","newList","oldList","nIdx","op","oIdx","oldNodesFound","Store","events","state","dispatch","cb","Route","path","def","query","parts","params","name","decodeURIComponent","Router","element","routes","store","location","window","route","processPath","async","oldRoute","fragment","newURL","hash","replace","rawQuery","pathParts","routeParts","index","rP","pP","oldRouteComponent","teardown","newRouteComponent","setup","redrawing","firstChild","setRedraw","cbk","scrollTo","encodeURIComponent","join","h","h3","router","init","config","modules","preStart","postStart","/","body","Element","Promise","resolve","then","start","navigateTo","defineProperty","get","on","setRedrawing","screen","display","fn"],"mappings":"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","file":"h3.js"}
@@ -1,9 +1,9 @@
/** - * H3 v0.9.0 "Impeccable Iconian" + * H3 v0.10.0 "Jittery Jem'Hadar" * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com> - * + * * @license MIT * For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE */ -const checkProperties=(e,t)=>{for(const r in e){if(!(r in t))return!1;if(!equal(e[r],t[r]))return!1}return!0},equal=(e,t)=>{if(null===e&&null===t||void 0===e&&void 0===t)return!0;if(void 0===e&&void 0!==t||void 0!==e&&void 0===t||null===e&&null!==t||null!==e&&null===t)return!1;if(e.constructor!==t.constructor)return!1;if("function"==typeof e&&e.toString()!==t.toString())return!1;if([String,Number,Boolean].includes(e.constructor))return e===t;if(e.constructor===Array){if(e.length!==t.length)return!1;for(let r=0;r<e.length;r++)if(!equal(e[r],t[r]))return!1;return!0}return checkProperties(e,t)},selectorRegex=/^([a-z][a-z0-9:_=-]*)?(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i;let $onrenderCallbacks=[];class VNode{constructor(...e){if(this.type=void 0,this.attributes={},this.data={},this.id=void 0,this.$html=void 0,this.$onrender=void 0,this.style=void 0,this.value=void 0,this.children=[],this.classList=[],this.eventListeners={},0===e.length)throw new Error("[VNode] No arguments passed to VNode constructor.");if(1===e.length){let t=e[0];if("string"==typeof t)this.processSelector(t);else{if("function"!=typeof t&&("object"!=typeof t||null===t))throw new Error("[VNode] Invalid first argument passed to VNode constructor.");"#text"===t.type?(this.type="#text",this.value=t.value):this.from(this.processVNodeObject(t))}}else if(2===e.length){let[t,r]=e;if("string"!=typeof t)throw new Error("[VNode] Invalid first argument passed to VNode constructor.");if(this.processSelector(t),"string"==typeof r)return void(this.children=[new VNode({type:"#text",value:r})]);if("function"!=typeof r&&("object"!=typeof r||null===r))throw new Error("[VNode] The second argument of a VNode constructor must be an object, an array or a string.");Array.isArray(r)||r instanceof Function||r instanceof VNode?this.processChildren(r):this.processProperties(r)}else{if(3!==e.length)throw new Error("[VNode] Too many arguments passed to VNode constructor.");{let[t,r,s]=e;if("string"!=typeof t)throw new Error("[VNode] Invalid first argument passed to VNode constructor.");if(this.processSelector(t),"object"!=typeof r||null===r)throw new Error("[VNode] Invalid second argument passed to VNode constructor.");this.processProperties(r),this.processChildren(s)}}}from(e){this.value=e.value,this.type=e.type,this.id=e.id,this.$html=e.$html,this.$onrender=e.$onrender,this.style=e.style,this.data=e.data,this.value=e.value,this.eventListeners=e.eventListeners,this.children=e.children,this.attributes=e.attributes,this.classList=e.classList}equal(e,t){return equal(e,void 0===t?this:t)}processProperties(e){this.id=this.id||e.id,this.$html=e.$html,this.$onrender=e.$onrender,this.style=e.style,this.value=e.value,this.data=e.data||{},this.classList=e.classList&&e.classList.length>0?e.classList:this.classList,this.attributes=e,Object.keys(e).filter(t=>t.startsWith("on")&&e[t]).forEach(t=>{if("function"!=typeof e[t])throw new Error(`[VNode] Event handler specified for ${t} event is not a function.`);this.eventListeners[t.slice(2)]=e[t],delete this.attributes[t]}),delete this.attributes.value,delete this.attributes.$html,delete this.attributes.$onrender,delete this.attributes.id,delete this.attributes.data,delete this.attributes.style,delete this.attributes.classList}processSelector(e){if(!e.match(selectorRegex))throw new Error("[VNode] Invalid selector: "+e);const[,t,r,s]=e.match(selectorRegex);this.type=t,r&&(this.id=r.slice(1)),this.classList=s&&s.split(".").slice(1)||[]}processVNodeObject(e){if(e instanceof VNode)return e;if(e instanceof Function){let t=e();if("string"==typeof t&&(t=new VNode({type:"#text",value:t})),!(t instanceof VNode))throw new Error("[VNode] Function argument does not return a VNode");return t}throw new Error("[VNode] Invalid first argument provided to VNode constructor.")}processChildren(e){const t=Array.isArray(e)?e:[e];this.children=t.map(e=>{if("string"==typeof e)return new VNode({type:"#text",value:e});if("function"==typeof e||"object"==typeof e&&null!==e)return this.processVNodeObject(e);if(e)throw new Error("[VNode] Specified child is not a VNode: "+e)}).filter(e=>e)}render(){if("#text"===this.type)return document.createTextNode(this.value);const e=document.createElement(this.type);return this.id&&(e.id=this.id),Object.keys(this.attributes).forEach(t=>{if(this.attributes[t]&&"string"==typeof this.attributes[t]){const r=document.createAttribute(t);r.value=this.attributes[t],e.setAttributeNode(r)}"string"==typeof this.attributes[t]&&e[t]||(e[t]=this.attributes[t])}),Object.keys(this.eventListeners).forEach(t=>{e.addEventListener(t,this.eventListeners[t])}),this.value&&(e.value=this.value),this.style&&(e.style.cssText=this.style),this.classList.forEach(t=>{e.classList.add(t)}),Object.keys(this.data).forEach(t=>{e.dataset[t]=this.data[t]}),this.children.forEach(t=>{const r=t.render();e.appendChild(r),t.$onrender&&$onrenderCallbacks.push(()=>t.$onrender(r))}),this.$html&&(e.innerHTML=this.$html),e}redraw(e){let{node:t,vnode:r}=e;const s=r,i=this;if(i.constructor!==s.constructor||i.type!==s.type||i.type===s.type&&"#text"===i.type&&i!==s){const e=s.render();return t.parentNode.replaceChild(e,t),s.$onrender&&s.$onrender(e),void i.from(s)}function n(e,t){let r=[],s=0,i=0;for(let n=0;n<e.children.length;n++){let o=-1;for(let a=0;a<t.children.length;a++)if(equal(e.children[n],t.children[a])&&!r.includes(a)){o=a,i++,s++;break}r.push(o)}if(i===s&&t.children.length===e.children.length)return r;if(i===t.children.length)for(let e=0;e<r.length;e++)-1===r[e]&&(r[e]=-3);if(s===e.children.length)for(let e=0;e<t.children.length;e++)r.includes(e)||r.splice(e,0,-2);if(t.children.length<e.children.length)for(let e=0;e<r.length;e++)-1!==r[e]||t.children[e]||(r[e]=-3);return r}i.id!==s.id&&(t.id=s.id||"",i.id=s.id),i.value!==s.value&&(t.value=s.value||"",i.value=s.value),equal(i.classList,s.classList)||(i.classList.forEach(e=>{s.classList.includes(e)||t.classList.remove(e)}),s.classList.forEach(e=>{i.classList.includes(e)||t.classList.add(e)}),i.classList=s.classList),i.style!==s.style&&(t.style.cssText=s.style||"",i.style=s.style),equal(i.data,s.data)||(Object.keys(i.data).forEach(e=>{s.data[e]?s.data[e]!==i.data[e]&&(t.dataset[e]=s.data[e]):delete t.dataset[e]}),Object.keys(s.data).forEach(e=>{i.data[e]||(t.dataset[e]=s.data[e])}),i.data=s.data),equal(i.attributes,s.attributes)||(Object.keys(i.attributes).forEach(e=>{!1===s.attributes[e]&&(t[e]=!1),s.attributes[e]?s.attributes[e]&&s.attributes[e]!==i.attributes[e]&&t.setAttribute(e,s.attributes[e]):t.removeAttribute(e)}),Object.keys(s.attributes).forEach(e=>{!i.attributes[e]&&s.attributes[e]&&t.setAttribute(e,s.attributes[e])}),i.attributes=s.attributes),equal(i.eventListeners,s.eventListeners)||(Object.keys(i.eventListeners).forEach(e=>{s.eventListeners[e]?equal(s.eventListeners[e],i.eventListeners[e])||(t.removeEventListener(e,i.eventListeners[e]),t.addEventListener(e,s.eventListeners[e])):t.removeEventListener(e,i.eventListeners[e])}),Object.keys(s.eventListeners).forEach(e=>{i.eventListeners[e]||t.addEventListener(e,s.eventListeners[e])}),i.eventListeners=s.eventListeners);let o=n(i,s),a=[...Array(o.filter(e=>-3!==e).length).keys()];for(;!equal(o,a);){let e=-1;for(let r of o){e++;let n=!1;if(r!==e){switch(r){case-1:i.children[e].redraw({node:t.childNodes[e],vnode:s.children[e]});break;case-2:i.children.splice(e,0,s.children[e]);const o=s.children[e].render();t.insertBefore(o,t.childNodes[e]),s.children[e].$onrender&&s.children[e].$onrender(o),n=!0;break;case-3:i.children.splice(e,1),t.removeChild(t.childNodes[e]),n=!0;break;default:const a=i.children.splice(r,1)[0];i.children.splice(e,0,a),t.insertBefore(t.childNodes[r],t.childNodes[e]),n=!0}if(n)break}}o=n(i,s),a=[...Array(o.length).keys()]}equal(i.$onrender,s.$onrender)||(i.$onrender=s.$onrender),i.$html!==s.$html&&(t.innerHTML=s.$html,i.$html=s.$html,i.$onrender&&i.$onrender(t))}}class Store{constructor(){this.events={},this.state={}}dispatch(e,t){if("$log"!==e&&this.dispatch("$log",{event:e,data:t}),this.events[e]){this.events[e].forEach(e=>{this.state={...this.state,...e(this.state,t)}})}}on(e,t){return(this.events[e]||(this.events[e]=[])).push(t),()=>{this.events[e]=this.events[e].filter(e=>e!==t)}}}class Route{constructor({path:e,def:t,query:r,parts:s}){if(this.path=e,this.def=t,this.query=r,this.parts=s,this.params={},this.query){this.query.split("&").forEach(e=>{const[t,r]=e.split("=");this.params[decodeURIComponent(t)]=decodeURIComponent(r)})}}}class Router{constructor({element:e,routes:t,store:r,location:s}){if(this.element=e,this.redraw=null,this.store=r,this.location=s||window.location,!t||0===Object.keys(t).length)throw new Error("[Router] No routes defined.");Object.keys(t);this.routes=t}setRedraw(e,t){this.redraw=()=>{e.redraw({node:this.element.childNodes[0],vnode:this.routes[this.route.def](t)}),this.store.dispatch("$redraw")}}async start(){const e=async e=>{const t=this.route,r=e&&e.newURL&&e.newURL.match(/(#.+)$/)&&e.newURL.match(/(#.+)$/)[1]||this.location.hash,s=r.replace(/\?.+$/,"").slice(1),i=r.match(/\?(.+)$/),n=i&&i[1]?i[1]:"",o=s.split("/").slice(1);let a={};for(let e of Object.keys(this.routes)){let t=e.split("/").slice(1),r=!0,i=0;for(a={};r&&t[i];){const e=t[i],s=o[i];e.startsWith(":")&&s?a[e.slice(1)]=s:r=e===s,i++}if(r){this.route=new Route({query:n,path:s,def:e,parts:a});break}}if(!this.route)throw new Error(`[Router] No route matches '${r}'`);if(t){const e=this.routes[t.def];e.state=e.teardown&&await e.teardown(e.state)}const l=this.routes[this.route.def];for(l.state={},l.setup&&await l.setup(l.state),redrawing=!0,this.store.dispatch("$navigation",this.route);this.element.firstChild;)this.element.removeChild(this.element.firstChild);const h=l(l.state),d=h.render();this.element.appendChild(d),this.setRedraw(h,l.state),redrawing=!1,h.$onrender&&h.$onrender(d),$onrenderCallbacks.forEach(e=>e()),$onrenderCallbacks=[],window.scrollTo(0,0),this.store.dispatch("$redraw")};window.addEventListener("hashchange",e),await e()}navigateTo(e,t){let r=Object.keys(t||{}).map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(t[e])}`).join("&");r=r?"?"+r:"",this.location.hash=`#${e}${r}`}}const h3=(...e)=>new VNode(...e);let store=null,router=null,redrawing=!1;h3.init=e=>{let{element:t,routes:r,modules:s,preStart:i,postStart:n,location:o}=e;if(!r){if("function"!=typeof e)throw new Error("[h3.init] The specified argument is not a valid configuration object or component function");r={"/":e}}if(t=t||document.body,!(t&&t instanceof Element))throw new Error("[h3.init] Invalid element specified.");return store=new Store,(s||[]).forEach(e=>{e(store)}),store.dispatch("$init"),router=new Router({element:t,routes:r,store:store,location:o}),Promise.resolve(i&&i()).then(()=>router.start()).then(()=>n&&n())},h3.navigateTo=(e,t)=>{if(!router)throw new Error("[h3.navigateTo] No application initialized, unable to navigate.");return router.navigateTo(e,t)},Object.defineProperty(h3,"route",{get:()=>{if(!router)throw new Error("[h3.route] No application initialized, unable to retrieve current route.");return router.route}}),Object.defineProperty(h3,"state",{get:()=>{if(!store)throw new Error("[h3.state] No application initialized, unable to retrieve current state.");return store.state}}),h3.on=(e,t)=>{if(!store)throw new Error("[h3.on] No application initialized, unable to listen to events.");return store.on(e,t)},h3.dispatch=(e,t)=>{if(!store)throw new Error("[h3.dispatch] No application initialized, unable to dispatch events.");return store.dispatch(e,t)},h3.redraw=e=>{if(!router||!router.redraw)throw new Error("[h3.redraw] No application initialized, unable to redraw.");redrawing||(redrawing=!0,router.redraw(),redrawing=e||!1)};export default h3; +const checkProperties=(e,t)=>{for(const r in e){if(!(r in t))return!1;if(!equal(e[r],t[r]))return!1}return!0},equal=(e,t)=>{if(null===e&&null===t||void 0===e&&void 0===t)return!0;if(void 0===e&&void 0!==t||void 0!==e&&void 0===t||null===e&&null!==t||null!==e&&null===t)return!1;if(e.constructor!==t.constructor)return!1;if("function"==typeof e&&e.toString()!==t.toString())return!1;if([String,Number,Boolean].includes(e.constructor))return e===t;if(e.constructor===Array){if(e.length!==t.length)return!1;for(let r=0;r<e.length;r++)if(!equal(e[r],t[r]))return!1;return!0}return checkProperties(e,t)},selectorRegex=/^([a-z][a-z0-9:_=-]*)?(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i,[PATCH,INSERT,DELETE]=[-1,-2,-3];let $onrenderCallbacks=[];class VNode{constructor(...e){if(this.type=void 0,this.props={},this.data={},this.id=void 0,this.$html=void 0,this.$onrender=void 0,this.style=void 0,this.value=void 0,this.children=[],this.classList=[],this.eventListeners={},0===e.length)throw new Error("[VNode] No arguments passed to VNode constructor.");if(1===e.length){let t=e[0];if("string"==typeof t)this.processSelector(t);else{if("function"!=typeof t&&("object"!=typeof t||null===t))throw new Error("[VNode] Invalid first argument passed to VNode constructor.");"#text"===t.type?(this.type="#text",this.value=t.value):this.from(this.processVNodeObject(t))}}else if(2===e.length){let[t,r]=e;if("string"!=typeof t)throw new Error("[VNode] Invalid first argument passed to VNode constructor.");if(this.processSelector(t),"string"==typeof r)return void(this.children=[new VNode({type:"#text",value:r})]);if("function"!=typeof r&&("object"!=typeof r||null===r))throw new Error("[VNode] The second argument of a VNode constructor must be an object, an array or a string.");Array.isArray(r)||r instanceof Function||r instanceof VNode?this.processChildren(r):this.processProperties(r)}else{let[t,r,s]=e;if(e.length>3&&(s=e.slice(2)),s=Array.isArray(s)?s:[s],"string"!=typeof t)throw new Error("[VNode] Invalid first argument passed to VNode constructor.");if(this.processSelector(t),r instanceof Function||r instanceof VNode||"string"==typeof r)s=[r].concat(s);else{if("object"!=typeof r||null===r)throw new Error("[VNode] Invalid second argument passed to VNode constructor.");this.processProperties(r)}this.processChildren(s)}}from(e){this.value=e.value,this.type=e.type,this.id=e.id,this.$html=e.$html,this.$onrender=e.$onrender,this.style=e.style,this.data=e.data,this.value=e.value,this.eventListeners=e.eventListeners,this.children=e.children,this.props=e.props,this.classList=e.classList}equal(e,t){return equal(e,void 0===t?this:t)}processProperties(e){this.id=this.id||e.id,this.$html=e.$html,this.$onrender=e.$onrender,this.style=e.style,this.value=e.value,this.data=e.data||{},this.classList=e.classList&&e.classList.length>0?e.classList:this.classList,this.props=e,Object.keys(e).filter(t=>t.startsWith("on")&&e[t]).forEach(t=>{if("function"!=typeof e[t])throw new Error(`[VNode] Event handler specified for ${t} event is not a function.`);this.eventListeners[t.slice(2)]=e[t],delete this.props[t]}),delete this.props.value,delete this.props.$html,delete this.props.$onrender,delete this.props.id,delete this.props.data,delete this.props.style,delete this.props.classList}processSelector(e){if(!e.match(selectorRegex)||0===e.length)throw new Error("[VNode] Invalid selector: "+e);const[,t,r,s]=e.match(selectorRegex);this.type=t,r&&(this.id=r.slice(1)),this.classList=s&&s.split(".").slice(1)||[]}processVNodeObject(e){if(e instanceof VNode)return e;if(e instanceof Function){let t=e();if("string"==typeof t&&(t=new VNode({type:"#text",value:t})),!(t instanceof VNode))throw new Error("[VNode] Function argument does not return a VNode");return t}throw new Error("[VNode] Invalid first argument provided to VNode constructor.")}processChildren(e){const t=Array.isArray(e)?e:[e];this.children=t.map(e=>{if("string"==typeof e)return new VNode({type:"#text",value:e});if("function"==typeof e||"object"==typeof e&&null!==e)return this.processVNodeObject(e);if(e)throw new Error("[VNode] Specified child is not a VNode: "+e)}).filter(e=>e)}render(){if("#text"===this.type)return document.createTextNode(this.value);const e=document.createElement(this.type);return this.id&&(e.id=this.id),Object.keys(this.props).forEach(t=>{"boolean"==typeof this.props[t]&&(this.props[t]?e.setAttribute(t,""):e.removeAttribute(t)),["string","number"].includes(typeof this.props[t])&&e.setAttribute(t,this.props[t]),e[t]=this.props[t]}),Object.keys(this.eventListeners).forEach(t=>{e.addEventListener(t,this.eventListeners[t])}),this.value&&(["textarea","input"].includes(this.type)?e.value=this.value:e.setAttribute("value",this.value)),this.style&&(e.style.cssText=this.style),this.classList.forEach(t=>{e.classList.add(t)}),Object.keys(this.data).forEach(t=>{e.dataset[t]=this.data[t]}),this.children.forEach(t=>{const r=t.render();e.appendChild(r),t.$onrender&&$onrenderCallbacks.push(()=>t.$onrender(r))}),this.$html&&(e.innerHTML=this.$html),e}redraw(e){let{node:t,vnode:r}=e;const s=r,o=this;if(o.constructor!==s.constructor||o.type!==s.type||o.type===s.type&&"#text"===o.type&&o!==s){const e=s.render();return t.parentNode.replaceChild(e,t),s.$onrender&&s.$onrender(e),void o.from(s)}o.id!==s.id&&(t.id=s.id||"",o.id=s.id),o.value!==s.value&&(o.value=s.value,["textarea","input"].includes(o.type)?t.value=s.value||"":t.setAttribute("value",s.value||"")),equal(o.classList,s.classList)||(o.classList.forEach(e=>{s.classList.includes(e)||t.classList.remove(e)}),s.classList.forEach(e=>{o.classList.includes(e)||t.classList.add(e)}),o.classList=s.classList),o.style!==s.style&&(t.style.cssText=s.style||"",o.style=s.style),equal(o.data,s.data)||(Object.keys(o.data).forEach(e=>{s.data[e]?s.data[e]!==o.data[e]&&(t.dataset[e]=s.data[e]):delete t.dataset[e]}),Object.keys(s.data).forEach(e=>{o.data[e]||(t.dataset[e]=s.data[e])}),o.data=s.data),equal(o.props,s.props)||(Object.keys(o.props).forEach(e=>{t[e]=s.props[e],"boolean"==typeof s.props[e]?(o.props[e]=s.props[e],s.props[e]?t.setAttribute(e,""):t.removeAttribute(e)):s.props[e]?s.props[e]&&s.props[e]!==o.props[e]&&(o.props[e]=s.props[e],["string","number"].includes(typeof s.props[e])&&t.setAttribute(e,s.props[e])):(delete o.props[e],t.removeAttribute(e))}),Object.keys(s.props).forEach(e=>{!o.props[e]&&s.props[e]&&(o.props[e]=s.props[e],t.setAttribute(e,s.props[e]))})),equal(o.eventListeners,s.eventListeners)||(Object.keys(o.eventListeners).forEach(e=>{s.eventListeners[e]?equal(s.eventListeners[e],o.eventListeners[e])||(t.removeEventListener(e,o.eventListeners[e]),t.addEventListener(e,s.eventListeners[e])):t.removeEventListener(e,o.eventListeners[e])}),Object.keys(s.eventListeners).forEach(e=>{o.eventListeners[e]||t.addEventListener(e,s.eventListeners[e])}),o.eventListeners=s.eventListeners);let i=mapChildren(o,s),n=[...Array(s.children.length).keys()];for(;!equal(i,n);){let e=-1;e:for(const r of i)if(e++,r!==e)switch(r){case PATCH:o.children[e].redraw({node:t.childNodes[e],vnode:s.children[e]});break e;case INSERT:{o.children.splice(e,0,s.children[e]);const r=s.children[e].render();t.insertBefore(r,t.childNodes[e]),s.children[e].$onrender&&s.children[e].$onrender(r);break e}case DELETE:o.children.splice(e,1),t.removeChild(t.childNodes[e]);break e;default:{const s=o.children.splice(r,1)[0];o.children.splice(e,0,s);const i=t.removeChild(t.childNodes[r]);t.insertBefore(i,t.childNodes[e]);break e}}i=mapChildren(o,s),n=[...Array(s.children.length).keys()]}equal(o.$onrender,s.$onrender)||(o.$onrender=s.$onrender),o.$html!==s.$html&&(t.innerHTML=s.$html,o.$html=s.$html,o.$onrender&&o.$onrender(t))}}const mapChildren=(e,t)=>{const r=t.children,s=e.children;let o=[];for(let e=0;e<r.length;e++){let t=PATCH;for(let i=0;i<s.length;i++)if(equal(r[e],s[i])&&!o.includes(i)){t=i;break}t<0&&r.length>=s.length&&o.length>=s.length&&(t=INSERT),o.push(t)}const i=o.filter(e=>e>=0);return s.length>r.length?[...Array(s.length-r.length).keys()].forEach(()=>o.push(DELETE)):i.length===s.length&&(o=o.map(e=>e<0?INSERT:e)),o};class Store{constructor(){this.events={},this.state={}}dispatch(e,t){if("$log"!==e&&this.dispatch("$log",{event:e,data:t}),this.events[e]){this.events[e].forEach(e=>{this.state={...this.state,...e(this.state,t)}})}}on(e,t){return(this.events[e]||(this.events[e]=[])).push(t),()=>{this.events[e]=this.events[e].filter(e=>e!==t)}}}class Route{constructor({path:e,def:t,query:r,parts:s}){if(this.path=e,this.def=t,this.query=r,this.parts=s,this.params={},this.query){this.query.split("&").forEach(e=>{const[t,r]=e.split("=");this.params[decodeURIComponent(t)]=decodeURIComponent(r)})}}}class Router{constructor({element:e,routes:t,store:r,location:s}){if(this.element=e,this.redraw=null,this.store=r,this.location=s||window.location,!t||0===Object.keys(t).length)throw new Error("[Router] No routes defined.");Object.keys(t);this.routes=t}setRedraw(e,t){this.redraw=()=>{e.redraw({node:this.element.childNodes[0],vnode:this.routes[this.route.def](t)}),this.store.dispatch("$redraw")}}async start(){const e=async e=>{const t=this.route,r=e&&e.newURL&&e.newURL.match(/(#.+)$/)&&e.newURL.match(/(#.+)$/)[1]||this.location.hash,s=r.replace(/\?.+$/,"").slice(1),o=r.match(/\?(.+)$/),i=o&&o[1]?o[1]:"",n=s.split("/").slice(1);let a={};for(let e of Object.keys(this.routes)){let t=e.split("/").slice(1),r=!0,o=0;for(a={};r&&t[o];){const e=t[o],s=n[o];e.startsWith(":")&&s?a[e.slice(1)]=s:r=e===s,o++}if(r){this.route=new Route({query:i,path:s,def:e,parts:a});break}}if(!this.route)throw new Error(`[Router] No route matches '${r}'`);let l={};if(t){const e=this.routes[t.def];l=e.teardown&&await e.teardown(e.state)||l}const h=this.routes[this.route.def];for(h.state=l,h.setup&&await h.setup(h.state),redrawing=!0,this.store.dispatch("$navigation",this.route);this.element.firstChild;)this.element.removeChild(this.element.firstChild);const d=h(h.state),c=d.render();this.element.appendChild(c),this.setRedraw(d,h.state),redrawing=!1,d.$onrender&&d.$onrender(c),$onrenderCallbacks.forEach(e=>e()),$onrenderCallbacks=[],window.scrollTo(0,0),this.store.dispatch("$redraw")};window.addEventListener("hashchange",e),await e()}navigateTo(e,t){let r=Object.keys(t||{}).map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(t[e])}`).join("&");r=r?"?"+r:"",this.location.hash=`#${e}${r}`}}export const h=(...e)=>new VNode(...e);export const h3={};let store=null,router=null,redrawing=!1;h3.init=e=>{let{element:t,routes:r,modules:s,preStart:o,postStart:i,location:n}=e;if(!r){if("function"!=typeof e)throw new Error("[h3.init] The specified argument is not a valid configuration object or component function");r={"/":e}}if(t=t||document.body,!(t&&t instanceof Element))throw new Error("[h3.init] Invalid element specified.");return store=new Store,(s||[]).forEach(e=>{e(store)}),store.dispatch("$init"),router=new Router({element:t,routes:r,store:store,location:n}),Promise.resolve(o&&o()).then(()=>router.start()).then(()=>i&&i())},h3.navigateTo=(e,t)=>{if(!router)throw new Error("[h3.navigateTo] No application initialized, unable to navigate.");return router.navigateTo(e,t)},Object.defineProperty(h3,"route",{get:()=>{if(!router)throw new Error("[h3.route] No application initialized, unable to retrieve current route.");return router.route}}),Object.defineProperty(h3,"state",{get:()=>{if(!store)throw new Error("[h3.state] No application initialized, unable to retrieve current state.");return store.state}}),h3.on=(e,t)=>{if(!store)throw new Error("[h3.on] No application initialized, unable to listen to events.");return store.on(e,t)},h3.dispatch=(e,t)=>{if(!store)throw new Error("[h3.dispatch] No application initialized, unable to dispatch events.");return store.dispatch(e,t)},h3.redraw=e=>{if(!router||!router.redraw)throw new Error("[h3.redraw] No application initialized, unable to redraw.");redrawing||(redrawing=!0,router.redraw(),redrawing=e||!1)},h3.screen=({setup:e,display:t,teardown:r})=>{if(!t||"function"!=typeof t)throw new Error("[h3.screen] No display property specified.");if(e&&"function"!=typeof e)throw new Error("[h3.screen] setup property is not a function.");if(r&&"function"!=typeof r)throw new Error("[h3.screen] teardown property is not a function.");const s=t;return e&&(s.setup=e),r&&(s.teardown=r),s};export default h3; //# sourceMappingURL=h3.js.map
@@ -1,6 +1,6 @@
{ "name": "@h3rald/h3", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 1, "requires": true, "dependencies": {
@@ -1,17 +1,17 @@
{ "name": "@h3rald/h3", - "version": "0.9.0", - "versionName": "Impeccable Iconian", + "version": "0.10.0", + "versionName": "Jittery Jem'Hadar", "description": "A tiny, extremely minimalist JavaScript microframework.", "main": "h3.js", "scripts": { "test": "jest", "coverage": "jest --coverage ", "coveralls": "npm run coverage && cat ./coverage/lcov.info | coveralls", + "prebuild": "node scripts/release.js", "copy": "cp h3.js docs/js/h3.js && cp h3.js docs/example/assets/js/h3.js", - "release": "node scripts/release.js", "guide": "hastyscribe docs/H3_DeveloperGuide.md", - "regenerate": "npm run release && npm run copy && npm run guide" + "build": "npm run copy && npm run guide" }, "repository": { "type": "git",