From 1053f7151f6dabeb2bd798293a9fc7615feeaf97 Mon Sep 17 00:00:00 2001 From: thoughtsunificator Date: Thu, 21 Nov 2024 00:28:40 +0100 Subject: [PATCH] Introduce ModelChain --- index.js | 1 + src/binding.js | 3 +- src/binding.test.js | 1 - src/core.js | 65 +++++++++++++++-------- src/core.test.js | 52 +++++++----------- src/model-chain.js | 113 ++++++++++++++++++++++++++++++++++++++++ src/model-chain.test.js | 97 ++++++++++++++++++++++++++++++++++ 7 files changed, 275 insertions(+), 57 deletions(-) create mode 100644 src/model-chain.js create mode 100644 src/model-chain.test.js diff --git a/index.js b/index.js index 269852e..45b0d9d 100644 --- a/index.js +++ b/index.js @@ -4,3 +4,4 @@ export { default as Observable } from "./src/observable.js" export { default as Listener } from "./src/listener.js" export { default as EventListener } from "./src/event-listener.js" export { default as Model } from "./src/model.js" +export { default as ModelChain } from "./src/model-chain.js" diff --git a/src/binding.js b/src/binding.js index e2a332c..9831ce3 100644 --- a/src/binding.js +++ b/src/binding.js @@ -149,9 +149,10 @@ class Binding { this._parent._children = this._parent._children.filter(child => child !== this) } if(this.root instanceof this.window.DocumentFragment) { - for(const child of this.root.fragmentChildren) { + for(const child of this.root.domodel.fragmentChildren) { child.remove() } + this.root.domodel.fragmentChildren = [] } else { this.root.remove() } diff --git a/src/binding.test.js b/src/binding.test.js index e65f992..479ab23 100644 --- a/src/binding.test.js +++ b/src/binding.test.js @@ -188,7 +188,6 @@ test("Binding remove placeholder documentFragment", (t) => { Core.run(MyModel5, { binding, parentNode: t.context.document.body }) binding.run({ tagName: "button" }, { binding: new MyBinding3({ observable: t.context.observable }), parentNode: binding.identifier.test }) t.is(t.context.document.body.innerHTML, '
') - console.log(binding.identifier.test) const b2 = new MyBinding3({ observable: t.context.observable }) binding.run({ tagName: "button" }, { identifier: "test3", binding: b2, parentNode: binding.identifier.test }) t.is(t.context.document.body.innerHTML, '
') diff --git a/src/core.js b/src/core.js index 6f1d880..497359c 100644 --- a/src/core.js +++ b/src/core.js @@ -1,4 +1,6 @@ import Binding from "./binding.js" +// eslint-disable-next-line no-unused-vars +import ModelChain from "./model-chain.js" /** * @global @@ -38,37 +40,46 @@ class Core { } /** - * @param {Object} model - * @param {Object} properties - * @param {Element} properties.parentNode - * @param {Binding} [properties.binding=new Binding()] - * @param {Method} [properties.method=Core.METHOD.APPEND_CHILD] + * @param {Object|ModelChain} model + * @param {Object} properties + * @param {Element} properties.parentNode + * @param {Binding} [properties.binding=new Binding()] + * @param {Method} [properties.method=Core.METHOD.APPEND_CHILD] * @returns {Element} - * @example Core.run(Model, { parentNode: document.body, binding: new Binding() }) + * @example Core.run(Model, { parentNode: document.body }) */ static run(model, { parentNode, binding = new Binding(), method = Core.METHOD.APPEND_CHILD } = {}) { - const node = Core.createElement(parentNode, model, binding) + let modelDefinition + if(model instanceof ModelChain) { + modelDefinition = model.definition + } else { + modelDefinition = model + } + const node = Core.createElement(parentNode, modelDefinition, binding) binding._root = node binding._model = model for (const name of getFunctionNames(binding.eventListener)) { binding.listen(binding.eventListener.observable, name, binding.eventListener[name].bind(binding), true) } - binding.onCreated() if(node instanceof node.ownerDocument.defaultView.DocumentFragment) { /** * When binding root's element is a DocumentFragment its children need to be referenced * so that they are removed when Binding.remove is called. - */ - node.fragmentChildren = [...node.children] + */ + node.domodel.fragmentChildren = [...node.children] } + if(node instanceof node.ownerDocument.defaultView.DocumentFragment && node.domodel.identifier) { + binding.identifier[node.domodel.identifier] = parentNode + } + binding.onCreated() const isPlaceholderDocumentFragment = parentNode.nodeType === node.ownerDocument.defaultView.Node.COMMENT_NODE if(isPlaceholderDocumentFragment) { - if(parentNode.placeholderNode) { - parentNode.placeholderNode.after(node) + if(parentNode.domodel?.placeholderNode) { + parentNode.domodel.placeholderNode.after(node) } else { parentNode.replaceWith(node) } - parentNode.placeholderNode = node + parentNode.domodel.placeholderNode = node } else if (method === Core.METHOD.APPEND_CHILD) { parentNode.appendChild(node) } else if (method === Core.METHOD.INSERT_BEFORE) { @@ -91,12 +102,12 @@ class Core { * Create an element from a model definition * @ignore * @param {Object} Node - * @param {Object} model + * @param {Object} modelDefinition * @param {Object} Binding * @returns {Element} */ - static createElement(parentNode, model, binding) { - const { tagName, children = [] } = model + static createElement(parentNode, modelDefinition, binding) { + const { tagName, children = [] } = modelDefinition let node if(tagName) { node = parentNode.ownerDocument.createElement(tagName) @@ -107,11 +118,12 @@ class Core { node = parentNode.ownerDocument.createComment("") } } - Object.keys(model).filter(property => Core.PROPERTIES.includes(property) === false).forEach(function(property) { + node.domodel = {} + Object.keys(modelDefinition).filter(property => Core.PROPERTIES.includes(property) === false).forEach(function(property) { if(typeof node[property] !== "undefined") { - node[property] = model[property] + node[property] = modelDefinition[property] } else { - node.setAttribute(property, model[property]) + node.setAttribute(property, modelDefinition[property]) } }) for(const child of children) { @@ -129,19 +141,30 @@ class Core { node.appendChild(childNode) } } - if(Object.prototype.hasOwnProperty.call(model, "identifier") === true) { - binding.identifier[model.identifier] = node + if(Object.prototype.hasOwnProperty.call(modelDefinition, "identifier") === true) { + binding.identifier[modelDefinition.identifier] = node + node.domodel.identifier = modelDefinition.identifier } return node } } +/** + * @ignore + * @param {object} obj + * @returns {Array} + */ function getFunctionNames(obj) { const prototype = Object.getPrototypeOf(obj) return getPrototypeFunctionNames(prototype) } +/** + * @ignore + * @param {object} obj + * @returns {Array} + */ function getPrototypeFunctionNames(prototype) { const functionNames = new Set() const ownPropertyDescriptors = Object.getOwnPropertyDescriptors(prototype) diff --git a/src/core.test.js b/src/core.test.js index 6e731ef..6ccb897 100644 --- a/src/core.test.js +++ b/src/core.test.js @@ -48,19 +48,31 @@ test("model fragment", (t) => { t.is(t.context.document.body.innerHTML, '
TestText1
TestText2
') }) +test("model fragment case 2", (t) => { + const binding = new Binding() + Core.run({ + identifier: "test2", + children: [ + { + tagName: "button" + } + ] + }, { binding: binding, parentNode: t.context.document.body }) + binding.run({ tagName: "button" }, { binding: new Binding(), parentNode: binding.identifier.test2 }) + t.is(t.context.document.body.innerHTML, "") +}) + test("model fragment placeholder", (t) => { const binding = new Binding() Core.run({ identifier: "test" }, { binding, parentNode: t.context.document.body }) - binding.run({ - tagName: "button" - }, { binding, parentNode: binding.identifier.test }) + binding.run({ tagName: "button" }, { binding, parentNode: binding.identifier.test }) t.is(t.context.document.body.innerHTML, "") }) test("model fragment placeholder case 2", (t) => { - const binding2 = new Binding() + const binding = new Binding() Core.run({ tagName: "div", children: [ @@ -74,39 +86,11 @@ test("model fragment placeholder case 2", (t) => { tagName: "small" } ] - }, { binding: binding2, parentNode: t.context.document.body }) - binding2.run({ - tagName: "button" - }, { binding: new Binding(), parentNode: binding2.identifier.test2 }) + }, { binding: binding, parentNode: t.context.document.body }) + binding.run({ tagName: "button" }, { binding: new Binding(), parentNode: binding.identifier.test2 }) t.is(t.context.document.body.innerHTML, "
") - // t.is(binding2.identifier.test2.tagName, "BUTTON") }) -// test("model fragment placeholder case 3", (t) => { -// const binding2 = new Binding() -// Core.run({ -// tagName: "div", -// children: [ -// { -// tagName: "span", -// }, -// { -// identifier: "test2", // DocumentFragment here is a placeholder and should not be added -// }, -// { -// identifier: "test3", // DocumentFragment here is a placeholder and should not be added -// }, -// { -// tagName: "small" -// } -// ] -// }, { binding: binding2, parentNode: t.context.document.body }) -// binding2.run({ -// tagName: "button" -// }, { binding: binding2, parentNode: binding2.identifier.test2 }) -// t.is(t.context.document.body.innerHTML, "
") -// }) - test("childNodes", (t) => { Core.run({ tagName: "div", diff --git a/src/model-chain.js b/src/model-chain.js new file mode 100644 index 0000000..ddcbd73 --- /dev/null +++ b/src/model-chain.js @@ -0,0 +1,113 @@ +/** + * @global + */ + +/** + * @param {Object} definition + * @returns {ModelChain} + */ +function ModelChain(definition) { + this.definition = structuredClone(definition) +} + +/** + * @param {string} [parentIdentifier] + * @param {object} definition + * @returns {ModelChain} + */ +ModelChain.prototype.prepend = function(parentIdentifier, definition) { + if(parentIdentifier === null) { + this.definition.children.unshift(definition) + } else { + const { object } = getObjectByIdentifier(parentIdentifier, this.definition) + if(!object.children) { + object.children = [] + } + object.children.unshift(definition) + } + return this +} + +/** + * @param {string} [parentIdentifier] + * @param {object} definition + * @returns {ModelChain} + */ +ModelChain.prototype.append = function(parentIdentifier, definition) { + if(parentIdentifier === null) { + this.definition.children.push(definition) + } else { + const { object } = getObjectByIdentifier(parentIdentifier, this.definition) + if(!object.children) { + object.children = [] + } + object.children.push(definition) + } + return this +} + +/** + * @param {string} identifier + * @param {object} definition + * @returns {ModelChain} + */ +ModelChain.prototype.replace = function(identifier, definition) { + const { object } = getObjectByIdentifier(identifier, this.definition) + for(const property in object) { + delete object[property] + } + Object.assign(object, definition) + return this +} + +/** + * @param {string} identifier + * @param {object} definition + * @returns {ModelChain} + */ +ModelChain.prototype.before = function(identifier, definition) { + const { parent, object } = getObjectByIdentifier(identifier, this.definition) + parent.children.splice(parent.children.indexOf(object), 0, definition) + return this +} + +/** + * @param {string} identifier + * @param {object} definition + * @returns {ModelChain} + */ +ModelChain.prototype.after = function(identifier, definition) { + const { parent, object } = getObjectByIdentifier(identifier, this.definition) + parent.children.splice(parent.children.indexOf(object) + 1, 0, definition) + return this +} + +/** + * @param {string} identifier + * @param {object} definition + * @returns {{object: object, parent: object}} + */ +function getObjectByIdentifier(identifier, definition) { + /** + * + * @param {object} object + * @param {object} parent + * @returns {{object: object, parent: object}} + */ + function walk(object, parent) { + for(const property in object) { + if(property === "identifier" && object[property] === identifier) { + return { object, parent } + } else if(property === "children") { + for(const child of object[property]) { + if(walk(child)) { + return { object: child, parent: object } + } + } + } + } + } + return walk(definition, definition) +} + +export default ModelChain diff --git a/src/model-chain.test.js b/src/model-chain.test.js new file mode 100644 index 0000000..e31b070 --- /dev/null +++ b/src/model-chain.test.js @@ -0,0 +1,97 @@ +import { JSDOM } from "jsdom" +import test from "ava" + +import { Core, ModelChain } from "../index.js" + +const myModel = { + tagName: "div", + children: [ + { + tagName: "div", + id: "foo", + identifier: "foo" + }, + { + tagName: "div", + id: "bar", + identifier: "bar" + } + ] +} + +test.beforeEach((t) => { + const virtualDOM = new JSDOM() + const { document } = virtualDOM.window + t.context.document = document +}) + +test("basic", (t) => { + const modelChain = new ModelChain(myModel) + t.true(modelChain instanceof ModelChain) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) + +test("prepend root", (t) => { + const modelChain = new ModelChain(myModel) + modelChain.prepend(null, { + tagName: "button" + }) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) + +test("prepend", (t) => { + const modelChain = new ModelChain(myModel) + modelChain.prepend("foo", { + tagName: "button" + }) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) + +test("append root", (t) => { + const modelChain = new ModelChain(myModel) + modelChain.append(null, { + tagName: "button" + }) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) + +test("append", (t) => { + const modelChain = new ModelChain(myModel) + modelChain.append("foo", { + tagName: "button" + }) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) + +test("replace", (t) => { + const modelChain = new ModelChain(myModel) + modelChain.replace("foo", { + tagName: "button" + }) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) + +test("before", (t) => { + const modelChain = new ModelChain(myModel) + modelChain.before("bar", { + tagName: "button" + }) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) + +test("after", (t) => { + const modelChain = new ModelChain(myModel) + modelChain.after("bar", { + tagName: "button" + }) + Core.run(modelChain, { parentNode: t.context.document.body }) + t.is(t.context.document.body.innerHTML, '
') +}) +