all repos — h3 @ ccd03b9efd8c0277d5c1ad666ad405bb5ca22479

A tiny, extremely minimalist JavaScript microframework.

Added tests.
h3rald h3rald@h3rald.com
Mon, 20 Apr 2020 17:43:25 +0200
commit

ccd03b9efd8c0277d5c1ad666ad405bb5ca22479

parent

73e9858ddad97a3aa317aff6a7ed0b1fe30e366f

5 files changed, 373 insertions(+), 430 deletions(-)

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

@@ -1,8 +1,6 @@

-// TODO: Rewrite const h3 = require("../h3.js").default; describe("h3", () => { - it("should expose an equal method to check object/array/function equality", () => { expect(h3.equal({}, {})).toBeTruthy(); expect(h3.equal([], [])).toBeTruthy();

@@ -83,7 +81,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, value: undefined, });

@@ -94,16 +93,22 @@ 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]); 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/); }); it("should support the creation of elements with a single, non-array child", () => {

@@ -147,7 +152,9 @@ expect(failing).toThrowError(/Invalid selector/);

}); it("should support the creation of virtual node elements with classes", () => { - expect(h3("div.a.b.c")).toEqual({ + const a = h3("div.a.b.c"); + const b = h3("div", { classList: ["a", "b", "c"] }); + expect(a).toEqual({ type: "div", children: [], attributes: {},

@@ -155,11 +162,13 @@ classList: ["a", "b", "c"],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "div", value: undefined, }); + expect(a).toEqual(b); }); it("should support the creation of virtual node elements with attributes and classes", () => {

@@ -171,7 +180,8 @@ data: {},

attributes: {}, eventListeners: {}, id: "test", - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "div", value: undefined,

@@ -189,7 +199,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "#text", value: "a",

@@ -201,7 +212,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "#text", value: "b",

@@ -212,7 +224,8 @@ classList: ["test"],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, value: undefined, });

@@ -231,7 +244,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "#text", value: "a",

@@ -243,7 +257,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "#text", value: "b",

@@ -252,7 +267,8 @@ ],

data: {}, eventListeners: {}, id: "test", - key: undefined, + $key: undefined, + $html: undefined, style: undefined, value: undefined, attributes: { title: "Test..." },

@@ -267,7 +283,8 @@ children: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, value: "AAA", attributes: { type: "text" },

@@ -275,6 +292,28 @@ classList: [],

}); }); + 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, + $key: undefined, + $html: undefined, + style: undefined, + value: undefined, + attributes: {}, + 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"])])

@@ -289,7 +328,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "#text", value: "a",

@@ -304,7 +344,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "#text", value: "test1",

@@ -315,7 +356,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, value: undefined, },

@@ -329,7 +371,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, type: "#text", value: "test2",

@@ -340,7 +383,8 @@ classList: [],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, value: undefined, },

@@ -349,373 +393,34 @@ classList: ["test"],

data: {}, eventListeners: {}, id: undefined, - key: undefined, + $key: undefined, + $html: undefined, style: undefined, value: undefined, }); }); -}); -describe("VNode", () => { - it("should initialize itself based on another node or a function returning a VNode", () => { - const a = h3("div#test", ["a", "b"]); - const b = h3("span"); - const c = h3(a); - const d = () => h3(a); - b.from(a); - expect(b).toEqual(a); - expect(c).toEqual(a); - expect(h3(d)).toEqual(a); - }); - - it("should provide a method to set properties", () => { - const a = h3("div"); - const failing = () => - a.setProps({ id: "test", key: "aaa", onclick: "test" }); - expect(failing).toThrowError(/not a function/); - const handler = () => "test"; - a.setProps({ id: "test", key: "aaa", onclick: handler }); - expect(a.key).toEqual("aaa"); - expect(a.id).toEqual("test"); - expect(a.eventListeners.click).toEqual(handler); - }); - - it("should provide a render method able to render textual nodes", () => { - const createTextNode = jest.spyOn(document, "createTextNode"); - const node = h3.render("test"); - expect(createTextNode).toHaveBeenCalledWith("test"); - expect(node.constructor).toEqual(Text); - }); - - it("should provide a render method able to render simple element nodes", () => { - const vnode = { - type: "br", - children: [], - attributes: {}, - classList: [], - }; - const createElement = jest.spyOn(document, "createElement"); - const node = h3.render(vnode); - expect(createElement).toHaveBeenCalledWith("br"); - expect(node.constructor).toEqual(HTMLBRElement); - }); - - it("should provide a render method able to render element nodes with attributes and classes", () => { - const vnode = { - type: "span", - children: [], - attributes: { title: "test" }, - classList: ["test1", "test2"], - }; - const createElement = jest.spyOn(document, "createElement"); - const node = h3.render(vnode); - 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 = { - type: "ul", - children: [ - { - type: "li", - children: ["test1"], - attributes: {}, - classList: [], - }, - { - type: "li", - children: ["test2"], - attributes: {}, - classList: [], - }, - ], - attributes: {}, - classList: [], - }; - const createElement = jest.spyOn(document, "createElement"); - const node = h3.render(vnode); - 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 a value", () => { - const vnode = { - type: "input", - children: [], - attributes: { value: "test" }, - classList: [], - }; - const createElement = jest.spyOn(document, "createElement"); - const node = h3.render(vnode); - 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 = { - type: "button", - children: [], - attributes: { onclick: handler }, - classList: [], - }; - const button = document.createElement("button"); - const createElement = jest - .spyOn(document, "createElement") - .mockImplementationOnce(() => { - return button; - }); - const addEventListener = jest.spyOn(button, "addEventListener"); - const node = h3.render(vnode); - expect(createElement).toHaveBeenCalledWith("button"); - expect(node.constructor).toEqual(HTMLButtonElement); - expect(addEventListener).toHaveBeenCalledWith("click", handler); + 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 a redraw method that is able to add new DOM nodes", () => { - const oldvnode = { - type: "div", - children: [ - { - type: "span", - children: [], - attributes: {}, - classList: [], - }, - ], - attributes: {}, - classList: [], - }; - const newvnode = { - type: "div", - children: [ - { - type: "span", - children: [], - attributes: { id: "a" }, - classList: [], - }, - { - type: "span", - children: [], - attributes: {}, - classList: [], - }, - ], - attributes: {}, - classList: [], - }; - const node = h3.render(oldvnode); - const span = node.childNodes[0]; - h3.redraw(node, newvnode, oldvnode); - 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", () => { - const newvnode = { - type: "div", - children: [ - { - type: "span", - children: [], - attributes: {}, - classList: [], - }, - ], - attributes: {}, - classList: [], - }; - const oldvnode = { - type: "div", - children: [ - { - type: "span", - children: [], - attributes: { id: "a" }, - classList: [], - }, - { - type: "span", - children: [], - attributes: {}, - classList: [], - }, - ], - attributes: {}, - classList: [], - }; - const node = h3.render(oldvnode); - const span = node.childNodes[1]; - h3.redraw(node, newvnode, oldvnode); - expect(oldvnode).toEqual(newvnode); - expect(oldvnode.children.length).toEqual(1); - expect(node.childNodes.length).toEqual(1); - expect(span).toEqual(node.childNodes[0]); - }); - - it("should provide a redraw method that is able to update different attributes", () => { - const oldvnode = { - type: "span", - children: [], - attributes: { title: "a", something: "b" }, - classList: [], - }; - const newvnode = { - type: "span", - children: [], - attributes: { title: "b", id: "bbb" }, - classList: [], - }; - const node = h3.render(oldvnode); - h3.redraw(node, newvnode, oldvnode); - 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 = { - type: "span", - children: [], - attributes: { title: "b" }, - classList: ["a", "b"], - }; - const newvnode = { - type: "span", - children: [], - attributes: { title: "b" }, - classList: ["c"], - }; - const node = h3.render(oldvnode); - h3.redraw(node, newvnode, oldvnode); - expect(oldvnode).toEqual(newvnode); - expect(node.classList.value).toEqual("c"); - }); - - it("should provide diff method to detect changed nodes if they have different elements", () => { - let oldvnode = { - type: "span", - children: [], - attributes: { title: "b" }, - classList: ["c"], - }; - const newvnode = { - type: "div", - children: [], - attributes: { title: "baaab" }, - classList: ["c"], - }; - const container = document.createElement("div"); - const node = h3.render(oldvnode); - container.appendChild(node); - h3.redraw(node, newvnode, oldvnode); - expect(node).not.toEqual(container.childNodes[0]); - expect(node.constructor).toEqual(HTMLSpanElement); - expect(container.childNodes[0].constructor).toEqual(HTMLDivElement); - }); - - it("should provide diff method to detect changed nodes if they have different node types", () => { - let oldvnode = { - type: "span", - children: [], - attributes: { title: "b" }, - classList: ["c"], - }; - const newvnode = "test"; - const container = document.createElement("div"); - const node = h3.render(oldvnode); - container.appendChild(node); - expect(node.constructor).toEqual(HTMLSpanElement); - h3.redraw(node, newvnode, oldvnode); - expect(node).not.toEqual(container.childNodes[0]); - expect(container.childNodes[0].data).toEqual("test"); - }); - - it("should provide diff method to detect changed nodes if they have different text", () => { - const oldvnode = "test1"; - const newvnode = "test2"; - const container = document.createElement("div"); - const node = h3.render(oldvnode); - container.appendChild(node); - expect(node.data).toEqual("test1"); - h3.redraw(node, newvnode, oldvnode); - expect(container.childNodes[0].data).toEqual("test2"); - }); - - it("should provide diff method to detect changed nodes and recurse", () => { - const oldvnode = { - type: "ul", - children: [ - { - type: "li", - attributes: { id: "aaa" }, - children: [], - classList: [], - }, - { - type: "li", - attributes: { id: "bbb" }, - children: [], - classList: [], - }, - { - type: "li", - attributes: { id: "ccc" }, - children: [], - classList: [], - }, - ], - attributes: { title: "b" }, - classList: ["c"], - }; - const newvnode = { - type: "ul", - children: [ - { - type: "li", - attributes: { id: "aaa" }, - children: [], - classList: [], - }, - { - type: "li", - attributes: { id: "ccc" }, - children: [], - classList: [], - }, - ], - attributes: { title: "b" }, - classList: ["c"], - }; - const node = h3.render(oldvnode); - h3.redraw(node, newvnode, oldvnode); - 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 a component method to instantiate components", function () { - const c = { - view: function (h3, data) { - return h3.equal(1, data); - }, - }; - expect(h3.component(c)(1)).toEqual(true); + 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!"); }); });
A __tests__/vnode.js

@@ -0,0 +1,259 @@

+const h3 = require("../h3.js").default; + +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", + $key: "123", + $html: "", + data: { a: "1", b: "2" }, + eventListeners: { click: fn }, + children: [], + attributes: { title: "test" }, + classList: ["a1", "a2"], + style: "padding: 2px", + }; + const vnode1 = h3("br"); + vnode1.from(obj); + const vnode2 = h3("input#test.a1.a2", { + value: "AAA", + $key: "123", + $html: "", + data: { a: "1", b: "2" }, + onclick: fn, + title: "test", + style: "padding: 2px", + }); + 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 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 element nodes with attributes and classes", () => { + const createElement = jest.spyOn(document, "createElement"); + const vnode = h3("span.test1.test2", { title: "test" }); + 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 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 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("it should provide a render method able to render elements with special attributes", () => { + const vnode = h3("div", { + id: "test", + style: "margin: auto;", + data: { test: "aaa" }, + $html: "<p>Hello!</p>", + }); + 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", h3("span")); + const newvnode = h3("div", [h3("span#a"), h3("span")]); + const node = oldvnode.render(); + const span = node.childNodes[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", () => { + const newvnode = h3("div", [h3("span")]); + const oldvnode = h3("div", [h3("span#a"), h3("span")]); + const node = oldvnode.render(); + const span = node.childNodes[1]; + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(oldvnode.children.length).toEqual(1); + expect(node.childNodes.length).toEqual(1); + expect(span).toEqual(node.childNodes[0]); + }); + + it("should provide a redraw method that is able to update different attributes", () => { + const oldvnode = h3("span", { title: "a", something: "b" }); + const newvnode = h3("span", { title: "b", id: "bbb" }); + const node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(node.getAttribute("title")).toEqual("b"); + expect(node.getAttribute("id")).toEqual("bbb"); + expect(node.hasAttribute("something")).toEqual(false); + }); + + it("should provide a redraw method that is able to update different classes", () => { + const oldvnode = h3("span.a.b", { title: "b" }); + const newvnode = h3("span.c", { title: "b" }); + const node = oldvnode.render(); + oldvnode.redraw({ node: node, vnode: newvnode }); + expect(oldvnode).toEqual(newvnode); + expect(node.classList.value).toEqual("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 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 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 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 a redraw method able to detect specific changes to style, data, value, attributes and eventListeners", () => { + const fn = () => false; + const oldvnode = h3("input", { + style: "margin: auto;", + data: { a: 111, b: 222 }, + value: "Test...", + title: "test", + onclick: () => true, + onkeypress: () => true, + }); + const newvnode = h3("input", { + style: "margin: 5px;", + data: { a: 112, c: 333 }, + value: "Test!", + title: "test #2", + placeholder: "test", + onkeypress: () => false, + 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("margin: 5px;"); + expect(node.dataset["a"]).toEqual("112"); + expect(node.dataset["c"]).toEqual("333"); + expect(node.dataset["b"]).toEqual(undefined); + expect(node.getAttribute("title")).toEqual("test #2"); + expect(node.getAttribute("placeholder")).toEqual("test"); + expect(node.value).toEqual("Test!"); + }); + + 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>" }); + 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(v1).toEqual(v2); + v3.redraw({ node: n3, vnode: v4 }); + expect(v3).toEqual(v4); + }); +});
M docs/example/assets/js/h3.jsdocs/example/assets/js/h3.js

@@ -110,7 +110,6 @@ if (

typeof data !== "function" && (typeof data !== "object" || data === null) ) { - console.log(data); throw new Error( "[VNode] The second argument of a VNode constructor must be an object, an array or a string." );

@@ -166,14 +165,17 @@ this.$html = attrs.$html;

this.style = attrs.style; this.value = attrs.value; this.data = attrs.data || {}; - this.classList = this.classList || attrs.classList || []; + this.classList = + attrs.classList && attrs.classList.length > 0 + ? attrs.classList + : this.classList; this.attributes = attrs || {}; Object.keys(attrs) .filter((a) => a.startsWith("on")) .forEach((key) => { if (typeof attrs[key] !== "function") { throw new Error( - `[VNode] Event handler specified for on${key} event is not a function.` + `[VNode] Event handler specified for ${key} event is not a function.` ); } this.eventListeners[key.slice(2)] = attrs[key];

@@ -282,26 +284,17 @@

// Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) redraw(data) { let { node, vnode } = data || {}; - if (!node && this.id) { - node = document.getElementById(this.id); - } - if (!vnode) { - vnode = this.render(); - } const newvnode = vnode; const oldvnode = this; if ( oldvnode.constructor !== newvnode.constructor || - oldvnode.type !== newvnode.type + oldvnode.type !== newvnode.type || + (oldvnode.type === newvnode.type && + oldvnode.type === "#text" && + oldvnode !== newvnode) ) { - // Different node types, replace the whole node (requires valid parent node) node.parentNode.replaceChild(newvnode.render(), node); - oldvnode = newvnode; - return; - } else if (oldvnode.constructor === String && oldvnode !== newvnode) { - // String nodes, update value - node.data = newvnode; - oldvnode = newvnode; + oldvnode.from(newvnode); return; } // ID
M docs/js/h3.jsdocs/js/h3.js

@@ -110,7 +110,6 @@ if (

typeof data !== "function" && (typeof data !== "object" || data === null) ) { - console.log(data); throw new Error( "[VNode] The second argument of a VNode constructor must be an object, an array or a string." );

@@ -166,14 +165,17 @@ this.$html = attrs.$html;

this.style = attrs.style; this.value = attrs.value; this.data = attrs.data || {}; - this.classList = this.classList || attrs.classList || []; + this.classList = + attrs.classList && attrs.classList.length > 0 + ? attrs.classList + : this.classList; this.attributes = attrs || {}; Object.keys(attrs) .filter((a) => a.startsWith("on")) .forEach((key) => { if (typeof attrs[key] !== "function") { throw new Error( - `[VNode] Event handler specified for on${key} event is not a function.` + `[VNode] Event handler specified for ${key} event is not a function.` ); } this.eventListeners[key.slice(2)] = attrs[key];

@@ -282,26 +284,17 @@

// Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) redraw(data) { let { node, vnode } = data || {}; - if (!node && this.id) { - node = document.getElementById(this.id); - } - if (!vnode) { - vnode = this.render(); - } const newvnode = vnode; const oldvnode = this; if ( oldvnode.constructor !== newvnode.constructor || - oldvnode.type !== newvnode.type + oldvnode.type !== newvnode.type || + (oldvnode.type === newvnode.type && + oldvnode.type === "#text" && + oldvnode !== newvnode) ) { - // Different node types, replace the whole node (requires valid parent node) node.parentNode.replaceChild(newvnode.render(), node); - oldvnode = newvnode; - return; - } else if (oldvnode.constructor === String && oldvnode !== newvnode) { - // String nodes, update value - node.data = newvnode; - oldvnode = newvnode; + oldvnode.from(newvnode); return; } // ID
M h3.jsh3.js

@@ -110,7 +110,6 @@ if (

typeof data !== "function" && (typeof data !== "object" || data === null) ) { - console.log(data); throw new Error( "[VNode] The second argument of a VNode constructor must be an object, an array or a string." );

@@ -166,14 +165,17 @@ this.$html = attrs.$html;

this.style = attrs.style; this.value = attrs.value; this.data = attrs.data || {}; - this.classList = this.classList || attrs.classList || []; + this.classList = + attrs.classList && attrs.classList.length > 0 + ? attrs.classList + : this.classList; this.attributes = attrs || {}; Object.keys(attrs) .filter((a) => a.startsWith("on")) .forEach((key) => { if (typeof attrs[key] !== "function") { throw new Error( - `[VNode] Event handler specified for on${key} event is not a function.` + `[VNode] Event handler specified for ${key} event is not a function.` ); } this.eventListeners[key.slice(2)] = attrs[key];

@@ -282,26 +284,17 @@

// Updates the current Virtual Node with a new Virtual Node (and syncs the existing DOM Node) redraw(data) { let { node, vnode } = data || {}; - if (!node && this.id) { - node = document.getElementById(this.id); - } - if (!vnode) { - vnode = this.render(); - } const newvnode = vnode; const oldvnode = this; if ( oldvnode.constructor !== newvnode.constructor || - oldvnode.type !== newvnode.type + oldvnode.type !== newvnode.type || + (oldvnode.type === newvnode.type && + oldvnode.type === "#text" && + oldvnode !== newvnode) ) { - // Different node types, replace the whole node (requires valid parent node) node.parentNode.replaceChild(newvnode.render(), node); - oldvnode = newvnode; - return; - } else if (oldvnode.constructor === String && oldvnode !== newvnode) { - // String nodes, update value - node.data = newvnode; - oldvnode = newvnode; + oldvnode.from(newvnode); return; } // ID