all repos — h3 @ 527d5b954cb18fe939fdf92e3263961c8538fe62

A tiny, extremely minimalist JavaScript microframework.

100% coverage.
h3rald h3rald@h3rald.com
Sat, 23 May 2020 18:28:01 +0200
commit

527d5b954cb18fe939fdf92e3263961c8538fe62

parent

08299339a5550cf4b412d579e8adcb644528921f

4 files changed, 120 insertions(+), 56 deletions(-)

jump to
M __tests__/h3.js__tests__/h3.js

@@ -409,5 +409,9 @@ 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); }); });
M __tests__/router.js__tests__/router.js

@@ -2,7 +2,13 @@ const h3 = require("../h3.js").default;

let preStartCalled = false; let postStartCalled = false; +let count = 0; +let result = 0; +const setCount = () => { + count = count + 2; + h3.dispatch("count/set", count); +}; let hash = "#/c2"; const mockLocation = { get hash() {

@@ -16,27 +22,30 @@ hash = value;

window.dispatchEvent(event); }, }; +const $onrender = (node) => { + node.classList.add("test"); +}; +const C1 = () => { + const parts = h3.route.parts; + const content = Object.keys(parts).map((key) => + h3("li", `${key}: ${parts[key]}`) + ); + return h3("ul.c1", { $onrender }, content); +}; + +const C2 = () => { + const params = h3.route.params; + const content = Object.keys(params).map((key) => + h3("li", `${key}: ${params[key]}`) + ); + return h3("ul.c2", { $onrender }, content); +}; describe("h3 (Router)", () => { beforeEach(async () => { const preStart = () => (preStartCalled = true); const postStart = () => (postStartCalled = true); - const C1 = () => { - const $onrender = (node) => node.classList.add("test"); - const parts = h3.route.parts; - const content = Object.keys(parts).map((key) => - h3("li", `${key}: ${parts[key]}`) - ); - return h3("ul.c1", { $onrender }, 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); - }; - return await h3.init({ + await h3.init({ routes: { "/c1/:a/:b/:c": C1, "/c2": C2,

@@ -56,34 +65,104 @@ expect(preStartCalled).toEqual(true);

expect(postStartCalled).toEqual(true); }); - it("should support the capturing of parts within the current route", () => { + 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" + ); + expect(document.body.childNodes[0].classList.contains("test")).toEqual( + true + ); + sub(); + done(); + }); mockLocation.hash = "#/c1/1/2/3"; - expect(document.body.childNodes[0].childNodes[1].textContent).toEqual( - "b: 2" - ); - expect(document.body.childNodes[0].classList.contains("test")).toEqual( - true - ); }); - it("should expose a navigateTo method to navigate to another path", () => { + 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 }); - expect(document.body.childNodes[0].childNodes[1].textContent).toEqual( - "test2: 2" - ); - h3.navigateTo("/c2"); - expect(document.body.childNodes[0].innerHTML).toEqual(""); }); - it("should fail if no route matches at startup", async () => { - mockLocation.hash = "#/test"; + it("should throw an error if no route matches", async () => { try { await h3.init({ element: document.body, - routes: { "/aaa": () => false }, + routes: { + "/c1/:a/:b/:c": () => h3("div"), + "/c2": () => h3("div"), + }, }); } catch (e) { expect(e.message).toMatch(/No route matches/); } + }); + + it("should execute $onrender callback after each navigation", async () => { + let executions = []; + let count = 0; + const c = () => { + return h3( + "div", + { + $onrender: (node) => { + executions.push("test1"); + }, + }, + [ + h3("span", { + $onrender: (node) => { + executions.push("test2"); + }, + }), + ] + ); + }; + expect(executions).toEqual([]); + await h3.init({ + element: document.body, + routes: { "/": c }, + }); + expect(executions).toEqual(["test2", "test1"]); + }); + + 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"); }); });
M __tests__/vnode.js__tests__/vnode.js

@@ -68,19 +68,6 @@ expect(node.childNodes[1].constructor).toEqual(HTMLLIElement);

expect(node.childNodes[0].childNodes[0].data).toEqual("test1"); }); - it("should provide an $onrender special attribute to execute code after the vnode is first rendered", () => { - const addClass = (node) => node.classList.add(node.tagName); - const vnode = h3("div", h3("span", {$onrender: addClass})); - let node = vnode.render(); - expect(node.childNodes[0].classList.contains("SPAN")).toEqual(true); - const vnode2 = h3("div", h3("div", {$onrender: addClass})); - vnode.redraw({node, vnode: vnode2}); - expect(node.childNodes[0].classList.contains("DIV")).toEqual(true); - const vnode3 = h3("div", [h3("div", {$onrender: addClass}), h3("h1", {$onrender: addClass})]); - vnode.redraw({node, vnode: vnode3}); - expect(node.childNodes[1].classList.contains("H1")).toEqual(true); - }); - it("should handle boolean attributes when redrawing", () => { const vnode1 = h3("input", { type: "checkbox", checked: true }); const node = vnode1.render();
M h3.jsh3.js

@@ -302,11 +302,11 @@ // 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; } + this.$onrender && $onrenderCallbacks.push(() => this.$onrender(node)); return node; }

@@ -324,8 +324,6 @@ oldvnode !== newvnode)

) { const renderedNode = newvnode.render(); node.parentNode.replaceChild(renderedNode, node); - newvnode.$onrender && - $onrenderCallbacks.push(() => newvnode.$onrender(renderedNode)); oldvnode.from(newvnode); return; }

@@ -477,10 +475,6 @@ } else {

// While there are children not found in oldvnode, add them and re-check const cnode = newvnode.children[notFoundInOld].render(); node.insertBefore(cnode, node.childNodes[notFoundInOld]); - newvnode.children[notFoundInOld].$onrender && - $onrenderCallbacks.push(() => - newvnode.children[notFoundInOld].$onrender(cnode) - ); oldvnode.children.splice( notFoundInOld, 0,

@@ -572,7 +566,7 @@ this.store.dispatch("$redraw");

}; } - start() { + async start() { const processPath = async (data) => { const oldRoute = this.route; const fragment =

@@ -585,6 +579,7 @@ 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);

@@ -630,7 +625,6 @@ }

const vnode = newRouteComponent(newRouteComponent.state); const node = vnode.render(); this.element.appendChild(node); - vnode.$onrender && $onrenderCallbacks.push(() => vnode.$onrender(node)); $onrenderCallbacks.forEach((cbk) => cbk()); $onrenderCallbacks = []; this.setRedraw(vnode, newRouteComponent.state);

@@ -638,8 +632,8 @@ window.scrollTo(0, 0);

this.store.dispatch("$redraw"); redrawing = false; }; - processPath(); window.addEventListener("hashchange", processPath); + await processPath(); } navigateTo(path, params) {

@@ -740,7 +734,7 @@

h3.redraw = (setRedrawing) => { if (!router || !router.redraw) { throw new Error( - "[h3.redraw] No application initialized, unable to update." + "[h3.redraw] No application initialized, unable to redraw." ); } if (redrawing) {