binding.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. var Emitter = require('emitter'),
  2. observer = require('./deps-parser').observer
  3. /*
  4. * Binding class.
  5. *
  6. * each property on the scope has one corresponding Binding object
  7. * which has multiple directive instances on the DOM
  8. * and multiple computed property dependents
  9. */
  10. function Binding (seed, key) {
  11. this.seed = seed
  12. this.key = key
  13. var path = key.split('.')
  14. this.set(getValue(seed.scope, path))
  15. this.defineAccessors(seed.scope, path)
  16. this.instances = []
  17. this.dependents = []
  18. this.dependencies = []
  19. }
  20. /*
  21. * Pre-process a passed in value based on its type
  22. */
  23. Binding.prototype.set = function (value) {
  24. var type = typeOf(value),
  25. self = this
  26. // preprocess the value depending on its type
  27. if (type === 'Object') {
  28. if (value.get || value.set) { // computed property
  29. self.isComputed = true
  30. }
  31. } else if (type === 'Array') {
  32. watchArray(value)
  33. value.on('mutate', function () {
  34. self.emitChange()
  35. })
  36. }
  37. this.value = value
  38. }
  39. /*
  40. * Define getter/setter for this binding on scope
  41. */
  42. Binding.prototype.defineAccessors = function (scope, path) {
  43. var self = this,
  44. key = path[0]
  45. if (path.length === 1) {
  46. // here we are! at the end of the path!
  47. // define the real value accessors.
  48. Object.defineProperty(scope, key, {
  49. get: function () {
  50. if (observer.isObserving) {
  51. observer.emit('get', self)
  52. }
  53. return self.isComputed
  54. ? self.value.get()
  55. : self.value
  56. },
  57. set: function (value) {
  58. if (self.isComputed) {
  59. // computed properties cannot be redefined
  60. // no need to call binding.update() here,
  61. // as dependency extraction has taken care of that
  62. if (self.value.set) {
  63. self.value.set(value)
  64. }
  65. } else if (value !== self.value) {
  66. self.value = value
  67. self.update(value)
  68. }
  69. }
  70. })
  71. } else {
  72. // we are not there yet!!!
  73. // create an intermediate subscope
  74. // which also has its own getter/setters
  75. var subScope = scope[key]
  76. if (!subScope) {
  77. subScope = {}
  78. Object.defineProperty(scope, key, {
  79. get: function () {
  80. return subScope
  81. },
  82. set: function (value) {
  83. // when the subScope is given a new value,
  84. // copy everything over to trigger the setters
  85. for (var prop in value) {
  86. subScope[prop] = value[prop]
  87. }
  88. }
  89. })
  90. }
  91. this.defineAccessors(subScope, path.slice(1))
  92. }
  93. }
  94. /*
  95. * Process the value, then trigger updates on all dependents
  96. */
  97. Binding.prototype.update = function (value) {
  98. this.set(value)
  99. this.instances.forEach(function (instance) {
  100. instance.update(value)
  101. })
  102. this.emitChange()
  103. }
  104. /*
  105. * Notify computed properties that depend on this binding
  106. * to update themselves
  107. */
  108. Binding.prototype.emitChange = function () {
  109. this.dependents.forEach(function (dept) {
  110. dept.refresh()
  111. })
  112. }
  113. // Helpers --------------------------------------------------------------------
  114. /*
  115. * Get a value from an object based on a path array
  116. */
  117. function getValue (scope, path) {
  118. if (path.length === 1) return scope[path[0]]
  119. var i = 0
  120. /* jshint boss: true */
  121. while (scope[path[i]]) {
  122. scope = scope[path[i]]
  123. i++
  124. }
  125. return i === path.length ? scope : undefined
  126. }
  127. /*
  128. * get accurate type of an object
  129. */
  130. var toString = Object.prototype.toString
  131. function typeOf (obj) {
  132. return toString.call(obj).slice(8, -1)
  133. }
  134. /*
  135. * augment an Array so that it emit events when mutated
  136. */
  137. var aproto = Array.prototype,
  138. arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse'],
  139. arrayAugmentations = {
  140. remove: function (index) {
  141. if (typeof index !== 'number') index = index.$index
  142. this.splice(index, 1)
  143. },
  144. replace: function (index, data) {
  145. if (typeof index !== 'number') index = index.$index
  146. this.splice(index, 1, data)
  147. }
  148. }
  149. function watchArray (collection) {
  150. Emitter(collection)
  151. arrayMutators.forEach(function (method) {
  152. collection[method] = function () {
  153. var result = aproto[method].apply(this, arguments)
  154. collection.emit('mutate', {
  155. method: method,
  156. args: aproto.slice.call(arguments),
  157. result: result
  158. })
  159. }
  160. })
  161. for (var method in arrayAugmentations) {
  162. collection[method] = arrayAugmentations[method]
  163. }
  164. }
  165. module.exports = Binding