binding.js

  1. import Core from "./core.js"
  2. import EventListener from "./event-listener.js"
  3. import Observable from "./observable.js"
  4. /**
  5. * @class
  6. * While a `Model` defines the outlook of a component, a `Binding` defines its behavior.
  7. * @param {EventListener} [eventListener=new EventListener(new Observable())]
  8. */
  9. function Binding(eventListener = new EventListener(new Observable())) {
  10. /**
  11. * Access any child Model identified with the "identifier" property.
  12. * Any identifier returns a Binding instance
  13. * @type {object}
  14. */
  15. this.identifier = {}
  16. /**
  17. * This is a shortcut to this.identifier["foo"].element
  18. * Access any child Model's element identified with the "identifier" property.
  19. * Any identifier returns an Element
  20. * @type {object}
  21. */
  22. this.elements = {}
  23. /**
  24. * Parent Binding
  25. * @type {Binding}
  26. */
  27. this.parent = null
  28. /** @type {Element} */
  29. this.root = null
  30. /** @type {Model} */
  31. this.model = null
  32. /**
  33. * List of child Binding
  34. * @type {Array<ChildBinding>}
  35. */
  36. this.children = []
  37. /**
  38. * Listener register
  39. * @ignore @type {Listener}
  40. */
  41. this.listeners = []
  42. /** @ignore @type {EventListener} */
  43. this.eventListener = eventListener
  44. /** @ignore Used to track listeners on remote foreign Element */
  45. this.remoteEventListeners = []
  46. /**
  47. * Observable register
  48. * @type {Map}
  49. */
  50. this._observables = new Map()
  51. }
  52. /**
  53. * @ignore
  54. * Call `onConnected` on this Binding and all its children
  55. */
  56. Binding.prototype._onConnected = function() {
  57. this.onConnected()
  58. for(const { binding } of this.children) {
  59. binding._onConnected()
  60. }
  61. }
  62. /**
  63. * Alias for `Observable.listen`, the listeners are also stored
  64. * for later removal.
  65. * @param {*} target
  66. * @param {string} eventName
  67. * @param {Function} callback
  68. * @param {boolean} [unshift=false]
  69. * @returns {Listener}
  70. * @example binding.listen(observable, "myEvent", message => console.log(message))
  71. */
  72. Binding.prototype.listen = function(target, eventName, callback, unshift = false) {
  73. let listener
  74. if(target instanceof Observable) {
  75. listener = target.listen(eventName, callback, unshift)
  76. } else {
  77. /**
  78. * New feature
  79. * This allows to listen to any object not just an Observable
  80. */
  81. if(!this.observables.has(target)) {
  82. this.observables.set(target, new Observable())
  83. }
  84. listener = this.observables.get(target).listen(eventName, callback, unshift)
  85. }
  86. if(unshift) {
  87. this.listeners.unshift(listener)
  88. } else {
  89. this.listeners.push(listener)
  90. }
  91. return listener
  92. }
  93. /**
  94. *
  95. * @param {*} target
  96. */
  97. Binding.prototype.emit = function(target, ...emitArgument) {
  98. const observable = this.observables.get(target)
  99. observable.emit(...emitArgument)
  100. }
  101. /**
  102. * Alias for `Core.run`, except that the target is pre-configured to
  103. * be the current `Binding`'s root `Element`. Allows identification and hierarchization
  104. * of `Models` inside the current Binding.
  105. * @param {Model} model
  106. * @param {RunParameters} runParameters
  107. * @param {str} identifier
  108. * @returns {Element}
  109. * @example binding.run(Model, { binding: new Binding() })
  110. */
  111. Binding.prototype.run = function(model, runParameters) {
  112. const { identifier, binding = new Binding() } = runParameters
  113. binding.parent = this
  114. this.children.push({ model, binding: binding, identifier })
  115. const element = Core.run(model, { target: runParameters.target || this.root, ...runParameters })
  116. if(identifier) {
  117. this.identifier[identifier] = { element, model: runParameters.model, binding: binding }
  118. this.elements[identifier] = element
  119. }
  120. return element
  121. }
  122. /**
  123. * Remove the associated `Model` and all its children from the DOM
  124. * and clean up any `DOM` `Event` or `Observable` listeners associated with them.
  125. */
  126. Binding.prototype.remove = function() {
  127. for(const { target, type, listener, options } of this.remoteEventListeners) {
  128. target.removeEventListener(type, listener, options)
  129. }
  130. this.remoteEventListeners = []
  131. const listeners = this.listeners.slice()
  132. for(const listener of listeners) {
  133. listener.remove()
  134. }
  135. const children = this.children.slice()
  136. for(const { binding } of children) {
  137. binding.remove()
  138. }
  139. if(this.parent !== null) {
  140. this.parent.children = this.parent.children.filter(child => child !== this)
  141. }
  142. this.root.remove()
  143. }
  144. /**
  145. * Store an `DOM` event listener for later removal.
  146. *
  147. * It can be used to store event listeners on foreign `Element` such as `Window`.
  148. * @param {Element} target
  149. * @param {string} type
  150. * @param {method} listener
  151. * @param {object} options
  152. */
  153. Binding.prototype.addEventListener = function(target, type, listener, options) {
  154. this.remoteEventListeners.push({ target, type, listener, options })
  155. target.addEventListener(type, listener, options)
  156. }
  157. /**
  158. * This hook is called after the `Element` is created but before the Element is connected to the `DOM`
  159. * @abstract
  160. */
  161. Binding.prototype.onCreated = function() {}
  162. /**
  163. * This hook is called after the `Element` is created and is connected to the `DOM`
  164. * @abstract
  165. */
  166. Binding.prototype.onConnected = function() {}
  167. /** Binding.document */
  168. Object.defineProperty(Binding.prototype, "document", {
  169. get: function() {
  170. return this.root.ownerDocument
  171. }
  172. })
  173. /** Binding.window */
  174. Object.defineProperty(Binding.prototype, "window", {
  175. get: function() {
  176. return this.document.defaultView
  177. }
  178. })
  179. /** Binding.observables */
  180. Object.defineProperty(Binding.prototype, "observables", {
  181. get: function() {
  182. if(this.parent) {
  183. return this.parent.observables
  184. } else {
  185. return this._observables
  186. }
  187. }
  188. })
  189. export default Binding
  190. /**
  191. * @typedef {import("./core.js").Model} Model
  192. * @typedef {import("./core.js").RunParameters} RunParameters
  193. * @typedef {import("./listener.js").default} Listener
  194. */
  195. /**
  196. * @memberof Binding
  197. * @typedef {object} ChildBinding
  198. * @property {ElementDefinition} childModel.model
  199. * @property {Binding} childModel.binding
  200. * @property {str} [childModel.identifier]
  201. */
  202. /**
  203. * @name Binding#window
  204. * @type {Window}
  205. */
  206. /**
  207. * @name Binding#document
  208. * @type {Document}
  209. */