Skip to content

Commit

Permalink
Introduce ModelChain
Browse files Browse the repository at this point in the history
  • Loading branch information
thoughtsunificator committed Nov 20, 2024
1 parent 37091e8 commit 1053f71
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 57 deletions.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 2 additions & 1 deletion src/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
1 change: 0 additions & 1 deletion src/binding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<div id="test"><button></button></div>')
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, '<div id="test"><button></button><button></button></div>')
Expand Down
65 changes: 44 additions & 21 deletions src/core.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Binding from "./binding.js"
// eslint-disable-next-line no-unused-vars
import ModelChain from "./model-chain.js"

/**
* @global
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -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<string>}
*/
function getFunctionNames(obj) {
const prototype = Object.getPrototypeOf(obj)
return getPrototypeFunctionNames(prototype)
}

/**
* @ignore
* @param {object} obj
* @returns {Array<string>}
*/
function getPrototypeFunctionNames(prototype) {
const functionNames = new Set()
const ownPropertyDescriptors = Object.getOwnPropertyDescriptors(prototype)
Expand Down
52 changes: 18 additions & 34 deletions src/core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,31 @@ test("model fragment", (t) => {
t.is(t.context.document.body.innerHTML, '<div class="test1">TestText1</div><div class="test2">TestText2</div>')
})

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, "<button></button><button></button>")
})

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, "<button></button>")
})

test("model fragment placeholder case 2", (t) => {
const binding2 = new Binding()
const binding = new Binding()
Core.run({
tagName: "div",
children: [
Expand All @@ -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, "<div><span></span><button></button><small></small></div>")
// 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, "<div><span></span><button></button><small></small></div>")
// })

test("childNodes", (t) => {
Core.run({
tagName: "div",
Expand Down
113 changes: 113 additions & 0 deletions src/model-chain.js
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 1053f71

Please sign in to comment.