/**
* @module core
* @description
* Core related methods
*
* @example
* import { Core } from "domodel"
*/
import Binding from "./binding.js"
/**
* @type {Method}
*/
export const METHOD = {
APPEND_CHILD: "APPEND_CHILD",
INSERT_BEFORE: "INSERT_BEFORE",
INSERT_AFTER: "INSERT_AFTER",
REPLACE_NODE: "REPLACE_NODE",
WRAP_NODE: "WRAP_NODE",
PREPEND: "PREPEND",
}
/**
* Create and connect a `Model` to the `DOM`
* @param {Model} model
* @param {RunParameters} runParameters
* @returns {Element}
* @example Core.run({ tagName: "div" }, { target: document.body })
*/
export function run(model, runParameters) {
const { target = runParameters.parentNode, binding = new Binding(), method = METHOD.APPEND_CHILD } = runParameters
const element = createElement(target, model, binding)
binding.root = element
binding.model = model
binding.onCreated()
for (const name of getFunctionNames(binding.eventListener)) {
binding.listen(binding.eventListener.observable, name, binding.eventListener[name].bind(binding), true)
}
connectElement(target, element, method, binding)
return element
}
/**
* Create an `Element` from a `Model`
* @ignore
* @param {Element} target
* @param {Model} model
* @param {Binding} binding
* @returns {Element}
*/
export function createElement(target, model, binding) {
const { tagName, children = [], identifier, attributes, childModel, ...elementProperties } = model
let element
if(tagName) { // ElementDefinition
element = target.ownerDocument.createElement(tagName)
for(const elementProperty in elementProperties) {
element[elementProperty] = model[elementProperty]
}
for(const attribute in attributes) {
element.setAttribute(attribute, attributes[attribute])
}
if(identifier) {
binding.identifier[identifier] = { element, model, binding }
binding.elements[identifier] = element
}
for(const child of children) {
const childElement = createElement(element, child, binding)
element.appendChild(childElement)
}
} else if(childModel) { // ChildModelDefinition
let childBinding
if(childModel.binding) {
childBinding = new childModel.binding(...(childModel.arguments || []))
}
element = binding.run(childModel.model, { target, binding: childBinding }, childBinding.identifier)
if(childModel.identifier) {
binding.identifier[childModel.identifier] = {
element,
model: childModel.model,
binding: childBinding
}
binding.elements[childModel.identifier] = element
}
}
return element
}
/**
* @ignore
* @param {Element} target
* @param {Element} element
* @param {Method} method
* @param {Binding} binding
*/
function connectElement(target, element, method, binding) {
if (method === METHOD.APPEND_CHILD) {
target.appendChild(element)
} else if (method === METHOD.INSERT_BEFORE) {
target.before(element)
} else if (method === METHOD.INSERT_AFTER) {
target.after(element)
} else if(method === METHOD.REPLACE_NODE) {
target.replaceWith(element)
} else if (method === METHOD.WRAP_NODE) {
element.appendChild(target.cloneNode(true))
target.replaceWith(element)
} else if (method === METHOD.PREPEND) {
target.prepend(element)
}
if(element.isConnected) {
binding._onConnected()
}
}
/**
* @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)
for(const name in ownPropertyDescriptors) {
if(name !== "constructor" && typeof ownPropertyDescriptors[name].value === "function") {
functionNames.add(name)
}
}
const parentPrototype = Object.getPrototypeOf(prototype)
if(Object.getPrototypeOf(parentPrototype)) {
for(const functionName of getPrototypeFunctionNames(parentPrototype)) {
functionNames.add(functionName)
}
}
return functionNames
}
export default {
run,
METHOD
}
/**
* @typedef {(ElementDefinition|ChildModelDefinition)} Model - While a `Binding` defines the behavior of a component, a `Model` defines its look.
*/
/**
* @typedef {object} ElementDefinition - Create an Element
* @property {tagName} tagName
* @property {object} [attributes] - These will be set using setAttribute
* @property {str} [identifier] - Creates an identifier for this Element
* @property {Array<Model>} [children]
*/
/**
* @typedef {object} ChildModelDefinition
* @property {ChildModel} childModel
*/
/**
* @typedef {object} ChildModel - Full-featured nesting of model
* @property {ElementDefinition} childModel.model
* @property {Binding} childModel.binding - Binding class
* @property {array} [childModel.arguments] - These properties will be passed to the binding
* @property {str} [childModel.identifier] - Creates an identifier for this ChildModel
*/
/**
* @typedef {object} RunParameters - Allow some degree of parameterization when running a Model
* @property {Element} target
* @property {Binding} [binding=new Binding()]
* @property {Method} [method=METHOD.APPEND_CHILD]
* @property {str} [identifier] - Creates an identifier for this Model
*/
/**
* @typedef {object} Method - Change how a model is connected to the DOM
* @property APPEND_CHILD {string}
* @property INSERT_BEFORE {string}
* @property INSERT_AFTER {string}
* @property REPLACE_NODE {string}
* @property WRAP_NODE {string}
* @property PREPEND {string}
*/