Updated child mapping algorithm; handling boolean and value attribute properly, added h function.
h3rald h3rald@h3rald.com
Mon, 27 Jul 2020 15:22:29 +0200
21 files changed,
3265 insertions(+),
3194 deletions(-)
jump to
M
__tests__/h3.js
→
__tests__/h3.js
@@ -1,420 +1,428 @@
-const h3 = require("../h3.js").default; +const mod = require("../h3.js"); +const h3 = mod.h3; +const h = mod.h; describe("h3", () => { - beforeEach(() => { - jest - .spyOn(window, "requestAnimationFrame") - .mockImplementation((cb) => cb()); - }); + beforeEach(() => { + jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => + cb() + ); + }); - afterEach(() => { - window.requestAnimationFrame.mockRestore(); - }); + afterEach(() => { + 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"); - expect(v1.equal(v2)).toEqual(true); - expect(v1.equal(v3)).toEqual(false); - expect(v4.equal({ type: "div" })).toEqual(false); - expect(v1.equal(null, null)).toEqual(true); - expect(v1.equal(null, undefined)).toEqual(false); - }); - - it("should support the creation of empty virtual node elements", () => { - expect(h3("div")).toEqual({ - type: "div", - children: [], - props: {}, - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: undefined, + it("should support a way to discriminate functions and objects", () => { + 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); + expect(v1.equal(null, null)).toEqual(true); + expect(v1.equal(null, undefined)).toEqual(false); }); - }); - 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 emptySelector = () => h3(""); - expect(empty).toThrowError(/No arguments passed/); - expect(invalid1st).toThrowError(/Invalid first argument/); - expect(invalid1st2).toThrowError(/Invalid first argument/); - expect(invalid1st3).toThrowError(/Invalid first argument/); - expect(invalid1st1).toThrowError(/does not return a VNode/); - expect(invalid1st1b).toThrowError(/Invalid first argument/); - 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(emptySelector).toThrowError(/Invalid selector/); - }); + it("should support the creation of empty virtual node elements", () => { + expect(h("div")).toEqual({ + type: "div", + children: [], + props: {}, + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: undefined, + }); + }); - it("should support several child arguments", () => { - let vnode = h3("div", {test: "a"}, "a", "b", "c"); - expect(vnode.children.length).toEqual(3); - vnode = h3("div", "a", "b", "c"); - expect(vnode.children.length).toEqual(3); - vnode = h3("div", "a", "b"); - expect(vnode.children.length).toEqual(2); - }); + it("should throw an error when invalid arguments are supplied", () => { + 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/); + expect(invalid1st3).toThrowError(/Invalid first argument/); + expect(invalid1st1).toThrowError(/does not return a VNode/); + expect(invalid1st1b).toThrowError(/Invalid first argument/); + 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(emptySelector).toThrowError(/Invalid selector/); + }); - it("should support the creation of elements with a single, non-array child", () => { - const vnode1 = h3("div", () => "test"); - const vnode2 = h3("div", () => h3("span")); - expect(vnode1.children[0].value).toEqual("test"); - expect(vnode2.children[0].type).toEqual("span"); - }); + 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 remove null/false/undefined children", () => { - const v1 = h3("div", [false, "test", undefined, null, ""]); - expect(v1.children).toEqual([ - h3({ type: "#text", value: "test" }), - h3({ type: "#text", value: "" }), - ]); - }); + it("should support the creation of elements with a single, non-array child", () => { + 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 support the creation of nodes with a single child node", () => { - const result = { - type: "div", - children: [ - { - type: "#text", - children: [], - props: {}, - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: "test", - }, - ], - props: {}, - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: undefined, - }; - expect(h3("div", "test")).toEqual(result); - const failing = () => h3("***"); - expect(failing).toThrowError(/Invalid selector/); - }); + it("should remove null/false/undefined children", () => { + const v1 = h("div", [false, "test", undefined, null, ""]); + expect(v1.children).toEqual([ + h({ type: "#text", value: "test" }), + h({ type: "#text", value: "" }), + ]); + }); - 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"] }); - expect(a).toEqual({ - type: "div", - children: [], - props: {}, - classList: ["a", "b", "c"], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - type: "div", - value: undefined, + it("should support the creation of nodes with a single child node", () => { + const result = { + type: "div", + children: [ + { + type: "#text", + children: [], + props: {}, + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: "test", + }, + ], + props: {}, + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: undefined, + }; + expect(h("div", "test")).toEqual(result); + const failing = () => h("***"); + expect(failing).toThrowError(/Invalid selector/); }); - expect(a).toEqual(b); - }); - it("should support the creation of virtual node elements with props and classes", () => { - expect(h3("div.test1.test2", { id: "test" })).toEqual({ - type: "div", - children: [], - classList: ["test1", "test2"], - data: {}, - props: {}, - eventListeners: {}, - id: "test", - $html: undefined, - style: undefined, - type: "div", - value: undefined, + it("should support the creation of virtual node elements with classes", () => { + const a = h("div.a.b.c"); + const b = h("div", { classList: ["a", "b", "c"] }); + expect(a).toEqual({ + type: "div", + children: [], + props: {}, + classList: ["a", "b", "c"], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + type: "div", + value: undefined, + }); + expect(a).toEqual(b); }); - }); - it("should support the creation of virtual node elements with text children and classes", () => { - expect(h3("div.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", - }, - ], - props: {}, - classList: ["test"], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: undefined, + 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: {}, + props: {}, + eventListeners: {}, + id: "test", + $html: undefined, + style: undefined, + type: "div", + value: undefined, + }); }); - }); - it("should support the creation of virtual node elements with text children, props, and classes", () => { - expect( - h3("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 text children and classes", () => { + expect(h("div.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", + }, + ], + props: {}, + classList: ["test"], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: undefined, + }); }); - }); - it("should support the creation of virtual node elements with props", () => { - expect(h3("input", { type: "text", value: "AAA" })).toEqual({ - type: "input", - children: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: "AAA", - props: { type: "text" }, - classList: [], + 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 event handlers", () => { - const fn = () => true; - expect(h3("button", { onclick: fn })).toEqual({ - type: "button", - children: [], - data: {}, - eventListeners: { - click: fn, - }, - id: undefined, - $html: undefined, - style: undefined, - value: undefined, - props: {}, - classList: [], + it("should support the creation of virtual node elements with props", () => { + expect(h("input", { type: "text", value: "AAA" })).toEqual({ + type: "input", + children: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: "AAA", + props: { type: "text" }, + classList: [], + }); }); - expect(() => h3("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"])]) - ).toEqual({ - props: {}, - type: "div", - children: [ - { - props: {}, - children: [], - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - type: "#text", - value: "a", - }, - { - type: "span", - children: [ - { - props: {}, - children: [], - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - type: "#text", - value: "test1", - }, - ], - props: {}, - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: undefined, - }, - { - type: "span", - children: [ - { - props: {}, - children: [], - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - type: "#text", - value: "test2", + it("should support the creation of virtual node elements with event handlers", () => { + const fn = () => true; + expect(h("button", { onclick: fn })).toEqual({ + type: "button", + children: [], + data: {}, + eventListeners: { + click: fn, }, - ], - props: {}, - classList: [], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: undefined, - }, - ], - classList: ["test"], - data: {}, - eventListeners: {}, - id: undefined, - $html: undefined, - style: undefined, - value: undefined, + id: undefined, + $html: undefined, + style: undefined, + value: undefined, + props: {}, + classList: [], + }); + expect(() => h("span", { onclick: "something" })).toThrowError( + /onclick event is not a function/ + ); }); - }); - it("should not allow certain methods and properties to be called/accessed before initialization", () => { - const route = () => h3.route; - const state = () => h3.state; - const redraw = () => h3.redraw(); - const dispatch = () => h3.dispatch(); - const on = () => h3.on(); - const navigateTo = () => h3.navigateTo(); - expect(route).toThrowError(/No application initialized/); - expect(state).toThrowError(/No application initialized/); - expect(redraw).toThrowError(/No application initialized/); - expect(dispatch).toThrowError(/No application initialized/); - expect(on).toThrowError(/No application initialized/); - expect(navigateTo).toThrowError(/No application initialized/); - }); + it("should support the creation of virtual node elements with element children and classes", () => { + expect( + h("div.test", [ + "a", + h("span", ["test1"]), + () => h("span", ["test2"]), + ]) + ).toEqual({ + props: {}, + type: "div", + children: [ + { + props: {}, + children: [], + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + type: "#text", + value: "a", + }, + { + type: "span", + children: [ + { + props: {}, + children: [], + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + type: "#text", + value: "test1", + }, + ], + props: {}, + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: undefined, + }, + { + type: "span", + children: [ + { + props: {}, + children: [], + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + type: "#text", + value: "test2", + }, + ], + props: {}, + classList: [], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: undefined, + }, + ], + classList: ["test"], + data: {}, + eventListeners: {}, + id: undefined, + $html: undefined, + style: undefined, + value: undefined, + }); + }); - it("should provide an init method to initialize a SPA with a single component", async () => { - const c = () => h3("div", "Hello, World!"); - const body = document.body; - const appendChild = jest.spyOn(body, "appendChild"); - await h3.init(c); - expect(appendChild).toHaveBeenCalled(); - expect(body.childNodes[0].childNodes[0].data).toEqual("Hello, World!"); - }); + it("should not allow certain methods and properties to be called/accessed before initialization", () => { + const route = () => h3.route; + const state = () => h3.state; + const redraw = () => h3.redraw(); + const dispatch = () => h3.dispatch(); + const on = () => h3.on(); + const navigateTo = () => h3.navigateTo(); + expect(route).toThrowError(/No application initialized/); + expect(state).toThrowError(/No application initialized/); + expect(redraw).toThrowError(/No application initialized/); + expect(dispatch).toThrowError(/No application initialized/); + expect(on).toThrowError(/No application initialized/); + expect(navigateTo).toThrowError(/No application initialized/); + }); - it("should provide some validation at initialization time", async () => { - try { - await h3.init({ element: "INVALID", routes: {} }); - } catch (e) { - expect(e.message).toMatch(/Invalid element/); - } - try { - await h3.init({ element: document.body }); - } catch (e) { - expect(e.message).toMatch(/not a valid configuration object/); - } - try { - await h3.init({ element: document.body, routes: {} }); - } catch (e) { - expect(e.message).toMatch(/No routes/); - } - }); + it("should provide an init method to initialize a SPA with a single component", async () => { + const c = () => h("div", "Hello, World!"); + const body = document.body; + const appendChild = jest.spyOn(body, "appendChild"); + await h3.init(c); + expect(appendChild).toHaveBeenCalled(); + expect(body.childNodes[0].childNodes[0].data).toEqual("Hello, World!"); + }); - it("should expose a redraw method", async () => { - const vnode = h3("div"); - await h3.init(() => vnode); - jest.spyOn(vnode, "redraw"); - h3.redraw(); - expect(vnode.redraw).toHaveBeenCalled(); - h3.redraw(true); - h3.redraw(); - h3.redraw(); - expect(vnode.redraw).toHaveBeenCalledTimes(2); - }); + it("should provide some validation at initialization time", async () => { + try { + await h3.init({ element: "INVALID", routes: {} }); + } catch (e) { + expect(e.message).toMatch(/Invalid element/); + } + try { + await h3.init({ element: document.body }); + } catch (e) { + expect(e.message).toMatch(/not a valid configuration object/); + } + try { + await h3.init({ element: document.body, routes: {} }); + } catch (e) { + expect(e.message).toMatch(/No routes/); + } + }); - it("should not redraw while a other redraw is in progress", async () => { - const vnode = h3("div"); - await h3.init({ - routes: { - "/": () => vnode, - }, + it("should expose a redraw method", async () => { + const vnode = h("div"); + await h3.init(() => vnode); + jest.spyOn(vnode, "redraw"); + h3.redraw(); + expect(vnode.redraw).toHaveBeenCalled(); + h3.redraw(true); + h3.redraw(); + h3.redraw(); + expect(vnode.redraw).toHaveBeenCalledTimes(2); }); - jest.spyOn(vnode, "redraw"); - h3.redraw(true); - h3.redraw(); - expect(vnode.redraw).toHaveBeenCalledTimes(1); - }); + + it("should not redraw while a other redraw is in progress", async () => { + const vnode = h("div"); + await h3.init({ + routes: { + "/": () => vnode, + }, + }); + jest.spyOn(vnode, "redraw"); + h3.redraw(true); + h3.redraw(); + expect(vnode.redraw).toHaveBeenCalledTimes(1); + }); });
M
__tests__/router.js
→
__tests__/router.js
@@ -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"); - }); });
M
__tests__/store.js
→
__tests__/store.js
@@ -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"); + }); });
M
__tests__/vnode.js
→
__tests__/vnode.js
@@ -1,370 +1,393 @@
-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: [], - props: { 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 props 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 props 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 props as properties and not create props", () => { - 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(""); + 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", () => { + const vnode = h("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 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 props", () => { - 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 props", () => { - 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, props, $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"); + }); });
M
docs/H3_DeveloperGuide.htm
→
docs/H3_DeveloperGuide.htm
@@ -8058,7 +8058,7 @@ </ul>
</div> <div id="footer"> - <p><span class="copy"></span> Fabio Cevasco – July 12, 2020</p> + <p><span class="copy"></span> Fabio Cevasco – July 27, 2020</p> <p><span>Powered by</span> <a href="https://h3rald.com/hastyscribe"><span class="hastyscribe"></span></a></p> </div> </div>
M
docs/example/assets/js/app.js
→
docs/example/assets/js/app.js
@@ -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, + }, });
M
docs/example/assets/js/components/AddTodoForm.js
→
docs/example/assets/js/components/AddTodoForm.js
@@ -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, + }, + "+" + ), + ]); }
M
docs/example/assets/js/components/EmptyTodoError.js
→
docs/example/assets/js/components/EmptyTodoError.js
@@ -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, + }, + "✘" + ), + ]); }
M
docs/example/assets/js/components/MainView.js
→
docs/example/assets/js/components/MainView.js
@@ -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]), + ]); }
M
docs/example/assets/js/components/Paginator.js
→
docs/example/assets/js/components/Paginator.js
@@ -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), + }, + ["→"] + ), + ]); }
M
docs/example/assets/js/components/SettingsView.js
→
docs/example/assets/js/components/SettingsView.js
@@ -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" + ), + ]); }
M
docs/example/assets/js/components/Todo.js
→
docs/example/assets/js/components/Todo.js
@@ -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), + }, + "✘" + ), + ]); }
M
docs/example/assets/js/components/TodoList.js
→
docs/example/assets/js/components/TodoList.js
@@ -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)); }
M
docs/example/assets/js/h3.js
→
docs/example/assets/js/h3.js
@@ -6,539 +6,537 @@ * @license MIT
* For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE */ const checkProperties = (obj1, obj2) => { - for (const key in obj1) { - if (!(key in obj2)) { - return false; - } - if (!equal(obj1[key], obj2[key])) { - return false; + for (const key in obj1) { + if (!(key in obj2)) { + return false; + } + if (!equal(obj1[key], obj2[key])) { + return false; + } } - } - return true; + return true; }; const equal = (obj1, obj2) => { - if ( - (obj1 === null && obj2 === null) || - (obj1 === undefined && obj2 === undefined) - ) { - return true; - } - if ( - (obj1 === undefined && obj2 !== undefined) || - (obj1 !== undefined && obj2 === undefined) || - (obj1 === null && obj2 !== null) || - (obj1 !== null && obj2 === null) - ) { - return false; - } - if (obj1.constructor !== obj2.constructor) { - return false; - } - if (typeof obj1 === "function") { - if (obj1.toString() !== obj2.toString()) { - return false; + if ( + (obj1 === null && obj2 === null) || + (obj1 === undefined && obj2 === undefined) + ) { + return true; } - } - if ([String, Number, Boolean].includes(obj1.constructor)) { - return obj1 === obj2; - } - if (obj1.constructor === Array) { - if (obj1.length !== obj2.length) { - return false; + if ( + (obj1 === undefined && obj2 !== undefined) || + (obj1 !== undefined && obj2 === undefined) || + (obj1 === null && obj2 !== null) || + (obj1 !== null && obj2 === null) + ) { + return false; } - for (let i = 0; i < obj1.length; i++) { - if (!equal(obj1[i], obj2[i])) { + if (obj1.constructor !== obj2.constructor) { return false; - } + } + if (typeof obj1 === "function") { + if (obj1.toString() !== obj2.toString()) { + return false; + } + } + if ([String, Number, Boolean].includes(obj1.constructor)) { + return obj1 === obj2; + } + if (obj1.constructor === Array) { + if (obj1.length !== obj2.length) { + return false; + } + for (let i = 0; i < obj1.length; i++) { + if (!equal(obj1[i], obj2[i])) { + return false; + } + } + return true; } - return true; - } - return checkProperties(obj1, obj2); // && checkProperties(obj2, obj1); + 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 = []; -let $onrenderCallbacks = []; +const mapChildren = (oldvnode, newvnode) => { + const newList = newvnode.children; + const oldList = oldvnode.children; + const 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); + } + if (oldList.length > newList.length) { + // Remove remaining nodes + [...Array(oldList.length - newList.length).keys()].forEach(() => + map.push(DELETE) + ); + } + return map; +}; // Virtual Node Implementation with HyperScript-like syntax class VNode { - - constructor(...args) { - this.type = undefined; - this.props = {}; - this.data = {}; - this.id = undefined; - this.$html = undefined; - this.$onrender = undefined; - this.style = undefined; - this.value = undefined; - this.children = []; - this.classList = []; - this.eventListeners = {}; - if (args.length === 0) { - throw new Error("[VNode] No arguments passed to VNode constructor."); - } - if (args.length === 1) { - let vnode = args[0]; - if (typeof vnode === "string") { - // Assume empty element - this.processSelector(vnode); - } else if ( - typeof vnode === "function" || - (typeof vnode === "object" && vnode !== null) - ) { - // Text node - if (vnode.type === "#text") { - this.type = "#text"; - this.value = vnode.value; - } else { - this.from(this.processVNodeObject(vnode)); + constructor(...args) { + this.type = undefined; + this.props = {}; + this.data = {}; + this.id = undefined; + this.$html = undefined; + this.$onrender = undefined; + this.style = undefined; + this.value = undefined; + this.children = []; + this.classList = []; + this.eventListeners = {}; + if (args.length === 0) { + throw new Error( + "[VNode] No arguments passed to VNode constructor." + ); } - } else { - throw new Error( - "[VNode] Invalid first argument passed to VNode constructor." - ); - } - } else if (args.length === 2) { - let [selector, data] = args; - if (typeof selector !== "string") { - throw new Error( - "[VNode] Invalid first argument passed to VNode constructor." - ); - } - this.processSelector(selector); - if (typeof data === "string") { - // Assume single child text node - this.children = [new VNode({ type: "#text", value: data })]; - return; - } - if ( - typeof data !== "function" && - (typeof data !== "object" || data === null) - ) { - throw new Error( - "[VNode] The second argument of a VNode constructor must be an object, an array or a string." - ); - } - if (Array.isArray(data)) { - // Assume 2nd argument as children - this.processChildren(data); - } else { - if (data instanceof Function || data instanceof VNode) { - this.processChildren(data); + if (args.length === 1) { + let vnode = args[0]; + if (typeof vnode === "string") { + // Assume empty element + this.processSelector(vnode); + } else if ( + typeof vnode === "function" || + (typeof vnode === "object" && vnode !== null) + ) { + // Text node + if (vnode.type === "#text") { + this.type = "#text"; + this.value = vnode.value; + } else { + this.from(this.processVNodeObject(vnode)); + } + } else { + throw new Error( + "[VNode] Invalid first argument passed to VNode constructor." + ); + } + } else if (args.length === 2) { + let [selector, data] = args; + if (typeof selector !== "string") { + throw new Error( + "[VNode] Invalid first argument passed to VNode constructor." + ); + } + this.processSelector(selector); + if (typeof data === "string") { + // Assume single child text node + this.children = [new VNode({ type: "#text", value: data })]; + return; + } + if ( + typeof data !== "function" && + (typeof data !== "object" || data === null) + ) { + throw new Error( + "[VNode] The second argument of a VNode constructor must be an object, an array or a string." + ); + } + if (Array.isArray(data)) { + // Assume 2nd argument as children + this.processChildren(data); + } else { + if (data instanceof Function || data instanceof VNode) { + this.processChildren(data); + } else { + // Not a VNode, assume props object + this.processProperties(data); + } + } } else { - // Not a VNode, assume props object - this.processProperties(data); + 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 ( + 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.processChildren(children); } - } - } 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 ( - 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.processChildren(children); } - } - from(data) { - this.value = data.value; - this.type = data.type; - this.id = data.id; - this.$html = data.$html; - this.$onrender = data.$onrender; - this.style = data.style; - this.data = data.data; - this.value = data.value; - this.eventListeners = data.eventListeners; - this.children = data.children; - this.props = data.props; - this.classList = data.classList; - } - - equal(a, b) { - return equal(a, b === undefined ? this : b); - } + from(data) { + this.value = data.value; + this.type = data.type; + this.id = data.id; + this.$html = data.$html; + this.$onrender = data.$onrender; + this.style = data.style; + this.data = data.data; + this.value = data.value; + this.eventListeners = data.eventListeners; + this.children = data.children; + this.props = data.props; + this.classList = data.classList; + } - processProperties(attrs) { - this.id = this.id || attrs.id; - this.$html = attrs.$html; - this.$onrender = attrs.$onrender; - this.style = attrs.style; - this.value = attrs.value; - this.data = attrs.data || {}; - this.classList = - attrs.classList && attrs.classList.length > 0 - ? attrs.classList - : this.classList; - this.props = attrs; - Object.keys(attrs) - .filter((a) => a.startsWith("on") && attrs[a]) - .forEach((key) => { - if (typeof attrs[key] !== "function") { - throw new Error( - `[VNode] Event handler specified for ${key} event is not a function.` - ); - } - this.eventListeners[key.slice(2)] = attrs[key]; - delete this.props[key]; - }); - 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) || selector.length === 0) { - throw new Error(`[VNode] Invalid selector: ${selector}`); + equal(a, b) { + return equal(a, b === undefined ? this : b); } - const [, type, id, classes] = selector.match(selectorRegex); - this.type = type; - if (id) { - this.id = id.slice(1); - } - this.classList = (classes && classes.split(".").slice(1)) || []; - } - processVNodeObject(arg) { - if (arg instanceof VNode) { - return arg; - } - if (arg instanceof Function) { - let vnode = arg(); - if (typeof vnode === "string") { - vnode = new VNode({ type: "#text", value: vnode }); - } - if (!(vnode instanceof VNode)) { - throw new Error("[VNode] Function argument does not return a VNode"); - } - return vnode; + processProperties(attrs) { + this.id = this.id || attrs.id; + this.$html = attrs.$html; + this.$onrender = attrs.$onrender; + this.style = attrs.style; + this.value = attrs.value; + this.data = attrs.data || {}; + this.classList = + attrs.classList && attrs.classList.length > 0 + ? attrs.classList + : this.classList; + this.props = attrs; + Object.keys(attrs) + .filter((a) => a.startsWith("on") && attrs[a]) + .forEach((key) => { + if (typeof attrs[key] !== "function") { + throw new Error( + `[VNode] Event handler specified for ${key} event is not a function.` + ); + } + this.eventListeners[key.slice(2)] = attrs[key]; + delete this.props[key]; + }); + 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; } - throw new Error( - "[VNode] Invalid first argument provided to VNode constructor." - ); - } - processChildren(arg) { - const children = Array.isArray(arg) ? arg : [arg]; - this.children = children - .map((c) => { - if (typeof c === "string") { - return new VNode({ type: "#text", value: c }); - } - if (typeof c === "function" || (typeof c === "object" && c !== null)) { - return this.processVNodeObject(c); + processSelector(selector) { + if (!selector.match(selectorRegex) || selector.length === 0) { + throw new Error(`[VNode] Invalid selector: ${selector}`); } - if (c) { - throw new Error(`[VNode] Specified child is not a VNode: ${c}`); + const [, type, id, classes] = selector.match(selectorRegex); + this.type = type; + if (id) { + this.id = id.slice(1); } - }) - .filter((c) => c); - } - - // Renders the actual DOM Node corresponding to the current Virtual Node - render() { - if (this.type === "#text") { - return document.createTextNode(this.value); + this.classList = (classes && classes.split(".").slice(1)) || []; } - const node = document.createElement(this.type); - if (this.id) { - node.id = this.id; - } - Object.keys(this.props).forEach((attr) => { - // Set props (only if non-empty strings) - if (this.props[attr] && typeof this.props[attr] === "string") { - const a = document.createAttribute(attr); - a.value = this.props[attr]; - node.setAttributeNode(a); - } - // Set properties - if (typeof this.props[attr] !== "string" || !node[attr]) { - node[attr] = this.props[attr]; - } - }); - // Event Listeners - Object.keys(this.eventListeners).forEach((event) => { - node.addEventListener(event, this.eventListeners[event]); - }); - // Value - if (this.value) { - node.value = this.value; - } - // Style - if (this.style) { - node.style.cssText = this.style; - } - // Classes - this.classList.forEach((c) => { - node.classList.add(c); - }); - // Data - Object.keys(this.data).forEach((key) => { - node.dataset[key] = this.data[key]; - }); - // Children - this.children.forEach((c) => { - const cnode = c.render(); - node.appendChild(cnode); - c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); - }); - if (this.$html) { - node.innerHTML = this.$html; - } - return node; - } - // Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) - redraw(data) { - let { node, vnode } = data; - const newvnode = vnode; - const oldvnode = this; - if ( - oldvnode.constructor !== newvnode.constructor || - oldvnode.type !== newvnode.type || - (oldvnode.type === newvnode.type && - oldvnode.type === "#text" && - oldvnode !== newvnode) - ) { - const renderedNode = newvnode.render(); - node.parentNode.replaceChild(renderedNode, node); - newvnode.$onrender && newvnode.$onrender(renderedNode); - oldvnode.from(newvnode); - return; - } - // ID - if (oldvnode.id !== newvnode.id) { - node.id = newvnode.id || ""; - oldvnode.id = newvnode.id; - } - // Value - if (oldvnode.value !== newvnode.value) { - node.value = newvnode.value || ""; - oldvnode.value = newvnode.value; - } - // Classes - if (!equal(oldvnode.classList, newvnode.classList)) { - oldvnode.classList.forEach((c) => { - if (!newvnode.classList.includes(c)) { - node.classList.remove(c); + processVNodeObject(arg) { + if (arg instanceof VNode) { + return arg; } - }); - newvnode.classList.forEach((c) => { - if (!oldvnode.classList.includes(c)) { - node.classList.add(c); + if (arg instanceof Function) { + let vnode = arg(); + if (typeof vnode === "string") { + vnode = new VNode({ type: "#text", value: vnode }); + } + if (!(vnode instanceof VNode)) { + throw new Error( + "[VNode] Function argument does not return a VNode" + ); + } + return vnode; } - }); - oldvnode.classList = newvnode.classList; + throw new Error( + "[VNode] Invalid first argument provided to VNode constructor." + ); } - // Style - if (oldvnode.style !== newvnode.style) { - node.style.cssText = newvnode.style || ""; - oldvnode.style = newvnode.style; + + processChildren(arg) { + const children = Array.isArray(arg) ? arg : [arg]; + this.children = children + .map((c) => { + if (typeof c === "string") { + return new VNode({ type: "#text", value: c }); + } + if ( + typeof c === "function" || + (typeof c === "object" && c !== null) + ) { + return this.processVNodeObject(c); + } + if (c) { + throw new Error( + `[VNode] Specified child is not a VNode: ${c}` + ); + } + }) + .filter((c) => c); } - // Data - if (!equal(oldvnode.data, newvnode.data)) { - Object.keys(oldvnode.data).forEach((a) => { - if (!newvnode.data[a]) { - delete node.dataset[a]; - } else if (newvnode.data[a] !== oldvnode.data[a]) { - node.dataset[a] = newvnode.data[a]; + + // Renders the actual DOM Node corresponding to the current Virtual Node + render() { + if (this.type === "#text") { + return document.createTextNode(this.value); } - }); - Object.keys(newvnode.data).forEach((a) => { - if (!oldvnode.data[a]) { - node.dataset[a] = newvnode.data[a]; + const node = document.createElement(this.type); + if (this.id) { + node.id = this.id; } - }); - oldvnode.data = newvnode.data; - } - // props - if (!equal(oldvnode.props, newvnode.props)) { - Object.keys(oldvnode.props).forEach((a) => { - if (newvnode.props[a] === false) { - node[a] = false; + 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 + node[p] = this.props[p]; + }); + // Event Listeners + Object.keys(this.eventListeners).forEach((event) => { + node.addEventListener(event, this.eventListeners[event]); + }); + // Value + if (this.value) { + if (["textarea", "input"].includes(this.type)) { + node.value = this.value || ""; + } else { + node.setAttribute("value", this.value || ""); + } } - if (!newvnode.props[a]) { - node.removeAttribute(a); - } else if ( - newvnode.props[a] && - newvnode.props[a] !== oldvnode.props[a] - ) { - node.setAttribute(a, newvnode.props[a]); + // Style + if (this.style) { + node.style.cssText = this.style; } - }); - Object.keys(newvnode.props).forEach((a) => { - if (!oldvnode.props[a] && newvnode.props[a]) { - node.setAttribute(a, newvnode.props[a]); + // Classes + this.classList.forEach((c) => { + node.classList.add(c); + }); + // Data + Object.keys(this.data).forEach((key) => { + node.dataset[key] = this.data[key]; + }); + // Children + this.children.forEach((c) => { + const cnode = c.render(); + node.appendChild(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); + }); + if (this.$html) { + node.innerHTML = this.$html; } - }); - oldvnode.props = newvnode.props; + return node; } - // Event listeners - if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) { - Object.keys(oldvnode.eventListeners).forEach((a) => { - if (!newvnode.eventListeners[a]) { - node.removeEventListener(a, oldvnode.eventListeners[a]); - } else if ( - !equal(newvnode.eventListeners[a], oldvnode.eventListeners[a]) + + // Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) + redraw(data) { + let { node, vnode } = data; + const newvnode = vnode; + const oldvnode = this; + if ( + oldvnode.constructor !== newvnode.constructor || + oldvnode.type !== newvnode.type || + (oldvnode.type === newvnode.type && + oldvnode.type === "#text" && + oldvnode !== newvnode) ) { - node.removeEventListener(a, oldvnode.eventListeners[a]); - node.addEventListener(a, newvnode.eventListeners[a]); + const renderedNode = newvnode.render(); + node.parentNode.replaceChild(renderedNode, node); + newvnode.$onrender && newvnode.$onrender(renderedNode); + oldvnode.from(newvnode); + return; } - }); - Object.keys(newvnode.eventListeners).forEach((a) => { - if (!oldvnode.eventListeners[a]) { - node.addEventListener(a, newvnode.eventListeners[a]); + // ID + if (oldvnode.id !== newvnode.id) { + node.id = newvnode.id || ""; + oldvnode.id = newvnode.id; } - }); - 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; - } + // Value + if (oldvnode.value !== newvnode.value) { + oldvnode.value = newvnode.value; + if (["textarea", "input"].includes(oldvnode.type)) { + node.value = newvnode.value || ""; + } else { + node.setAttribute("value", newvnode.value || ""); + } } - 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; - } + // Classes + if (!equal(oldvnode.classList, newvnode.classList)) { + oldvnode.classList.forEach((c) => { + if (!newvnode.classList.includes(c)) { + node.classList.remove(c); + } + }); + newvnode.classList.forEach((c) => { + if (!oldvnode.classList.includes(c)) { + node.classList.add(c); + } + }); + oldvnode.classList = newvnode.classList; } - } - 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); - } + // Style + if (oldvnode.style !== newvnode.style) { + node.style.cssText = newvnode.style || ""; + oldvnode.style = newvnode.style; } - } - // 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; - } + // Data + if (!equal(oldvnode.data, newvnode.data)) { + Object.keys(oldvnode.data).forEach((a) => { + if (!newvnode.data[a]) { + delete node.dataset[a]; + } else if (newvnode.data[a] !== oldvnode.data[a]) { + node.dataset[a] = newvnode.data[a]; + } + }); + Object.keys(newvnode.data).forEach((a) => { + if (!oldvnode.data[a]) { + node.dataset[a] = newvnode.data[a]; + } + }); + oldvnode.data = newvnode.data; } - } - return map; - } - let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; - while (!equal(childMap, resultMap)) { - let count = -1; - for (let i of childMap) { - count++; - let breakFor = false; - if (i === count) { - // Matching nodes; - continue; + // 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.props[a] && + newvnode.props[a] !== oldvnode.props[a] + ) { + oldvnode.props[a] = newvnode.props[a]; + if ( + ["string", "number"].includes(typeof newvnode.props[a]) + ) { + node.setAttribute(a, newvnode.props[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]); + } + }); } - switch (i) { - case -1: - // different node, check - oldvnode.children[count].redraw({ - node: node.childNodes[count], - vnode: newvnode.children[count], + // Event listeners + if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) { + Object.keys(oldvnode.eventListeners).forEach((a) => { + if (!newvnode.eventListeners[a]) { + node.removeEventListener(a, oldvnode.eventListeners[a]); + } else if ( + !equal( + newvnode.eventListeners[a], + oldvnode.eventListeners[a] + ) + ) { + node.removeEventListener(a, oldvnode.eventListeners[a]); + node.addEventListener(a, newvnode.eventListeners[a]); + } }); - break; - case -2: - // add node - 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 - oldvnode.children.splice(count, 1); - node.removeChild(node.childNodes[count]); - breakFor = true; - break; - default: - // Node found, move nodes and remap - 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; + Object.keys(newvnode.eventListeners).forEach((a) => { + if (!oldvnode.eventListeners[a]) { + node.addEventListener(a, newvnode.eventListeners[a]); + } + }); + oldvnode.eventListeners = newvnode.eventListeners; + } + // Children + let childMap = mapChildren(oldvnode, newvnode); + let resultMap = [...Array(newvnode.children.length).keys()]; + while (!equal(childMap, resultMap)) { + let count = -1; + checkmap: for (const i of childMap) { + count++; + if (i === count) { + // Matching nodes; + continue; + } + switch (i) { + case PATCH: { + oldvnode.children[count].redraw({ + node: node.childNodes[count], + vnode: newvnode.children[count], + }); + 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); + break checkmap; + } + case DELETE: { + oldvnode.children.splice(count, 1); + node.removeChild(node.childNodes[count]); + break checkmap; + } + default: { + const vtarget = oldvnode.children.splice(i, 1)[0]; + oldvnode.children.splice(count, 0, vtarget); + const target = node.removeChild(node.childNodes[i]); + node.insertBefore(target, node.childNodes[count]); + break checkmap; + } + } + } + childMap = mapChildren(oldvnode, newvnode); + resultMap = [...Array(newvnode.children.length).keys()]; + } + // $onrender + if (!equal(oldvnode.$onrender, newvnode.$onrender)) { + oldvnode.$onrender = newvnode.$onrender; } - if (breakFor) { - break; + // innerHTML + if (oldvnode.$html !== newvnode.$html) { + node.innerHTML = newvnode.$html; + oldvnode.$html = newvnode.$html; + oldvnode.$onrender && oldvnode.$onrender(node); } - } - childMap = mapChildren(oldvnode, newvnode); - resultMap = [...Array(childMap.length).keys()]; } - // $onrender - if (!equal(oldvnode.$onrender, newvnode.$onrender)) { - oldvnode.$onrender = newvnode.$onrender; - } - // innerHTML - if (oldvnode.$html !== newvnode.$html) { - node.innerHTML = newvnode.$html; - oldvnode.$html = newvnode.$html; - oldvnode.$onrender && oldvnode.$onrender(node); - } - } } /**@@ -548,248 +546,256 @@ * <https://github.com/storeon/storeon/blob/master/LICENSE>
* Copyright 2019 Andrey Sitnik <andrey@sitnik.ru> */ class Store { - constructor() { - this.events = {}; - this.state = {}; - } - dispatch(event, data) { - if (event !== "$log") this.dispatch("$log", { event, data }); - if (this.events[event]) { - let changes = {}; - let changed; - this.events[event].forEach((i) => { - this.state = { ...this.state, ...i(this.state, data) }; - }); + constructor() { + this.events = {}; + this.state = {}; + } + dispatch(event, data) { + if (event !== "$log") this.dispatch("$log", { event, data }); + if (this.events[event]) { + let changes = {}; + let changed; + this.events[event].forEach((i) => { + this.state = { ...this.state, ...i(this.state, data) }; + }); + } } - } - on(event, cb) { - (this.events[event] || (this.events[event] = [])).push(cb); + on(event, cb) { + (this.events[event] || (this.events[event] = [])).push(cb); - return () => { - this.events[event] = this.events[event].filter((i) => i !== cb); - }; - } + return () => { + this.events[event] = this.events[event].filter((i) => i !== cb); + }; + } } class Route { - constructor({ path, def, query, parts }) { - this.path = path; - this.def = def; - this.query = query; - this.parts = parts; - this.params = {}; - if (this.query) { - const rawParams = this.query.split("&"); - rawParams.forEach((p) => { - const [name, value] = p.split("="); - this.params[decodeURIComponent(name)] = decodeURIComponent(value); - }); + constructor({ path, def, query, parts }) { + this.path = path; + this.def = def; + this.query = query; + this.parts = parts; + this.params = {}; + if (this.query) { + const rawParams = this.query.split("&"); + rawParams.forEach((p) => { + const [name, value] = p.split("="); + this.params[decodeURIComponent(name)] = decodeURIComponent( + value + ); + }); + } } - } } class Router { - constructor({ element, routes, store, location }) { - this.element = element; - this.redraw = null; - this.store = store; - this.location = location || window.location; - if (!routes || Object.keys(routes).length === 0) { - throw new Error("[Router] No routes defined."); + constructor({ element, routes, store, location }) { + this.element = element; + this.redraw = null; + this.store = store; + this.location = location || window.location; + if (!routes || Object.keys(routes).length === 0) { + throw new Error("[Router] No routes defined."); + } + const defs = Object.keys(routes); + this.routes = routes; } - const defs = Object.keys(routes); - this.routes = routes; - } - setRedraw(vnode, state) { - this.redraw = () => { - vnode.redraw({ - node: this.element.childNodes[0], - vnode: this.routes[this.route.def](state), - }); - this.store.dispatch("$redraw"); - }; - } + setRedraw(vnode, state) { + this.redraw = () => { + vnode.redraw({ + node: this.element.childNodes[0], + vnode: this.routes[this.route.def](state), + }); + this.store.dispatch("$redraw"); + }; + } - async start() { - const processPath = async (data) => { - const oldRoute = this.route; - const fragment = - (data && - data.newURL && - data.newURL.match(/(#.+)$/) && - data.newURL.match(/(#.+)$/)[1]) || - this.location.hash; - const path = fragment.replace(/\?.+$/, "").slice(1); - const rawQuery = fragment.match(/\?(.+)$/); - const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; - const pathParts = path.split("/").slice(1); + async start() { + const processPath = async (data) => { + const oldRoute = this.route; + const fragment = + (data && + data.newURL && + data.newURL.match(/(#.+)$/) && + data.newURL.match(/(#.+)$/)[1]) || + this.location.hash; + const path = fragment.replace(/\?.+$/, "").slice(1); + const rawQuery = fragment.match(/\?(.+)$/); + const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; + const pathParts = path.split("/").slice(1); - let parts = {}; - for (let def of Object.keys(this.routes)) { - let routeParts = def.split("/").slice(1); - let match = true; - let index = 0; - parts = {}; - while (match && routeParts[index]) { - const rP = routeParts[index]; - const pP = pathParts[index]; - if (rP.startsWith(":") && pP) { - parts[rP.slice(1)] = pP; - } else { - match = rP === pP; - } - index++; - } - if (match) { - this.route = new Route({ query, path, def, parts }); - break; - } - } - if (!this.route) { - throw new Error(`[Router] No route matches '${fragment}'`); - } - // Old route component teardown - if (oldRoute) { - const oldRouteComponent = this.routes[oldRoute.def]; - oldRouteComponent.state = - oldRouteComponent.teardown && - (await oldRouteComponent.teardown(oldRouteComponent.state)); - } - // New route component setup - const newRouteComponent = this.routes[this.route.def]; - newRouteComponent.state = {}; - newRouteComponent.setup && - (await newRouteComponent.setup(newRouteComponent.state)); - // Redrawing... - redrawing = true; - this.store.dispatch("$navigation", this.route); - while (this.element.firstChild) { - this.element.removeChild(this.element.firstChild); - } - const vnode = newRouteComponent(newRouteComponent.state); - const node = vnode.render(); - this.element.appendChild(node); - this.setRedraw(vnode, newRouteComponent.state); - redrawing = false; - vnode.$onrender && vnode.$onrender(node); - $onrenderCallbacks.forEach((cbk) => cbk()); - $onrenderCallbacks = []; - window.scrollTo(0, 0); - this.store.dispatch("$redraw"); - }; - window.addEventListener("hashchange", processPath); - await processPath(); - } + let parts = {}; + for (let def of Object.keys(this.routes)) { + let routeParts = def.split("/").slice(1); + let match = true; + let index = 0; + parts = {}; + while (match && routeParts[index]) { + const rP = routeParts[index]; + const pP = pathParts[index]; + if (rP.startsWith(":") && pP) { + parts[rP.slice(1)] = pP; + } else { + match = rP === pP; + } + index++; + } + if (match) { + this.route = new Route({ query, path, def, parts }); + break; + } + } + if (!this.route) { + throw new Error(`[Router] No route matches '${fragment}'`); + } + // Old route component teardown + if (oldRoute) { + const oldRouteComponent = this.routes[oldRoute.def]; + oldRouteComponent.state = + oldRouteComponent.teardown && + (await oldRouteComponent.teardown(oldRouteComponent.state)); + } + // New route component setup + const newRouteComponent = this.routes[this.route.def]; + newRouteComponent.state = {}; + newRouteComponent.setup && + (await newRouteComponent.setup(newRouteComponent.state)); + // Redrawing... + redrawing = true; + this.store.dispatch("$navigation", this.route); + while (this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } + const vnode = newRouteComponent(newRouteComponent.state); + const node = vnode.render(); + this.element.appendChild(node); + this.setRedraw(vnode, newRouteComponent.state); + redrawing = false; + vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; + window.scrollTo(0, 0); + this.store.dispatch("$redraw"); + }; + window.addEventListener("hashchange", processPath); + await processPath(); + } - navigateTo(path, params) { - let query = Object.keys(params || {}) - .map((p) => `${encodeURIComponent(p)}=${encodeURIComponent(params[p])}`) - .join("&"); - query = query ? `?${query}` : ""; - this.location.hash = `#${path}${query}`; - } + navigateTo(path, params) { + let query = Object.keys(params || {}) + .map( + (p) => + `${encodeURIComponent(p)}=${encodeURIComponent(params[p])}` + ) + .join("&"); + query = query ? `?${query}` : ""; + this.location.hash = `#${path}${query}`; + } } // High Level API -const h3 = (...args) => { - return new VNode(...args); + +export const h = (...args) => { + return new VNode(...args); }; + +export const h3 = {}; let store = null; let router = null; let redrawing = false; h3.init = (config) => { - let { element, routes, modules, preStart, postStart, location } = config; - if (!routes) { - // Assume config is a component object, define default route - if (typeof config !== "function") { - throw new Error( - "[h3.init] The specified argument is not a valid configuration object or component function" - ); + let { element, routes, modules, preStart, postStart, location } = config; + if (!routes) { + // Assume config is a component object, define default route + if (typeof config !== "function") { + throw new Error( + "[h3.init] The specified argument is not a valid configuration object or component function" + ); + } + routes = { "/": config }; + } + element = element || document.body; + if (!(element && element instanceof Element)) { + throw new Error("[h3.init] Invalid element specified."); } - routes = { "/": config }; - } - element = element || document.body; - if (!(element && element instanceof Element)) { - throw new Error("[h3.init] Invalid element specified."); - } - // Initialize store - store = new Store(); - (modules || []).forEach((i) => { - i(store); - }); - store.dispatch("$init"); - // Initialize router - router = new Router({ element, routes, store, location }); - return Promise.resolve(preStart && preStart()) - .then(() => router.start()) - .then(() => postStart && postStart()); + // Initialize store + store = new Store(); + (modules || []).forEach((i) => { + i(store); + }); + store.dispatch("$init"); + // Initialize router + router = new Router({ element, routes, store, location }); + return Promise.resolve(preStart && preStart()) + .then(() => router.start()) + .then(() => postStart && postStart()); }; h3.navigateTo = (path, params) => { - if (!router) { - throw new Error( - "[h3.navigateTo] No application initialized, unable to navigate." - ); - } - return router.navigateTo(path, params); + if (!router) { + throw new Error( + "[h3.navigateTo] No application initialized, unable to navigate." + ); + } + return router.navigateTo(path, params); }; Object.defineProperty(h3, "route", { - get: () => { - if (!router) { - throw new Error( - "[h3.route] No application initialized, unable to retrieve current route." - ); - } - return router.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; - }, + get: () => { + if (!store) { + throw new Error( + "[h3.state] No application initialized, unable to retrieve current state." + ); + } + return store.state; + }, }); h3.on = (event, cb) => { - if (!store) { - throw new Error( - "[h3.on] No application initialized, unable to listen to events." - ); - } - return store.on(event, cb); + if (!store) { + throw new Error( + "[h3.on] No application initialized, unable to listen to events." + ); + } + return store.on(event, cb); }; h3.dispatch = (event, data) => { - if (!store) { - throw new Error( - "[h3.dispatch] No application initialized, unable to dispatch events." - ); - } - return store.dispatch(event, data); + if (!store) { + throw new Error( + "[h3.dispatch] No application initialized, unable to dispatch events." + ); + } + return store.dispatch(event, data); }; h3.redraw = (setRedrawing) => { - if (!router || !router.redraw) { - throw new Error( - "[h3.redraw] No application initialized, unable to redraw." - ); - } - if (redrawing) { - return; - } - redrawing = true; - router.redraw(); - redrawing = setRedrawing || false; + if (!router || !router.redraw) { + throw new Error( + "[h3.redraw] No application initialized, unable to redraw." + ); + } + if (redrawing) { + return; + } + redrawing = true; + router.redraw(); + redrawing = setRedrawing || false; }; export default h3;
M
docs/example/assets/js/modules.js
→
docs/example/assets/js/modules.js
@@ -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];
M
docs/js/app.js
→
docs/js/app.js
@@ -1,77 +1,77 @@
-import h3 from "./h3.js"; +import { h3, h } from "./h3.js"; import marked from "./vendor/marked.js"; import Prism from "./vendor/prism.js"; const labels = { - overview: "Overview", - "quick-start": "Quick Start", - "key-concepts": "Key Concepts", - tutorial: "Tutorial", - api: "API", - about: "About", + overview: "Overview", + "quick-start": "Quick Start", + "key-concepts": "Key Concepts", + tutorial: "Tutorial", + api: "API", + about: "About", }; const pages = {}; 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(); - } + 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 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 h("div.page", [ + Header, + h("div.row", [ + h("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" }), - ]), - h3("div.version.col-sm.col-md", [ - h3("div.version-number", "v0.10.0"), - h3("div.version-label", "“Jittery Jem'Hadar“"), - ]), - h3("label.drawer-toggle.button.col-sm-last", { for: "drawer-control" }), - ]); + return h("header.row.sticky", [ + h("a.logo.col-sm-1", { href: "#/" }, [ + h("img", { alt: "H3", src: "images/h3.svg" }), + ]), + h("div.version.col-sm.col-md", [ + h("div.version-number", "v0.10.0"), + h("div.version-label", "“Jittery Jem'Hadar“"), + ]), + 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]) - ); - return h3("nav#navigation.col-md-3", [ - h3("label.drawer-close", { for: "drawer-control" }), - ...menu, - ]); + const menu = ids.map((p) => + h(`a${p === id ? ".active" : ""}`, { href: `#/${p}` }, labels[p]) + ); + 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 = html + ? h("div.content", { $html: html }) + : h("div.spinner-container", h("span.spinner")); + return h("main.col-sm-12.col-md-9", [ + h("div.card.fluid", h("div.section", content)), + ]); }; h3.init(Page);
M
docs/js/h3.js
→
docs/js/h3.js
@@ -6,539 +6,537 @@ * @license MIT
* For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE */ const checkProperties = (obj1, obj2) => { - for (const key in obj1) { - if (!(key in obj2)) { - return false; - } - if (!equal(obj1[key], obj2[key])) { - return false; + for (const key in obj1) { + if (!(key in obj2)) { + return false; + } + if (!equal(obj1[key], obj2[key])) { + return false; + } } - } - return true; + return true; }; const equal = (obj1, obj2) => { - if ( - (obj1 === null && obj2 === null) || - (obj1 === undefined && obj2 === undefined) - ) { - return true; - } - if ( - (obj1 === undefined && obj2 !== undefined) || - (obj1 !== undefined && obj2 === undefined) || - (obj1 === null && obj2 !== null) || - (obj1 !== null && obj2 === null) - ) { - return false; - } - if (obj1.constructor !== obj2.constructor) { - return false; - } - if (typeof obj1 === "function") { - if (obj1.toString() !== obj2.toString()) { - return false; + if ( + (obj1 === null && obj2 === null) || + (obj1 === undefined && obj2 === undefined) + ) { + return true; } - } - if ([String, Number, Boolean].includes(obj1.constructor)) { - return obj1 === obj2; - } - if (obj1.constructor === Array) { - if (obj1.length !== obj2.length) { - return false; + if ( + (obj1 === undefined && obj2 !== undefined) || + (obj1 !== undefined && obj2 === undefined) || + (obj1 === null && obj2 !== null) || + (obj1 !== null && obj2 === null) + ) { + return false; } - for (let i = 0; i < obj1.length; i++) { - if (!equal(obj1[i], obj2[i])) { + if (obj1.constructor !== obj2.constructor) { return false; - } + } + if (typeof obj1 === "function") { + if (obj1.toString() !== obj2.toString()) { + return false; + } + } + if ([String, Number, Boolean].includes(obj1.constructor)) { + return obj1 === obj2; + } + if (obj1.constructor === Array) { + if (obj1.length !== obj2.length) { + return false; + } + for (let i = 0; i < obj1.length; i++) { + if (!equal(obj1[i], obj2[i])) { + return false; + } + } + return true; } - return true; - } - return checkProperties(obj1, obj2); // && checkProperties(obj2, obj1); + 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 = []; -let $onrenderCallbacks = []; +const mapChildren = (oldvnode, newvnode) => { + const newList = newvnode.children; + const oldList = oldvnode.children; + const 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); + } + if (oldList.length > newList.length) { + // Remove remaining nodes + [...Array(oldList.length - newList.length).keys()].forEach(() => + map.push(DELETE) + ); + } + return map; +}; // Virtual Node Implementation with HyperScript-like syntax class VNode { - - constructor(...args) { - this.type = undefined; - this.props = {}; - this.data = {}; - this.id = undefined; - this.$html = undefined; - this.$onrender = undefined; - this.style = undefined; - this.value = undefined; - this.children = []; - this.classList = []; - this.eventListeners = {}; - if (args.length === 0) { - throw new Error("[VNode] No arguments passed to VNode constructor."); - } - if (args.length === 1) { - let vnode = args[0]; - if (typeof vnode === "string") { - // Assume empty element - this.processSelector(vnode); - } else if ( - typeof vnode === "function" || - (typeof vnode === "object" && vnode !== null) - ) { - // Text node - if (vnode.type === "#text") { - this.type = "#text"; - this.value = vnode.value; - } else { - this.from(this.processVNodeObject(vnode)); + constructor(...args) { + this.type = undefined; + this.props = {}; + this.data = {}; + this.id = undefined; + this.$html = undefined; + this.$onrender = undefined; + this.style = undefined; + this.value = undefined; + this.children = []; + this.classList = []; + this.eventListeners = {}; + if (args.length === 0) { + throw new Error( + "[VNode] No arguments passed to VNode constructor." + ); } - } else { - throw new Error( - "[VNode] Invalid first argument passed to VNode constructor." - ); - } - } else if (args.length === 2) { - let [selector, data] = args; - if (typeof selector !== "string") { - throw new Error( - "[VNode] Invalid first argument passed to VNode constructor." - ); - } - this.processSelector(selector); - if (typeof data === "string") { - // Assume single child text node - this.children = [new VNode({ type: "#text", value: data })]; - return; - } - if ( - typeof data !== "function" && - (typeof data !== "object" || data === null) - ) { - throw new Error( - "[VNode] The second argument of a VNode constructor must be an object, an array or a string." - ); - } - if (Array.isArray(data)) { - // Assume 2nd argument as children - this.processChildren(data); - } else { - if (data instanceof Function || data instanceof VNode) { - this.processChildren(data); + if (args.length === 1) { + let vnode = args[0]; + if (typeof vnode === "string") { + // Assume empty element + this.processSelector(vnode); + } else if ( + typeof vnode === "function" || + (typeof vnode === "object" && vnode !== null) + ) { + // Text node + if (vnode.type === "#text") { + this.type = "#text"; + this.value = vnode.value; + } else { + this.from(this.processVNodeObject(vnode)); + } + } else { + throw new Error( + "[VNode] Invalid first argument passed to VNode constructor." + ); + } + } else if (args.length === 2) { + let [selector, data] = args; + if (typeof selector !== "string") { + throw new Error( + "[VNode] Invalid first argument passed to VNode constructor." + ); + } + this.processSelector(selector); + if (typeof data === "string") { + // Assume single child text node + this.children = [new VNode({ type: "#text", value: data })]; + return; + } + if ( + typeof data !== "function" && + (typeof data !== "object" || data === null) + ) { + throw new Error( + "[VNode] The second argument of a VNode constructor must be an object, an array or a string." + ); + } + if (Array.isArray(data)) { + // Assume 2nd argument as children + this.processChildren(data); + } else { + if (data instanceof Function || data instanceof VNode) { + this.processChildren(data); + } else { + // Not a VNode, assume props object + this.processProperties(data); + } + } } else { - // Not a VNode, assume props object - this.processProperties(data); + 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 ( + 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.processChildren(children); } - } - } 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 ( - 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.processChildren(children); } - } - from(data) { - this.value = data.value; - this.type = data.type; - this.id = data.id; - this.$html = data.$html; - this.$onrender = data.$onrender; - this.style = data.style; - this.data = data.data; - this.value = data.value; - this.eventListeners = data.eventListeners; - this.children = data.children; - this.props = data.props; - this.classList = data.classList; - } - - equal(a, b) { - return equal(a, b === undefined ? this : b); - } + from(data) { + this.value = data.value; + this.type = data.type; + this.id = data.id; + this.$html = data.$html; + this.$onrender = data.$onrender; + this.style = data.style; + this.data = data.data; + this.value = data.value; + this.eventListeners = data.eventListeners; + this.children = data.children; + this.props = data.props; + this.classList = data.classList; + } - processProperties(attrs) { - this.id = this.id || attrs.id; - this.$html = attrs.$html; - this.$onrender = attrs.$onrender; - this.style = attrs.style; - this.value = attrs.value; - this.data = attrs.data || {}; - this.classList = - attrs.classList && attrs.classList.length > 0 - ? attrs.classList - : this.classList; - this.props = attrs; - Object.keys(attrs) - .filter((a) => a.startsWith("on") && attrs[a]) - .forEach((key) => { - if (typeof attrs[key] !== "function") { - throw new Error( - `[VNode] Event handler specified for ${key} event is not a function.` - ); - } - this.eventListeners[key.slice(2)] = attrs[key]; - delete this.props[key]; - }); - 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) || selector.length === 0) { - throw new Error(`[VNode] Invalid selector: ${selector}`); + equal(a, b) { + return equal(a, b === undefined ? this : b); } - const [, type, id, classes] = selector.match(selectorRegex); - this.type = type; - if (id) { - this.id = id.slice(1); - } - this.classList = (classes && classes.split(".").slice(1)) || []; - } - processVNodeObject(arg) { - if (arg instanceof VNode) { - return arg; - } - if (arg instanceof Function) { - let vnode = arg(); - if (typeof vnode === "string") { - vnode = new VNode({ type: "#text", value: vnode }); - } - if (!(vnode instanceof VNode)) { - throw new Error("[VNode] Function argument does not return a VNode"); - } - return vnode; + processProperties(attrs) { + this.id = this.id || attrs.id; + this.$html = attrs.$html; + this.$onrender = attrs.$onrender; + this.style = attrs.style; + this.value = attrs.value; + this.data = attrs.data || {}; + this.classList = + attrs.classList && attrs.classList.length > 0 + ? attrs.classList + : this.classList; + this.props = attrs; + Object.keys(attrs) + .filter((a) => a.startsWith("on") && attrs[a]) + .forEach((key) => { + if (typeof attrs[key] !== "function") { + throw new Error( + `[VNode] Event handler specified for ${key} event is not a function.` + ); + } + this.eventListeners[key.slice(2)] = attrs[key]; + delete this.props[key]; + }); + 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; } - throw new Error( - "[VNode] Invalid first argument provided to VNode constructor." - ); - } - processChildren(arg) { - const children = Array.isArray(arg) ? arg : [arg]; - this.children = children - .map((c) => { - if (typeof c === "string") { - return new VNode({ type: "#text", value: c }); - } - if (typeof c === "function" || (typeof c === "object" && c !== null)) { - return this.processVNodeObject(c); + processSelector(selector) { + if (!selector.match(selectorRegex) || selector.length === 0) { + throw new Error(`[VNode] Invalid selector: ${selector}`); } - if (c) { - throw new Error(`[VNode] Specified child is not a VNode: ${c}`); + const [, type, id, classes] = selector.match(selectorRegex); + this.type = type; + if (id) { + this.id = id.slice(1); } - }) - .filter((c) => c); - } - - // Renders the actual DOM Node corresponding to the current Virtual Node - render() { - if (this.type === "#text") { - return document.createTextNode(this.value); + this.classList = (classes && classes.split(".").slice(1)) || []; } - const node = document.createElement(this.type); - if (this.id) { - node.id = this.id; - } - Object.keys(this.props).forEach((attr) => { - // Set props (only if non-empty strings) - if (this.props[attr] && typeof this.props[attr] === "string") { - const a = document.createAttribute(attr); - a.value = this.props[attr]; - node.setAttributeNode(a); - } - // Set properties - if (typeof this.props[attr] !== "string" || !node[attr]) { - node[attr] = this.props[attr]; - } - }); - // Event Listeners - Object.keys(this.eventListeners).forEach((event) => { - node.addEventListener(event, this.eventListeners[event]); - }); - // Value - if (this.value) { - node.value = this.value; - } - // Style - if (this.style) { - node.style.cssText = this.style; - } - // Classes - this.classList.forEach((c) => { - node.classList.add(c); - }); - // Data - Object.keys(this.data).forEach((key) => { - node.dataset[key] = this.data[key]; - }); - // Children - this.children.forEach((c) => { - const cnode = c.render(); - node.appendChild(cnode); - c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); - }); - if (this.$html) { - node.innerHTML = this.$html; - } - return node; - } - // Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) - redraw(data) { - let { node, vnode } = data; - const newvnode = vnode; - const oldvnode = this; - if ( - oldvnode.constructor !== newvnode.constructor || - oldvnode.type !== newvnode.type || - (oldvnode.type === newvnode.type && - oldvnode.type === "#text" && - oldvnode !== newvnode) - ) { - const renderedNode = newvnode.render(); - node.parentNode.replaceChild(renderedNode, node); - newvnode.$onrender && newvnode.$onrender(renderedNode); - oldvnode.from(newvnode); - return; - } - // ID - if (oldvnode.id !== newvnode.id) { - node.id = newvnode.id || ""; - oldvnode.id = newvnode.id; - } - // Value - if (oldvnode.value !== newvnode.value) { - node.value = newvnode.value || ""; - oldvnode.value = newvnode.value; - } - // Classes - if (!equal(oldvnode.classList, newvnode.classList)) { - oldvnode.classList.forEach((c) => { - if (!newvnode.classList.includes(c)) { - node.classList.remove(c); + processVNodeObject(arg) { + if (arg instanceof VNode) { + return arg; } - }); - newvnode.classList.forEach((c) => { - if (!oldvnode.classList.includes(c)) { - node.classList.add(c); + if (arg instanceof Function) { + let vnode = arg(); + if (typeof vnode === "string") { + vnode = new VNode({ type: "#text", value: vnode }); + } + if (!(vnode instanceof VNode)) { + throw new Error( + "[VNode] Function argument does not return a VNode" + ); + } + return vnode; } - }); - oldvnode.classList = newvnode.classList; + throw new Error( + "[VNode] Invalid first argument provided to VNode constructor." + ); } - // Style - if (oldvnode.style !== newvnode.style) { - node.style.cssText = newvnode.style || ""; - oldvnode.style = newvnode.style; + + processChildren(arg) { + const children = Array.isArray(arg) ? arg : [arg]; + this.children = children + .map((c) => { + if (typeof c === "string") { + return new VNode({ type: "#text", value: c }); + } + if ( + typeof c === "function" || + (typeof c === "object" && c !== null) + ) { + return this.processVNodeObject(c); + } + if (c) { + throw new Error( + `[VNode] Specified child is not a VNode: ${c}` + ); + } + }) + .filter((c) => c); } - // Data - if (!equal(oldvnode.data, newvnode.data)) { - Object.keys(oldvnode.data).forEach((a) => { - if (!newvnode.data[a]) { - delete node.dataset[a]; - } else if (newvnode.data[a] !== oldvnode.data[a]) { - node.dataset[a] = newvnode.data[a]; + + // Renders the actual DOM Node corresponding to the current Virtual Node + render() { + if (this.type === "#text") { + return document.createTextNode(this.value); } - }); - Object.keys(newvnode.data).forEach((a) => { - if (!oldvnode.data[a]) { - node.dataset[a] = newvnode.data[a]; + const node = document.createElement(this.type); + if (this.id) { + node.id = this.id; } - }); - oldvnode.data = newvnode.data; - } - // props - if (!equal(oldvnode.props, newvnode.props)) { - Object.keys(oldvnode.props).forEach((a) => { - if (newvnode.props[a] === false) { - node[a] = false; + 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 + node[p] = this.props[p]; + }); + // Event Listeners + Object.keys(this.eventListeners).forEach((event) => { + node.addEventListener(event, this.eventListeners[event]); + }); + // Value + if (this.value) { + if (["textarea", "input"].includes(this.type)) { + node.value = this.value || ""; + } else { + node.setAttribute("value", this.value || ""); + } } - if (!newvnode.props[a]) { - node.removeAttribute(a); - } else if ( - newvnode.props[a] && - newvnode.props[a] !== oldvnode.props[a] - ) { - node.setAttribute(a, newvnode.props[a]); + // Style + if (this.style) { + node.style.cssText = this.style; } - }); - Object.keys(newvnode.props).forEach((a) => { - if (!oldvnode.props[a] && newvnode.props[a]) { - node.setAttribute(a, newvnode.props[a]); + // Classes + this.classList.forEach((c) => { + node.classList.add(c); + }); + // Data + Object.keys(this.data).forEach((key) => { + node.dataset[key] = this.data[key]; + }); + // Children + this.children.forEach((c) => { + const cnode = c.render(); + node.appendChild(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); + }); + if (this.$html) { + node.innerHTML = this.$html; } - }); - oldvnode.props = newvnode.props; + return node; } - // Event listeners - if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) { - Object.keys(oldvnode.eventListeners).forEach((a) => { - if (!newvnode.eventListeners[a]) { - node.removeEventListener(a, oldvnode.eventListeners[a]); - } else if ( - !equal(newvnode.eventListeners[a], oldvnode.eventListeners[a]) + + // Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) + redraw(data) { + let { node, vnode } = data; + const newvnode = vnode; + const oldvnode = this; + if ( + oldvnode.constructor !== newvnode.constructor || + oldvnode.type !== newvnode.type || + (oldvnode.type === newvnode.type && + oldvnode.type === "#text" && + oldvnode !== newvnode) ) { - node.removeEventListener(a, oldvnode.eventListeners[a]); - node.addEventListener(a, newvnode.eventListeners[a]); + const renderedNode = newvnode.render(); + node.parentNode.replaceChild(renderedNode, node); + newvnode.$onrender && newvnode.$onrender(renderedNode); + oldvnode.from(newvnode); + return; } - }); - Object.keys(newvnode.eventListeners).forEach((a) => { - if (!oldvnode.eventListeners[a]) { - node.addEventListener(a, newvnode.eventListeners[a]); + // ID + if (oldvnode.id !== newvnode.id) { + node.id = newvnode.id || ""; + oldvnode.id = newvnode.id; } - }); - 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; - } + // Value + if (oldvnode.value !== newvnode.value) { + oldvnode.value = newvnode.value; + if (["textarea", "input"].includes(oldvnode.type)) { + node.value = newvnode.value || ""; + } else { + node.setAttribute("value", newvnode.value || ""); + } } - 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; - } + // Classes + if (!equal(oldvnode.classList, newvnode.classList)) { + oldvnode.classList.forEach((c) => { + if (!newvnode.classList.includes(c)) { + node.classList.remove(c); + } + }); + newvnode.classList.forEach((c) => { + if (!oldvnode.classList.includes(c)) { + node.classList.add(c); + } + }); + oldvnode.classList = newvnode.classList; } - } - 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); - } + // Style + if (oldvnode.style !== newvnode.style) { + node.style.cssText = newvnode.style || ""; + oldvnode.style = newvnode.style; } - } - // 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; - } + // Data + if (!equal(oldvnode.data, newvnode.data)) { + Object.keys(oldvnode.data).forEach((a) => { + if (!newvnode.data[a]) { + delete node.dataset[a]; + } else if (newvnode.data[a] !== oldvnode.data[a]) { + node.dataset[a] = newvnode.data[a]; + } + }); + Object.keys(newvnode.data).forEach((a) => { + if (!oldvnode.data[a]) { + node.dataset[a] = newvnode.data[a]; + } + }); + oldvnode.data = newvnode.data; } - } - return map; - } - let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; - while (!equal(childMap, resultMap)) { - let count = -1; - for (let i of childMap) { - count++; - let breakFor = false; - if (i === count) { - // Matching nodes; - continue; + // 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.props[a] && + newvnode.props[a] !== oldvnode.props[a] + ) { + oldvnode.props[a] = newvnode.props[a]; + if ( + ["string", "number"].includes(typeof newvnode.props[a]) + ) { + node.setAttribute(a, newvnode.props[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]); + } + }); } - switch (i) { - case -1: - // different node, check - oldvnode.children[count].redraw({ - node: node.childNodes[count], - vnode: newvnode.children[count], + // Event listeners + if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) { + Object.keys(oldvnode.eventListeners).forEach((a) => { + if (!newvnode.eventListeners[a]) { + node.removeEventListener(a, oldvnode.eventListeners[a]); + } else if ( + !equal( + newvnode.eventListeners[a], + oldvnode.eventListeners[a] + ) + ) { + node.removeEventListener(a, oldvnode.eventListeners[a]); + node.addEventListener(a, newvnode.eventListeners[a]); + } }); - break; - case -2: - // add node - 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 - oldvnode.children.splice(count, 1); - node.removeChild(node.childNodes[count]); - breakFor = true; - break; - default: - // Node found, move nodes and remap - 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; + Object.keys(newvnode.eventListeners).forEach((a) => { + if (!oldvnode.eventListeners[a]) { + node.addEventListener(a, newvnode.eventListeners[a]); + } + }); + oldvnode.eventListeners = newvnode.eventListeners; + } + // Children + let childMap = mapChildren(oldvnode, newvnode); + let resultMap = [...Array(newvnode.children.length).keys()]; + while (!equal(childMap, resultMap)) { + let count = -1; + checkmap: for (const i of childMap) { + count++; + if (i === count) { + // Matching nodes; + continue; + } + switch (i) { + case PATCH: { + oldvnode.children[count].redraw({ + node: node.childNodes[count], + vnode: newvnode.children[count], + }); + 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); + break checkmap; + } + case DELETE: { + oldvnode.children.splice(count, 1); + node.removeChild(node.childNodes[count]); + break checkmap; + } + default: { + const vtarget = oldvnode.children.splice(i, 1)[0]; + oldvnode.children.splice(count, 0, vtarget); + const target = node.removeChild(node.childNodes[i]); + node.insertBefore(target, node.childNodes[count]); + break checkmap; + } + } + } + childMap = mapChildren(oldvnode, newvnode); + resultMap = [...Array(newvnode.children.length).keys()]; + } + // $onrender + if (!equal(oldvnode.$onrender, newvnode.$onrender)) { + oldvnode.$onrender = newvnode.$onrender; } - if (breakFor) { - break; + // innerHTML + if (oldvnode.$html !== newvnode.$html) { + node.innerHTML = newvnode.$html; + oldvnode.$html = newvnode.$html; + oldvnode.$onrender && oldvnode.$onrender(node); } - } - childMap = mapChildren(oldvnode, newvnode); - resultMap = [...Array(childMap.length).keys()]; } - // $onrender - if (!equal(oldvnode.$onrender, newvnode.$onrender)) { - oldvnode.$onrender = newvnode.$onrender; - } - // innerHTML - if (oldvnode.$html !== newvnode.$html) { - node.innerHTML = newvnode.$html; - oldvnode.$html = newvnode.$html; - oldvnode.$onrender && oldvnode.$onrender(node); - } - } } /**@@ -548,248 +546,256 @@ * <https://github.com/storeon/storeon/blob/master/LICENSE>
* Copyright 2019 Andrey Sitnik <andrey@sitnik.ru> */ class Store { - constructor() { - this.events = {}; - this.state = {}; - } - dispatch(event, data) { - if (event !== "$log") this.dispatch("$log", { event, data }); - if (this.events[event]) { - let changes = {}; - let changed; - this.events[event].forEach((i) => { - this.state = { ...this.state, ...i(this.state, data) }; - }); + constructor() { + this.events = {}; + this.state = {}; + } + dispatch(event, data) { + if (event !== "$log") this.dispatch("$log", { event, data }); + if (this.events[event]) { + let changes = {}; + let changed; + this.events[event].forEach((i) => { + this.state = { ...this.state, ...i(this.state, data) }; + }); + } } - } - on(event, cb) { - (this.events[event] || (this.events[event] = [])).push(cb); + on(event, cb) { + (this.events[event] || (this.events[event] = [])).push(cb); - return () => { - this.events[event] = this.events[event].filter((i) => i !== cb); - }; - } + return () => { + this.events[event] = this.events[event].filter((i) => i !== cb); + }; + } } class Route { - constructor({ path, def, query, parts }) { - this.path = path; - this.def = def; - this.query = query; - this.parts = parts; - this.params = {}; - if (this.query) { - const rawParams = this.query.split("&"); - rawParams.forEach((p) => { - const [name, value] = p.split("="); - this.params[decodeURIComponent(name)] = decodeURIComponent(value); - }); + constructor({ path, def, query, parts }) { + this.path = path; + this.def = def; + this.query = query; + this.parts = parts; + this.params = {}; + if (this.query) { + const rawParams = this.query.split("&"); + rawParams.forEach((p) => { + const [name, value] = p.split("="); + this.params[decodeURIComponent(name)] = decodeURIComponent( + value + ); + }); + } } - } } class Router { - constructor({ element, routes, store, location }) { - this.element = element; - this.redraw = null; - this.store = store; - this.location = location || window.location; - if (!routes || Object.keys(routes).length === 0) { - throw new Error("[Router] No routes defined."); + constructor({ element, routes, store, location }) { + this.element = element; + this.redraw = null; + this.store = store; + this.location = location || window.location; + if (!routes || Object.keys(routes).length === 0) { + throw new Error("[Router] No routes defined."); + } + const defs = Object.keys(routes); + this.routes = routes; } - const defs = Object.keys(routes); - this.routes = routes; - } - setRedraw(vnode, state) { - this.redraw = () => { - vnode.redraw({ - node: this.element.childNodes[0], - vnode: this.routes[this.route.def](state), - }); - this.store.dispatch("$redraw"); - }; - } + setRedraw(vnode, state) { + this.redraw = () => { + vnode.redraw({ + node: this.element.childNodes[0], + vnode: this.routes[this.route.def](state), + }); + this.store.dispatch("$redraw"); + }; + } - async start() { - const processPath = async (data) => { - const oldRoute = this.route; - const fragment = - (data && - data.newURL && - data.newURL.match(/(#.+)$/) && - data.newURL.match(/(#.+)$/)[1]) || - this.location.hash; - const path = fragment.replace(/\?.+$/, "").slice(1); - const rawQuery = fragment.match(/\?(.+)$/); - const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; - const pathParts = path.split("/").slice(1); + async start() { + const processPath = async (data) => { + const oldRoute = this.route; + const fragment = + (data && + data.newURL && + data.newURL.match(/(#.+)$/) && + data.newURL.match(/(#.+)$/)[1]) || + this.location.hash; + const path = fragment.replace(/\?.+$/, "").slice(1); + const rawQuery = fragment.match(/\?(.+)$/); + const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; + const pathParts = path.split("/").slice(1); - let parts = {}; - for (let def of Object.keys(this.routes)) { - let routeParts = def.split("/").slice(1); - let match = true; - let index = 0; - parts = {}; - while (match && routeParts[index]) { - const rP = routeParts[index]; - const pP = pathParts[index]; - if (rP.startsWith(":") && pP) { - parts[rP.slice(1)] = pP; - } else { - match = rP === pP; - } - index++; - } - if (match) { - this.route = new Route({ query, path, def, parts }); - break; - } - } - if (!this.route) { - throw new Error(`[Router] No route matches '${fragment}'`); - } - // Old route component teardown - if (oldRoute) { - const oldRouteComponent = this.routes[oldRoute.def]; - oldRouteComponent.state = - oldRouteComponent.teardown && - (await oldRouteComponent.teardown(oldRouteComponent.state)); - } - // New route component setup - const newRouteComponent = this.routes[this.route.def]; - newRouteComponent.state = {}; - newRouteComponent.setup && - (await newRouteComponent.setup(newRouteComponent.state)); - // Redrawing... - redrawing = true; - this.store.dispatch("$navigation", this.route); - while (this.element.firstChild) { - this.element.removeChild(this.element.firstChild); - } - const vnode = newRouteComponent(newRouteComponent.state); - const node = vnode.render(); - this.element.appendChild(node); - this.setRedraw(vnode, newRouteComponent.state); - redrawing = false; - vnode.$onrender && vnode.$onrender(node); - $onrenderCallbacks.forEach((cbk) => cbk()); - $onrenderCallbacks = []; - window.scrollTo(0, 0); - this.store.dispatch("$redraw"); - }; - window.addEventListener("hashchange", processPath); - await processPath(); - } + let parts = {}; + for (let def of Object.keys(this.routes)) { + let routeParts = def.split("/").slice(1); + let match = true; + let index = 0; + parts = {}; + while (match && routeParts[index]) { + const rP = routeParts[index]; + const pP = pathParts[index]; + if (rP.startsWith(":") && pP) { + parts[rP.slice(1)] = pP; + } else { + match = rP === pP; + } + index++; + } + if (match) { + this.route = new Route({ query, path, def, parts }); + break; + } + } + if (!this.route) { + throw new Error(`[Router] No route matches '${fragment}'`); + } + // Old route component teardown + if (oldRoute) { + const oldRouteComponent = this.routes[oldRoute.def]; + oldRouteComponent.state = + oldRouteComponent.teardown && + (await oldRouteComponent.teardown(oldRouteComponent.state)); + } + // New route component setup + const newRouteComponent = this.routes[this.route.def]; + newRouteComponent.state = {}; + newRouteComponent.setup && + (await newRouteComponent.setup(newRouteComponent.state)); + // Redrawing... + redrawing = true; + this.store.dispatch("$navigation", this.route); + while (this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } + const vnode = newRouteComponent(newRouteComponent.state); + const node = vnode.render(); + this.element.appendChild(node); + this.setRedraw(vnode, newRouteComponent.state); + redrawing = false; + vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; + window.scrollTo(0, 0); + this.store.dispatch("$redraw"); + }; + window.addEventListener("hashchange", processPath); + await processPath(); + } - navigateTo(path, params) { - let query = Object.keys(params || {}) - .map((p) => `${encodeURIComponent(p)}=${encodeURIComponent(params[p])}`) - .join("&"); - query = query ? `?${query}` : ""; - this.location.hash = `#${path}${query}`; - } + navigateTo(path, params) { + let query = Object.keys(params || {}) + .map( + (p) => + `${encodeURIComponent(p)}=${encodeURIComponent(params[p])}` + ) + .join("&"); + query = query ? `?${query}` : ""; + this.location.hash = `#${path}${query}`; + } } // High Level API -const h3 = (...args) => { - return new VNode(...args); + +export const h = (...args) => { + return new VNode(...args); }; + +export const h3 = {}; let store = null; let router = null; let redrawing = false; h3.init = (config) => { - let { element, routes, modules, preStart, postStart, location } = config; - if (!routes) { - // Assume config is a component object, define default route - if (typeof config !== "function") { - throw new Error( - "[h3.init] The specified argument is not a valid configuration object or component function" - ); + let { element, routes, modules, preStart, postStart, location } = config; + if (!routes) { + // Assume config is a component object, define default route + if (typeof config !== "function") { + throw new Error( + "[h3.init] The specified argument is not a valid configuration object or component function" + ); + } + routes = { "/": config }; + } + element = element || document.body; + if (!(element && element instanceof Element)) { + throw new Error("[h3.init] Invalid element specified."); } - routes = { "/": config }; - } - element = element || document.body; - if (!(element && element instanceof Element)) { - throw new Error("[h3.init] Invalid element specified."); - } - // Initialize store - store = new Store(); - (modules || []).forEach((i) => { - i(store); - }); - store.dispatch("$init"); - // Initialize router - router = new Router({ element, routes, store, location }); - return Promise.resolve(preStart && preStart()) - .then(() => router.start()) - .then(() => postStart && postStart()); + // Initialize store + store = new Store(); + (modules || []).forEach((i) => { + i(store); + }); + store.dispatch("$init"); + // Initialize router + router = new Router({ element, routes, store, location }); + return Promise.resolve(preStart && preStart()) + .then(() => router.start()) + .then(() => postStart && postStart()); }; h3.navigateTo = (path, params) => { - if (!router) { - throw new Error( - "[h3.navigateTo] No application initialized, unable to navigate." - ); - } - return router.navigateTo(path, params); + if (!router) { + throw new Error( + "[h3.navigateTo] No application initialized, unable to navigate." + ); + } + return router.navigateTo(path, params); }; Object.defineProperty(h3, "route", { - get: () => { - if (!router) { - throw new Error( - "[h3.route] No application initialized, unable to retrieve current route." - ); - } - return router.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; - }, + get: () => { + if (!store) { + throw new Error( + "[h3.state] No application initialized, unable to retrieve current state." + ); + } + return store.state; + }, }); h3.on = (event, cb) => { - if (!store) { - throw new Error( - "[h3.on] No application initialized, unable to listen to events." - ); - } - return store.on(event, cb); + if (!store) { + throw new Error( + "[h3.on] No application initialized, unable to listen to events." + ); + } + return store.on(event, cb); }; h3.dispatch = (event, data) => { - if (!store) { - throw new Error( - "[h3.dispatch] No application initialized, unable to dispatch events." - ); - } - return store.dispatch(event, data); + if (!store) { + throw new Error( + "[h3.dispatch] No application initialized, unable to dispatch events." + ); + } + return store.dispatch(event, data); }; h3.redraw = (setRedrawing) => { - if (!router || !router.redraw) { - throw new Error( - "[h3.redraw] No application initialized, unable to redraw." - ); - } - if (redrawing) { - return; - } - redrawing = true; - router.redraw(); - redrawing = setRedrawing || false; + if (!router || !router.redraw) { + throw new Error( + "[h3.redraw] No application initialized, unable to redraw." + ); + } + if (redrawing) { + return; + } + redrawing = true; + router.redraw(); + redrawing = setRedrawing || false; }; export default h3;
M
h3.js
→
h3.js
@@ -6,539 +6,537 @@ * @license MIT
* For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE */ const checkProperties = (obj1, obj2) => { - for (const key in obj1) { - if (!(key in obj2)) { - return false; - } - if (!equal(obj1[key], obj2[key])) { - return false; + for (const key in obj1) { + if (!(key in obj2)) { + return false; + } + if (!equal(obj1[key], obj2[key])) { + return false; + } } - } - return true; + return true; }; const equal = (obj1, obj2) => { - if ( - (obj1 === null && obj2 === null) || - (obj1 === undefined && obj2 === undefined) - ) { - return true; - } - if ( - (obj1 === undefined && obj2 !== undefined) || - (obj1 !== undefined && obj2 === undefined) || - (obj1 === null && obj2 !== null) || - (obj1 !== null && obj2 === null) - ) { - return false; - } - if (obj1.constructor !== obj2.constructor) { - return false; - } - if (typeof obj1 === "function") { - if (obj1.toString() !== obj2.toString()) { - return false; + if ( + (obj1 === null && obj2 === null) || + (obj1 === undefined && obj2 === undefined) + ) { + return true; } - } - if ([String, Number, Boolean].includes(obj1.constructor)) { - return obj1 === obj2; - } - if (obj1.constructor === Array) { - if (obj1.length !== obj2.length) { - return false; + if ( + (obj1 === undefined && obj2 !== undefined) || + (obj1 !== undefined && obj2 === undefined) || + (obj1 === null && obj2 !== null) || + (obj1 !== null && obj2 === null) + ) { + return false; } - for (let i = 0; i < obj1.length; i++) { - if (!equal(obj1[i], obj2[i])) { + if (obj1.constructor !== obj2.constructor) { return false; - } + } + if (typeof obj1 === "function") { + if (obj1.toString() !== obj2.toString()) { + return false; + } + } + if ([String, Number, Boolean].includes(obj1.constructor)) { + return obj1 === obj2; + } + if (obj1.constructor === Array) { + if (obj1.length !== obj2.length) { + return false; + } + for (let i = 0; i < obj1.length; i++) { + if (!equal(obj1[i], obj2[i])) { + return false; + } + } + return true; } - return true; - } - return checkProperties(obj1, obj2); // && checkProperties(obj2, obj1); + 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 = []; -let $onrenderCallbacks = []; +const mapChildren = (oldvnode, newvnode) => { + const newList = newvnode.children; + const oldList = oldvnode.children; + const 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); + } + if (oldList.length > newList.length) { + // Remove remaining nodes + [...Array(oldList.length - newList.length).keys()].forEach(() => + map.push(DELETE) + ); + } + return map; +}; // Virtual Node Implementation with HyperScript-like syntax class VNode { - - constructor(...args) { - this.type = undefined; - this.props = {}; - this.data = {}; - this.id = undefined; - this.$html = undefined; - this.$onrender = undefined; - this.style = undefined; - this.value = undefined; - this.children = []; - this.classList = []; - this.eventListeners = {}; - if (args.length === 0) { - throw new Error("[VNode] No arguments passed to VNode constructor."); - } - if (args.length === 1) { - let vnode = args[0]; - if (typeof vnode === "string") { - // Assume empty element - this.processSelector(vnode); - } else if ( - typeof vnode === "function" || - (typeof vnode === "object" && vnode !== null) - ) { - // Text node - if (vnode.type === "#text") { - this.type = "#text"; - this.value = vnode.value; - } else { - this.from(this.processVNodeObject(vnode)); + constructor(...args) { + this.type = undefined; + this.props = {}; + this.data = {}; + this.id = undefined; + this.$html = undefined; + this.$onrender = undefined; + this.style = undefined; + this.value = undefined; + this.children = []; + this.classList = []; + this.eventListeners = {}; + if (args.length === 0) { + throw new Error( + "[VNode] No arguments passed to VNode constructor." + ); } - } else { - throw new Error( - "[VNode] Invalid first argument passed to VNode constructor." - ); - } - } else if (args.length === 2) { - let [selector, data] = args; - if (typeof selector !== "string") { - throw new Error( - "[VNode] Invalid first argument passed to VNode constructor." - ); - } - this.processSelector(selector); - if (typeof data === "string") { - // Assume single child text node - this.children = [new VNode({ type: "#text", value: data })]; - return; - } - if ( - typeof data !== "function" && - (typeof data !== "object" || data === null) - ) { - throw new Error( - "[VNode] The second argument of a VNode constructor must be an object, an array or a string." - ); - } - if (Array.isArray(data)) { - // Assume 2nd argument as children - this.processChildren(data); - } else { - if (data instanceof Function || data instanceof VNode) { - this.processChildren(data); + if (args.length === 1) { + let vnode = args[0]; + if (typeof vnode === "string") { + // Assume empty element + this.processSelector(vnode); + } else if ( + typeof vnode === "function" || + (typeof vnode === "object" && vnode !== null) + ) { + // Text node + if (vnode.type === "#text") { + this.type = "#text"; + this.value = vnode.value; + } else { + this.from(this.processVNodeObject(vnode)); + } + } else { + throw new Error( + "[VNode] Invalid first argument passed to VNode constructor." + ); + } + } else if (args.length === 2) { + let [selector, data] = args; + if (typeof selector !== "string") { + throw new Error( + "[VNode] Invalid first argument passed to VNode constructor." + ); + } + this.processSelector(selector); + if (typeof data === "string") { + // Assume single child text node + this.children = [new VNode({ type: "#text", value: data })]; + return; + } + if ( + typeof data !== "function" && + (typeof data !== "object" || data === null) + ) { + throw new Error( + "[VNode] The second argument of a VNode constructor must be an object, an array or a string." + ); + } + if (Array.isArray(data)) { + // Assume 2nd argument as children + this.processChildren(data); + } else { + if (data instanceof Function || data instanceof VNode) { + this.processChildren(data); + } else { + // Not a VNode, assume props object + this.processProperties(data); + } + } } else { - // Not a VNode, assume props object - this.processProperties(data); + 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 ( + 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.processChildren(children); } - } - } 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 ( - 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.processChildren(children); } - } - from(data) { - this.value = data.value; - this.type = data.type; - this.id = data.id; - this.$html = data.$html; - this.$onrender = data.$onrender; - this.style = data.style; - this.data = data.data; - this.value = data.value; - this.eventListeners = data.eventListeners; - this.children = data.children; - this.props = data.props; - this.classList = data.classList; - } - - equal(a, b) { - return equal(a, b === undefined ? this : b); - } + from(data) { + this.value = data.value; + this.type = data.type; + this.id = data.id; + this.$html = data.$html; + this.$onrender = data.$onrender; + this.style = data.style; + this.data = data.data; + this.value = data.value; + this.eventListeners = data.eventListeners; + this.children = data.children; + this.props = data.props; + this.classList = data.classList; + } - processProperties(attrs) { - this.id = this.id || attrs.id; - this.$html = attrs.$html; - this.$onrender = attrs.$onrender; - this.style = attrs.style; - this.value = attrs.value; - this.data = attrs.data || {}; - this.classList = - attrs.classList && attrs.classList.length > 0 - ? attrs.classList - : this.classList; - this.props = attrs; - Object.keys(attrs) - .filter((a) => a.startsWith("on") && attrs[a]) - .forEach((key) => { - if (typeof attrs[key] !== "function") { - throw new Error( - `[VNode] Event handler specified for ${key} event is not a function.` - ); - } - this.eventListeners[key.slice(2)] = attrs[key]; - delete this.props[key]; - }); - 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) || selector.length === 0) { - throw new Error(`[VNode] Invalid selector: ${selector}`); + equal(a, b) { + return equal(a, b === undefined ? this : b); } - const [, type, id, classes] = selector.match(selectorRegex); - this.type = type; - if (id) { - this.id = id.slice(1); - } - this.classList = (classes && classes.split(".").slice(1)) || []; - } - processVNodeObject(arg) { - if (arg instanceof VNode) { - return arg; - } - if (arg instanceof Function) { - let vnode = arg(); - if (typeof vnode === "string") { - vnode = new VNode({ type: "#text", value: vnode }); - } - if (!(vnode instanceof VNode)) { - throw new Error("[VNode] Function argument does not return a VNode"); - } - return vnode; + processProperties(attrs) { + this.id = this.id || attrs.id; + this.$html = attrs.$html; + this.$onrender = attrs.$onrender; + this.style = attrs.style; + this.value = attrs.value; + this.data = attrs.data || {}; + this.classList = + attrs.classList && attrs.classList.length > 0 + ? attrs.classList + : this.classList; + this.props = attrs; + Object.keys(attrs) + .filter((a) => a.startsWith("on") && attrs[a]) + .forEach((key) => { + if (typeof attrs[key] !== "function") { + throw new Error( + `[VNode] Event handler specified for ${key} event is not a function.` + ); + } + this.eventListeners[key.slice(2)] = attrs[key]; + delete this.props[key]; + }); + 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; } - throw new Error( - "[VNode] Invalid first argument provided to VNode constructor." - ); - } - processChildren(arg) { - const children = Array.isArray(arg) ? arg : [arg]; - this.children = children - .map((c) => { - if (typeof c === "string") { - return new VNode({ type: "#text", value: c }); - } - if (typeof c === "function" || (typeof c === "object" && c !== null)) { - return this.processVNodeObject(c); + processSelector(selector) { + if (!selector.match(selectorRegex) || selector.length === 0) { + throw new Error(`[VNode] Invalid selector: ${selector}`); } - if (c) { - throw new Error(`[VNode] Specified child is not a VNode: ${c}`); + const [, type, id, classes] = selector.match(selectorRegex); + this.type = type; + if (id) { + this.id = id.slice(1); } - }) - .filter((c) => c); - } - - // Renders the actual DOM Node corresponding to the current Virtual Node - render() { - if (this.type === "#text") { - return document.createTextNode(this.value); + this.classList = (classes && classes.split(".").slice(1)) || []; } - const node = document.createElement(this.type); - if (this.id) { - node.id = this.id; - } - Object.keys(this.props).forEach((attr) => { - // Set props (only if non-empty strings) - if (this.props[attr] && typeof this.props[attr] === "string") { - const a = document.createAttribute(attr); - a.value = this.props[attr]; - node.setAttributeNode(a); - } - // Set properties - if (typeof this.props[attr] !== "string" || !node[attr]) { - node[attr] = this.props[attr]; - } - }); - // Event Listeners - Object.keys(this.eventListeners).forEach((event) => { - node.addEventListener(event, this.eventListeners[event]); - }); - // Value - if (this.value) { - node.value = this.value; - } - // Style - if (this.style) { - node.style.cssText = this.style; - } - // Classes - this.classList.forEach((c) => { - node.classList.add(c); - }); - // Data - Object.keys(this.data).forEach((key) => { - node.dataset[key] = this.data[key]; - }); - // Children - this.children.forEach((c) => { - const cnode = c.render(); - node.appendChild(cnode); - c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); - }); - if (this.$html) { - node.innerHTML = this.$html; - } - return node; - } - // Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) - redraw(data) { - let { node, vnode } = data; - const newvnode = vnode; - const oldvnode = this; - if ( - oldvnode.constructor !== newvnode.constructor || - oldvnode.type !== newvnode.type || - (oldvnode.type === newvnode.type && - oldvnode.type === "#text" && - oldvnode !== newvnode) - ) { - const renderedNode = newvnode.render(); - node.parentNode.replaceChild(renderedNode, node); - newvnode.$onrender && newvnode.$onrender(renderedNode); - oldvnode.from(newvnode); - return; - } - // ID - if (oldvnode.id !== newvnode.id) { - node.id = newvnode.id || ""; - oldvnode.id = newvnode.id; - } - // Value - if (oldvnode.value !== newvnode.value) { - node.value = newvnode.value || ""; - oldvnode.value = newvnode.value; - } - // Classes - if (!equal(oldvnode.classList, newvnode.classList)) { - oldvnode.classList.forEach((c) => { - if (!newvnode.classList.includes(c)) { - node.classList.remove(c); + processVNodeObject(arg) { + if (arg instanceof VNode) { + return arg; } - }); - newvnode.classList.forEach((c) => { - if (!oldvnode.classList.includes(c)) { - node.classList.add(c); + if (arg instanceof Function) { + let vnode = arg(); + if (typeof vnode === "string") { + vnode = new VNode({ type: "#text", value: vnode }); + } + if (!(vnode instanceof VNode)) { + throw new Error( + "[VNode] Function argument does not return a VNode" + ); + } + return vnode; } - }); - oldvnode.classList = newvnode.classList; + throw new Error( + "[VNode] Invalid first argument provided to VNode constructor." + ); } - // Style - if (oldvnode.style !== newvnode.style) { - node.style.cssText = newvnode.style || ""; - oldvnode.style = newvnode.style; + + processChildren(arg) { + const children = Array.isArray(arg) ? arg : [arg]; + this.children = children + .map((c) => { + if (typeof c === "string") { + return new VNode({ type: "#text", value: c }); + } + if ( + typeof c === "function" || + (typeof c === "object" && c !== null) + ) { + return this.processVNodeObject(c); + } + if (c) { + throw new Error( + `[VNode] Specified child is not a VNode: ${c}` + ); + } + }) + .filter((c) => c); } - // Data - if (!equal(oldvnode.data, newvnode.data)) { - Object.keys(oldvnode.data).forEach((a) => { - if (!newvnode.data[a]) { - delete node.dataset[a]; - } else if (newvnode.data[a] !== oldvnode.data[a]) { - node.dataset[a] = newvnode.data[a]; + + // Renders the actual DOM Node corresponding to the current Virtual Node + render() { + if (this.type === "#text") { + return document.createTextNode(this.value); } - }); - Object.keys(newvnode.data).forEach((a) => { - if (!oldvnode.data[a]) { - node.dataset[a] = newvnode.data[a]; + const node = document.createElement(this.type); + if (this.id) { + node.id = this.id; } - }); - oldvnode.data = newvnode.data; - } - // props - if (!equal(oldvnode.props, newvnode.props)) { - Object.keys(oldvnode.props).forEach((a) => { - if (newvnode.props[a] === false) { - node[a] = false; + 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 + node[p] = this.props[p]; + }); + // Event Listeners + Object.keys(this.eventListeners).forEach((event) => { + node.addEventListener(event, this.eventListeners[event]); + }); + // Value + if (this.value) { + if (["textarea", "input"].includes(this.type)) { + node.value = this.value || ""; + } else { + node.setAttribute("value", this.value || ""); + } } - if (!newvnode.props[a]) { - node.removeAttribute(a); - } else if ( - newvnode.props[a] && - newvnode.props[a] !== oldvnode.props[a] - ) { - node.setAttribute(a, newvnode.props[a]); + // Style + if (this.style) { + node.style.cssText = this.style; } - }); - Object.keys(newvnode.props).forEach((a) => { - if (!oldvnode.props[a] && newvnode.props[a]) { - node.setAttribute(a, newvnode.props[a]); + // Classes + this.classList.forEach((c) => { + node.classList.add(c); + }); + // Data + Object.keys(this.data).forEach((key) => { + node.dataset[key] = this.data[key]; + }); + // Children + this.children.forEach((c) => { + const cnode = c.render(); + node.appendChild(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); + }); + if (this.$html) { + node.innerHTML = this.$html; } - }); - oldvnode.props = newvnode.props; + return node; } - // Event listeners - if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) { - Object.keys(oldvnode.eventListeners).forEach((a) => { - if (!newvnode.eventListeners[a]) { - node.removeEventListener(a, oldvnode.eventListeners[a]); - } else if ( - !equal(newvnode.eventListeners[a], oldvnode.eventListeners[a]) + + // Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) + redraw(data) { + let { node, vnode } = data; + const newvnode = vnode; + const oldvnode = this; + if ( + oldvnode.constructor !== newvnode.constructor || + oldvnode.type !== newvnode.type || + (oldvnode.type === newvnode.type && + oldvnode.type === "#text" && + oldvnode !== newvnode) ) { - node.removeEventListener(a, oldvnode.eventListeners[a]); - node.addEventListener(a, newvnode.eventListeners[a]); + const renderedNode = newvnode.render(); + node.parentNode.replaceChild(renderedNode, node); + newvnode.$onrender && newvnode.$onrender(renderedNode); + oldvnode.from(newvnode); + return; } - }); - Object.keys(newvnode.eventListeners).forEach((a) => { - if (!oldvnode.eventListeners[a]) { - node.addEventListener(a, newvnode.eventListeners[a]); + // ID + if (oldvnode.id !== newvnode.id) { + node.id = newvnode.id || ""; + oldvnode.id = newvnode.id; } - }); - 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; - } + // Value + if (oldvnode.value !== newvnode.value) { + oldvnode.value = newvnode.value; + if (["textarea", "input"].includes(oldvnode.type)) { + node.value = newvnode.value || ""; + } else { + node.setAttribute("value", newvnode.value || ""); + } } - 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; - } + // Classes + if (!equal(oldvnode.classList, newvnode.classList)) { + oldvnode.classList.forEach((c) => { + if (!newvnode.classList.includes(c)) { + node.classList.remove(c); + } + }); + newvnode.classList.forEach((c) => { + if (!oldvnode.classList.includes(c)) { + node.classList.add(c); + } + }); + oldvnode.classList = newvnode.classList; } - } - 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); - } + // Style + if (oldvnode.style !== newvnode.style) { + node.style.cssText = newvnode.style || ""; + oldvnode.style = newvnode.style; } - } - // 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; - } + // Data + if (!equal(oldvnode.data, newvnode.data)) { + Object.keys(oldvnode.data).forEach((a) => { + if (!newvnode.data[a]) { + delete node.dataset[a]; + } else if (newvnode.data[a] !== oldvnode.data[a]) { + node.dataset[a] = newvnode.data[a]; + } + }); + Object.keys(newvnode.data).forEach((a) => { + if (!oldvnode.data[a]) { + node.dataset[a] = newvnode.data[a]; + } + }); + oldvnode.data = newvnode.data; } - } - return map; - } - let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; - while (!equal(childMap, resultMap)) { - let count = -1; - for (let i of childMap) { - count++; - let breakFor = false; - if (i === count) { - // Matching nodes; - continue; + // 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.props[a] && + newvnode.props[a] !== oldvnode.props[a] + ) { + oldvnode.props[a] = newvnode.props[a]; + if ( + ["string", "number"].includes(typeof newvnode.props[a]) + ) { + node.setAttribute(a, newvnode.props[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]); + } + }); } - switch (i) { - case -1: - // different node, check - oldvnode.children[count].redraw({ - node: node.childNodes[count], - vnode: newvnode.children[count], + // Event listeners + if (!equal(oldvnode.eventListeners, newvnode.eventListeners)) { + Object.keys(oldvnode.eventListeners).forEach((a) => { + if (!newvnode.eventListeners[a]) { + node.removeEventListener(a, oldvnode.eventListeners[a]); + } else if ( + !equal( + newvnode.eventListeners[a], + oldvnode.eventListeners[a] + ) + ) { + node.removeEventListener(a, oldvnode.eventListeners[a]); + node.addEventListener(a, newvnode.eventListeners[a]); + } }); - break; - case -2: - // add node - 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 - oldvnode.children.splice(count, 1); - node.removeChild(node.childNodes[count]); - breakFor = true; - break; - default: - // Node found, move nodes and remap - 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; + Object.keys(newvnode.eventListeners).forEach((a) => { + if (!oldvnode.eventListeners[a]) { + node.addEventListener(a, newvnode.eventListeners[a]); + } + }); + oldvnode.eventListeners = newvnode.eventListeners; + } + // Children + let childMap = mapChildren(oldvnode, newvnode); + let resultMap = [...Array(newvnode.children.length).keys()]; + while (!equal(childMap, resultMap)) { + let count = -1; + checkmap: for (const i of childMap) { + count++; + if (i === count) { + // Matching nodes; + continue; + } + switch (i) { + case PATCH: { + oldvnode.children[count].redraw({ + node: node.childNodes[count], + vnode: newvnode.children[count], + }); + 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); + break checkmap; + } + case DELETE: { + oldvnode.children.splice(count, 1); + node.removeChild(node.childNodes[count]); + break checkmap; + } + default: { + const vtarget = oldvnode.children.splice(i, 1)[0]; + oldvnode.children.splice(count, 0, vtarget); + const target = node.removeChild(node.childNodes[i]); + node.insertBefore(target, node.childNodes[count]); + break checkmap; + } + } + } + childMap = mapChildren(oldvnode, newvnode); + resultMap = [...Array(newvnode.children.length).keys()]; + } + // $onrender + if (!equal(oldvnode.$onrender, newvnode.$onrender)) { + oldvnode.$onrender = newvnode.$onrender; } - if (breakFor) { - break; + // innerHTML + if (oldvnode.$html !== newvnode.$html) { + node.innerHTML = newvnode.$html; + oldvnode.$html = newvnode.$html; + oldvnode.$onrender && oldvnode.$onrender(node); } - } - childMap = mapChildren(oldvnode, newvnode); - resultMap = [...Array(childMap.length).keys()]; } - // $onrender - if (!equal(oldvnode.$onrender, newvnode.$onrender)) { - oldvnode.$onrender = newvnode.$onrender; - } - // innerHTML - if (oldvnode.$html !== newvnode.$html) { - node.innerHTML = newvnode.$html; - oldvnode.$html = newvnode.$html; - oldvnode.$onrender && oldvnode.$onrender(node); - } - } } /**@@ -548,248 +546,256 @@ * <https://github.com/storeon/storeon/blob/master/LICENSE>
* Copyright 2019 Andrey Sitnik <andrey@sitnik.ru> */ class Store { - constructor() { - this.events = {}; - this.state = {}; - } - dispatch(event, data) { - if (event !== "$log") this.dispatch("$log", { event, data }); - if (this.events[event]) { - let changes = {}; - let changed; - this.events[event].forEach((i) => { - this.state = { ...this.state, ...i(this.state, data) }; - }); + constructor() { + this.events = {}; + this.state = {}; + } + dispatch(event, data) { + if (event !== "$log") this.dispatch("$log", { event, data }); + if (this.events[event]) { + let changes = {}; + let changed; + this.events[event].forEach((i) => { + this.state = { ...this.state, ...i(this.state, data) }; + }); + } } - } - on(event, cb) { - (this.events[event] || (this.events[event] = [])).push(cb); + on(event, cb) { + (this.events[event] || (this.events[event] = [])).push(cb); - return () => { - this.events[event] = this.events[event].filter((i) => i !== cb); - }; - } + return () => { + this.events[event] = this.events[event].filter((i) => i !== cb); + }; + } } class Route { - constructor({ path, def, query, parts }) { - this.path = path; - this.def = def; - this.query = query; - this.parts = parts; - this.params = {}; - if (this.query) { - const rawParams = this.query.split("&"); - rawParams.forEach((p) => { - const [name, value] = p.split("="); - this.params[decodeURIComponent(name)] = decodeURIComponent(value); - }); + constructor({ path, def, query, parts }) { + this.path = path; + this.def = def; + this.query = query; + this.parts = parts; + this.params = {}; + if (this.query) { + const rawParams = this.query.split("&"); + rawParams.forEach((p) => { + const [name, value] = p.split("="); + this.params[decodeURIComponent(name)] = decodeURIComponent( + value + ); + }); + } } - } } class Router { - constructor({ element, routes, store, location }) { - this.element = element; - this.redraw = null; - this.store = store; - this.location = location || window.location; - if (!routes || Object.keys(routes).length === 0) { - throw new Error("[Router] No routes defined."); + constructor({ element, routes, store, location }) { + this.element = element; + this.redraw = null; + this.store = store; + this.location = location || window.location; + if (!routes || Object.keys(routes).length === 0) { + throw new Error("[Router] No routes defined."); + } + const defs = Object.keys(routes); + this.routes = routes; } - const defs = Object.keys(routes); - this.routes = routes; - } - setRedraw(vnode, state) { - this.redraw = () => { - vnode.redraw({ - node: this.element.childNodes[0], - vnode: this.routes[this.route.def](state), - }); - this.store.dispatch("$redraw"); - }; - } + setRedraw(vnode, state) { + this.redraw = () => { + vnode.redraw({ + node: this.element.childNodes[0], + vnode: this.routes[this.route.def](state), + }); + this.store.dispatch("$redraw"); + }; + } - async start() { - const processPath = async (data) => { - const oldRoute = this.route; - const fragment = - (data && - data.newURL && - data.newURL.match(/(#.+)$/) && - data.newURL.match(/(#.+)$/)[1]) || - this.location.hash; - const path = fragment.replace(/\?.+$/, "").slice(1); - const rawQuery = fragment.match(/\?(.+)$/); - const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; - const pathParts = path.split("/").slice(1); + async start() { + const processPath = async (data) => { + const oldRoute = this.route; + const fragment = + (data && + data.newURL && + data.newURL.match(/(#.+)$/) && + data.newURL.match(/(#.+)$/)[1]) || + this.location.hash; + const path = fragment.replace(/\?.+$/, "").slice(1); + const rawQuery = fragment.match(/\?(.+)$/); + const query = rawQuery && rawQuery[1] ? rawQuery[1] : ""; + const pathParts = path.split("/").slice(1); - let parts = {}; - for (let def of Object.keys(this.routes)) { - let routeParts = def.split("/").slice(1); - let match = true; - let index = 0; - parts = {}; - while (match && routeParts[index]) { - const rP = routeParts[index]; - const pP = pathParts[index]; - if (rP.startsWith(":") && pP) { - parts[rP.slice(1)] = pP; - } else { - match = rP === pP; - } - index++; - } - if (match) { - this.route = new Route({ query, path, def, parts }); - break; - } - } - if (!this.route) { - throw new Error(`[Router] No route matches '${fragment}'`); - } - // Old route component teardown - if (oldRoute) { - const oldRouteComponent = this.routes[oldRoute.def]; - oldRouteComponent.state = - oldRouteComponent.teardown && - (await oldRouteComponent.teardown(oldRouteComponent.state)); - } - // New route component setup - const newRouteComponent = this.routes[this.route.def]; - newRouteComponent.state = {}; - newRouteComponent.setup && - (await newRouteComponent.setup(newRouteComponent.state)); - // Redrawing... - redrawing = true; - this.store.dispatch("$navigation", this.route); - while (this.element.firstChild) { - this.element.removeChild(this.element.firstChild); - } - const vnode = newRouteComponent(newRouteComponent.state); - const node = vnode.render(); - this.element.appendChild(node); - this.setRedraw(vnode, newRouteComponent.state); - redrawing = false; - vnode.$onrender && vnode.$onrender(node); - $onrenderCallbacks.forEach((cbk) => cbk()); - $onrenderCallbacks = []; - window.scrollTo(0, 0); - this.store.dispatch("$redraw"); - }; - window.addEventListener("hashchange", processPath); - await processPath(); - } + let parts = {}; + for (let def of Object.keys(this.routes)) { + let routeParts = def.split("/").slice(1); + let match = true; + let index = 0; + parts = {}; + while (match && routeParts[index]) { + const rP = routeParts[index]; + const pP = pathParts[index]; + if (rP.startsWith(":") && pP) { + parts[rP.slice(1)] = pP; + } else { + match = rP === pP; + } + index++; + } + if (match) { + this.route = new Route({ query, path, def, parts }); + break; + } + } + if (!this.route) { + throw new Error(`[Router] No route matches '${fragment}'`); + } + // Old route component teardown + if (oldRoute) { + const oldRouteComponent = this.routes[oldRoute.def]; + oldRouteComponent.state = + oldRouteComponent.teardown && + (await oldRouteComponent.teardown(oldRouteComponent.state)); + } + // New route component setup + const newRouteComponent = this.routes[this.route.def]; + newRouteComponent.state = {}; + newRouteComponent.setup && + (await newRouteComponent.setup(newRouteComponent.state)); + // Redrawing... + redrawing = true; + this.store.dispatch("$navigation", this.route); + while (this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } + const vnode = newRouteComponent(newRouteComponent.state); + const node = vnode.render(); + this.element.appendChild(node); + this.setRedraw(vnode, newRouteComponent.state); + redrawing = false; + vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; + window.scrollTo(0, 0); + this.store.dispatch("$redraw"); + }; + window.addEventListener("hashchange", processPath); + await processPath(); + } - navigateTo(path, params) { - let query = Object.keys(params || {}) - .map((p) => `${encodeURIComponent(p)}=${encodeURIComponent(params[p])}`) - .join("&"); - query = query ? `?${query}` : ""; - this.location.hash = `#${path}${query}`; - } + navigateTo(path, params) { + let query = Object.keys(params || {}) + .map( + (p) => + `${encodeURIComponent(p)}=${encodeURIComponent(params[p])}` + ) + .join("&"); + query = query ? `?${query}` : ""; + this.location.hash = `#${path}${query}`; + } } // High Level API -const h3 = (...args) => { - return new VNode(...args); + +export const h = (...args) => { + return new VNode(...args); }; + +export const h3 = {}; let store = null; let router = null; let redrawing = false; h3.init = (config) => { - let { element, routes, modules, preStart, postStart, location } = config; - if (!routes) { - // Assume config is a component object, define default route - if (typeof config !== "function") { - throw new Error( - "[h3.init] The specified argument is not a valid configuration object or component function" - ); + let { element, routes, modules, preStart, postStart, location } = config; + if (!routes) { + // Assume config is a component object, define default route + if (typeof config !== "function") { + throw new Error( + "[h3.init] The specified argument is not a valid configuration object or component function" + ); + } + routes = { "/": config }; + } + element = element || document.body; + if (!(element && element instanceof Element)) { + throw new Error("[h3.init] Invalid element specified."); } - routes = { "/": config }; - } - element = element || document.body; - if (!(element && element instanceof Element)) { - throw new Error("[h3.init] Invalid element specified."); - } - // Initialize store - store = new Store(); - (modules || []).forEach((i) => { - i(store); - }); - store.dispatch("$init"); - // Initialize router - router = new Router({ element, routes, store, location }); - return Promise.resolve(preStart && preStart()) - .then(() => router.start()) - .then(() => postStart && postStart()); + // Initialize store + store = new Store(); + (modules || []).forEach((i) => { + i(store); + }); + store.dispatch("$init"); + // Initialize router + router = new Router({ element, routes, store, location }); + return Promise.resolve(preStart && preStart()) + .then(() => router.start()) + .then(() => postStart && postStart()); }; h3.navigateTo = (path, params) => { - if (!router) { - throw new Error( - "[h3.navigateTo] No application initialized, unable to navigate." - ); - } - return router.navigateTo(path, params); + if (!router) { + throw new Error( + "[h3.navigateTo] No application initialized, unable to navigate." + ); + } + return router.navigateTo(path, params); }; Object.defineProperty(h3, "route", { - get: () => { - if (!router) { - throw new Error( - "[h3.route] No application initialized, unable to retrieve current route." - ); - } - return router.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; - }, + get: () => { + if (!store) { + throw new Error( + "[h3.state] No application initialized, unable to retrieve current state." + ); + } + return store.state; + }, }); h3.on = (event, cb) => { - if (!store) { - throw new Error( - "[h3.on] No application initialized, unable to listen to events." - ); - } - return store.on(event, cb); + if (!store) { + throw new Error( + "[h3.on] No application initialized, unable to listen to events." + ); + } + return store.on(event, cb); }; h3.dispatch = (event, data) => { - if (!store) { - throw new Error( - "[h3.dispatch] No application initialized, unable to dispatch events." - ); - } - return store.dispatch(event, data); + if (!store) { + throw new Error( + "[h3.dispatch] No application initialized, unable to dispatch events." + ); + } + return store.dispatch(event, data); }; h3.redraw = (setRedrawing) => { - if (!router || !router.redraw) { - throw new Error( - "[h3.redraw] No application initialized, unable to redraw." - ); - } - if (redrawing) { - return; - } - redrawing = true; - router.redraw(); - redrawing = setRedrawing || false; + if (!router || !router.redraw) { + throw new Error( + "[h3.redraw] No application initialized, unable to redraw." + ); + } + if (redrawing) { + return; + } + redrawing = true; + router.redraw(); + redrawing = setRedrawing || false; }; export default h3;
M
h3.js.map
→
h3.js.map
@@ -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","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","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","mapChildren","oldvnode","newvnode","newList","children","oldList","map","nIdx","op","oIdx","push","keys","forEach","VNode","[object Object]","args","this","type","props","data","id","$html","$onrender","style","value","classList","eventListeners","Error","vnode","processSelector","from","processVNodeObject","selector","isArray","Function","processChildren","processProperties","slice","concat","a","b","attrs","Object","filter","startsWith","match","classes","split","arg","c","document","createTextNode","node","createElement","p","setAttribute","removeAttribute","event","addEventListener","cssText","add","dataset","cnode","render","appendChild","innerHTML","renderedNode","parentNode","replaceChild","remove","removeEventListener","childMap","resultMap","count","checkmap","redraw","childNodes","splice","insertBefore","removeChild","vtarget","target","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"],"mappings":"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","file":"h3.js"}
M
h3.min.js
→
h3.min.js
@@ -5,5 +5,5 @@ *
* @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.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=>{if(this.props[t]&&"string"==typeof this.props[t]){const r=document.createAttribute(t);r.value=this.props[t],e.setAttributeNode(r)}"string"==typeof this.props[t]&&e[t]||(e[t]=this.props[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 o(e,t){let r=[],s=0,i=0;for(let o=0;o<e.children.length;o++){let n=-1;for(let a=0;a<t.children.length;a++)if(equal(e.children[o],t.children[a])&&!r.includes(a)){n=a,i++,s++;break}r.push(n)}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.props,s.props)||(Object.keys(i.props).forEach(e=>{!1===s.props[e]&&(t[e]=!1),s.props[e]?s.props[e]&&s.props[e]!==i.props[e]&&t.setAttribute(e,s.props[e]):t.removeAttribute(e)}),Object.keys(s.props).forEach(e=>{!i.props[e]&&s.props[e]&&t.setAttribute(e,s.props[e])}),i.props=s.props),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 n=o(i,s),a=[...Array(n.filter(e=>-3!==e).length).keys()];for(;!equal(n,a);){let e=-1;for(let r of n){e++;let o=!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 n=s.children[e].render();t.insertBefore(n,t.childNodes[e]),s.children[e].$onrender&&s.children[e].$onrender(n),o=!0;break;case-3:i.children.splice(e,1),t.removeChild(t.childNodes[e]),o=!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]),o=!0}if(o)break}}n=o(i,s),a=[...Array(n.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(/\?(.+)$/),o=i&&i[1]?i[1]:"",n=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=n[i];e.startsWith(":")&&s?a[e.slice(1)]=s:r=e===s,i++}if(r){this.route=new Route({query:o,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:o,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(i&&i()).then(()=>router.start()).then(()=>o&&o())},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=[];const mapChildren=(e,t)=>{const r=t.children,s=e.children,i=[];for(let e=0;e<r.length;e++){let t=PATCH;for(let o=0;o<s.length;o++)if(equal(r[e],s[o])&&!i.includes(o)){t=o;break}t<0&&r.length>=s.length&&i.length>=s.length&&(t=INSERT),i.push(t)}return s.length>r.length&&[...Array(s.length-r.length).keys()].forEach(()=>i.push(DELETE)),i};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,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)}i.id!==s.id&&(t.id=s.id||"",i.id=s.id),i.value!==s.value&&(i.value=s.value,["textarea","input"].includes(i.type)?t.value=s.value||"":t.setAttribute("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.props,s.props)||(Object.keys(i.props).forEach(e=>{t[e]=s.props[e],"boolean"==typeof s.props[e]?(i.props[e]=s.props[e],s.props[e]?t.setAttribute(e,""):t.removeAttribute(e)):s.props[e]?s.props[e]&&s.props[e]!==i.props[e]&&(i.props[e]=s.props[e],["string","number"].includes(typeof s.props[e])&&t.setAttribute(e,s.props[e])):(delete i.props[e],t.removeAttribute(e))}),Object.keys(s.props).forEach(e=>{!i.props[e]&&s.props[e]&&(i.props[e]=s.props[e],t.setAttribute(e,s.props[e]))})),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=mapChildren(i,s),n=[...Array(s.children.length).keys()];for(;!equal(o,n);){let e=-1;e:for(const r of o)if(e++,r!==e)switch(r){case PATCH:i.children[e].redraw({node:t.childNodes[e],vnode:s.children[e]});break e;case INSERT:{i.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:i.children.splice(e,1),t.removeChild(t.childNodes[e]);break e;default:{const s=i.children.splice(r,1)[0];i.children.splice(e,0,s);const o=t.removeChild(t.childNodes[r]);t.insertBefore(o,t.childNodes[e]);break e}}o=mapChildren(i,s),n=[...Array(s.children.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(/\?(.+)$/),o=i&&i[1]?i[1]:"",n=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=n[i];e.startsWith(":")&&s?a[e.slice(1)]=s:r=e===s,i++}if(r){this.route=new Route({query:o,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}`}}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:i,postStart:o,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(i&&i()).then(()=>router.start()).then(()=>o&&o())},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; //# sourceMappingURL=h3.js.map