binding.js

import Core from "./core.js"
import EventListener from "./event-listener.js"
import Observable from "./observable.js"

/**
* @class
* While a `Model` defines the outlook of a component, a `Binding` defines its behavior.
* @param {EventListener} [eventListener=new EventListener(new Observable())]
*/
function Binding(eventListener = new EventListener(new Observable())) {
	/**
	 * Access any child Model identified with the "identifier" property.
	 * Any identifier returns a Binding instance
	 * @type {object}
	 */
	this.identifier = {}
	/**
	 * This is a shortcut to this.identifier["foo"].element
	 * Access any child Model's element identified with the "identifier" property.
	 * Any identifier returns an Element
	 * @type {object}
	 */
	this.elements = {}
	/**
	 * Parent Binding
	 * @type {Binding}
	 */
	this.parent = null
	/** @type {Element} */
	this.root = null
	/** @type {Model} */
	this.model = null
	/**
	 * List of child Binding
	 * @type {Array<ChildBinding>}
	 */
	this.children = []
	/**
	 * Listener register
	 * @ignore @type {Listener}
	 */
	this.listeners = []
	/** @ignore @type {EventListener} */
	this.eventListener = eventListener
	/** @ignore Used to track listeners on remote foreign Element */
	this.remoteEventListeners = []
	/**
	 * Observable register
	 * @type {Map}
	 */
	this._observables = new Map()
}



/**
 * @ignore
 * Call `onConnected` on this Binding and all its children
 */
Binding.prototype._onConnected = function() {
	this.onConnected()
	for(const { binding } of this.children) {
		binding._onConnected()
	}
}

/**
 * Alias for `Observable.listen`, the listeners are also stored
 * for later removal.
 * @param {*}          target
 * @param {string}     eventName
 * @param {Function}   callback
 * @param {boolean}    [unshift=false]
 * @returns {Listener}
 * @example binding.listen(observable, "myEvent", message => console.log(message))
 */
Binding.prototype.listen = function(target, eventName, callback, unshift = false) {
	let listener
	if(target instanceof Observable) {
		listener = target.listen(eventName, callback, unshift)
	} else {
		/**
		 * New feature
		 * This allows to listen to any object not just an Observable
		 */
		if(!this.observables.has(target)) {
			this.observables.set(target, new Observable())
		}
		listener = this.observables.get(target).listen(eventName, callback, unshift)
	}
	if(unshift) {
		this.listeners.unshift(listener)
	} else {
		this.listeners.push(listener)
	}
	return listener
}

/**
 *
 * @param {*} target
 */
Binding.prototype.emit = function(target, ...emitArgument) {
	const observable = this.observables.get(target)
	observable.emit(...emitArgument)
}

/**
 * Alias for `Core.run`, except that the target is pre-configured to
 * be the current `Binding`'s root `Element`. Allows identification and hierarchization
 * of `Models` inside the current Binding.
 * @param   {Model}         model
 * @param   {RunParameters} runParameters
 * @param   {str}           identifier
 * @returns {Element}
 * @example binding.run(Model, { binding: new Binding() })
 */
Binding.prototype.run = function(model, runParameters) {
	const { identifier, binding = new Binding() } = runParameters
	binding.parent = this
	this.children.push({ model, binding: binding, identifier })
	const element = Core.run(model, { target: runParameters.target || this.root, ...runParameters })
	if(identifier) {
		this.identifier[identifier] = { element, model: runParameters.model, binding: binding }
		this.elements[identifier] = element
	}
	return element
}

/**
 * Remove the associated `Model` and all its children from the DOM
 * and clean up any `DOM` `Event` or `Observable` listeners associated with them.
 */
Binding.prototype.remove = function() {
	for(const { target, type, listener, options } of this.remoteEventListeners)  {
		target.removeEventListener(type, listener, options)
	}
	this.remoteEventListeners = []
	const listeners = this.listeners.slice()
	for(const listener of listeners) {
		listener.remove()
	}
	const children = this.children.slice()
	for(const { binding } of children) {
		binding.remove()
	}
	if(this.parent !== null) {
		this.parent.children = this.parent.children.filter(child => child !== this)
	}
	this.root.remove()
}

/**
 * Store an `DOM` event listener for later removal.
 *
 * It can be used to store event listeners on foreign `Element` such as `Window`.
 * @param {Element} target
 * @param {string}  type
 * @param {method}  listener
 * @param {object}  options
 */
Binding.prototype.addEventListener = function(target, type, listener, options) {
	this.remoteEventListeners.push({ target, type, listener, options })
	target.addEventListener(type, listener, options)
}

/**
 * This hook is called after the `Element` is created but before the Element is connected to the `DOM`
 * @abstract
 */
Binding.prototype.onCreated = function() {}

/**
 * This hook is called after the `Element` is created and is connected to the `DOM`
 * @abstract
 */
Binding.prototype.onConnected = function() {}

/** Binding.document */
Object.defineProperty(Binding.prototype, "document", {
	get: function() {
		return this.root.ownerDocument
	}
})

/** Binding.window */
Object.defineProperty(Binding.prototype, "window", {
	get: function() {
		return this.document.defaultView
	}
})

/** Binding.observables */
Object.defineProperty(Binding.prototype, "observables", {
	get: function() {
		if(this.parent) {
			return this.parent.observables
		} else {
			return this._observables
		}
	}
})

export default Binding

/**
 * @typedef {import("./core.js").Model} Model
 * @typedef {import("./core.js").RunParameters} RunParameters
 * @typedef {import("./listener.js").default} Listener
 */

/**
 * @memberof Binding
 * @typedef  {object}                                ChildBinding
 * @property {ElementDefinition}                     childModel.model
 * @property {Binding}                               childModel.binding
 * @property {str}                                   [childModel.identifier]
 */

/**
 * @name Binding#window
 * @type {Window}
 */

/**
 * @name Binding#document
 * @type {Document}
 */