seed.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. var config = require('./config'),
  2. Emitter = require('emitter'),
  3. DirectiveParser = require('./directive-parser'),
  4. TextNodeParser = require('./textnode-parser')
  5. var slice = Array.prototype.slice,
  6. ctrlAttr = config.prefix + '-controller',
  7. eachAttr = config.prefix + '-each'
  8. var depsObserver = new Emitter(),
  9. parsingDeps = false
  10. /*
  11. * The main ViewModel class
  12. * scans a node and parse it to populate data bindings
  13. */
  14. function Seed (el, options) {
  15. if (typeof el === 'string') {
  16. el = document.querySelector(el)
  17. }
  18. this.el = el
  19. el.seed = this
  20. this._bindings = {}
  21. this._computed = []
  22. // copy options
  23. options = options || {}
  24. for (var op in options) {
  25. this[op] = options[op]
  26. }
  27. // initialize the scope object
  28. var dataPrefix = config.prefix + '-data'
  29. var scope = this.scope =
  30. (options && options.data)
  31. || config.datum[el.getAttribute(dataPrefix)]
  32. || {}
  33. el.removeAttribute(dataPrefix)
  34. // if the passed in data is already consumed by
  35. // a Seed instance, make a copy from it
  36. if (scope.$seed) {
  37. scope = this.scope = scope.$dump()
  38. }
  39. // expose some useful stuff on the scope
  40. scope.$seed = this
  41. scope.$destroy = this._destroy.bind(this)
  42. scope.$dump = this._dump.bind(this)
  43. scope.$index = options.index
  44. scope.$parent = options.parentSeed && options.parentSeed.scope
  45. // add event listener to update corresponding binding
  46. // when a property is set
  47. this.on('set', this._updateBinding.bind(this))
  48. // now parse the DOM
  49. this._compileNode(el, true)
  50. // if has controller function, apply it
  51. var ctrlID = el.getAttribute(ctrlAttr)
  52. if (ctrlID) {
  53. el.removeAttribute(ctrlAttr)
  54. var factory = config.controllers[ctrlID]
  55. if (factory) {
  56. factory.call(this, this.scope)
  57. } else {
  58. console.warn('controller ' + ctrlID + ' is not defined.')
  59. }
  60. }
  61. // extract dependencies for computed properties
  62. parsingDeps = true
  63. this._computed.forEach(this._parseDeps.bind(this))
  64. delete this._computed
  65. parsingDeps = false
  66. }
  67. /*
  68. * Compile a node (recursive)
  69. */
  70. Seed.prototype._compileNode = function (node, root) {
  71. var seed = this
  72. if (node.nodeType === 3) { // text node
  73. seed._compileTextNode(node)
  74. } else if (node.nodeType === 1) {
  75. var eachExp = node.getAttribute(eachAttr),
  76. ctrlExp = node.getAttribute(ctrlAttr)
  77. if (eachExp) { // each block
  78. var directive = DirectiveParser.parse(eachAttr, eachExp)
  79. if (directive) {
  80. directive.el = node
  81. seed._bind(directive)
  82. }
  83. } else if (ctrlExp && !root) { // nested controllers
  84. var child = new Seed(node, {
  85. child: true,
  86. parentSeed: seed
  87. })
  88. if (node.id) {
  89. seed['$' + node.id] = child
  90. }
  91. } else { // normal node
  92. // parse if has attributes
  93. if (node.attributes && node.attributes.length) {
  94. slice.call(node.attributes).forEach(function (attr) {
  95. if (attr.name === ctrlAttr) return
  96. var valid = false
  97. attr.value.split(',').forEach(function (exp) {
  98. var directive = DirectiveParser.parse(attr.name, exp)
  99. if (directive) {
  100. valid = true
  101. directive.el = node
  102. seed._bind(directive)
  103. }
  104. })
  105. if (valid) node.removeAttribute(attr.name)
  106. })
  107. }
  108. // recursively compile childNodes
  109. if (node.childNodes.length) {
  110. slice.call(node.childNodes).forEach(function (child) {
  111. seed._compileNode(child)
  112. })
  113. }
  114. }
  115. }
  116. }
  117. /*
  118. * Compile a text node
  119. */
  120. Seed.prototype._compileTextNode = function (node) {
  121. return TextNodeParser.parse(node)
  122. }
  123. /*
  124. * Add a directive instance to the correct binding & scope
  125. */
  126. Seed.prototype._bind = function (directive) {
  127. directive.seed = this
  128. var key = directive.key,
  129. epr = this.eachPrefixRE,
  130. isEachKey = epr && epr.test(key),
  131. scope = this
  132. if (isEachKey) {
  133. key = directive.key = key.replace(epr, '')
  134. }
  135. if (epr && !isEachKey) {
  136. scope = this.parentSeed
  137. }
  138. var ownerScope = determinScope(directive, scope),
  139. binding =
  140. ownerScope._bindings[key] ||
  141. ownerScope._createBinding(key)
  142. // add directive to this binding
  143. binding.instances.push(directive)
  144. directive.binding = binding
  145. // invoke bind hook if exists
  146. if (directive.bind) {
  147. directive.bind(binding.value)
  148. }
  149. // set initial value
  150. directive.update(binding.value)
  151. }
  152. Seed.prototype._createBinding = function (key) {
  153. var binding = new Binding(this.scope[key])
  154. this._bindings[key] = binding
  155. // bind accessor triggers to scope
  156. var seed = this
  157. Object.defineProperty(this.scope, key, {
  158. get: function () {
  159. if (parsingDeps) {
  160. depsObserver.emit('get', binding)
  161. }
  162. seed.emit('get', key)
  163. return binding.isComputed
  164. ? binding.value()
  165. : binding.value
  166. },
  167. set: function (value) {
  168. if (value === binding.value) return
  169. seed.emit('set', key, value)
  170. }
  171. })
  172. return binding
  173. }
  174. Seed.prototype._updateBinding = function (key, value) {
  175. var binding = this._bindings[key],
  176. type = binding.type = typeOf(value)
  177. // preprocess the value depending on its type
  178. if (type === 'Object') {
  179. if (value.get) { // computed property
  180. this._computed.push(binding)
  181. binding.isComputed = true
  182. value = value.get
  183. } else { // normal object
  184. // TODO watchObject
  185. }
  186. } else if (type === 'Array') {
  187. watchArray(value)
  188. value.on('mutate', function () {
  189. binding.emitChange()
  190. })
  191. }
  192. binding.value = value
  193. // update all instances
  194. binding.instances.forEach(function (instance) {
  195. instance.update(value)
  196. })
  197. // notify dependents to refresh themselves
  198. binding.emitChange()
  199. }
  200. Seed.prototype._parseDeps = function (binding) {
  201. depsObserver.on('get', function (dep) {
  202. if (!dep.dependents) {
  203. dep.dependents = []
  204. }
  205. dep.dependents.push.apply(dep.dependents, binding.instances)
  206. })
  207. binding.value()
  208. depsObserver.off('get')
  209. }
  210. Seed.prototype._unbind = function () {
  211. var unbind = function (instance) {
  212. if (instance.unbind) {
  213. instance.unbind()
  214. }
  215. }
  216. for (var key in this._bindings) {
  217. this._bindings[key].instances.forEach(unbind)
  218. }
  219. }
  220. Seed.prototype._destroy = function () {
  221. this._unbind()
  222. delete this.el.seed
  223. this.el.parentNode.removeChild(this.el)
  224. if (this.parentSeed && this.id) {
  225. delete this.parentSeed['$' + this.id]
  226. }
  227. }
  228. Seed.prototype._dump = function () {
  229. var dump = {}, binding, val,
  230. subDump = function (scope) {
  231. return scope.$dump()
  232. }
  233. for (var key in this._bindings) {
  234. binding = this._bindings[key]
  235. val = binding.value
  236. if (!val) continue
  237. if (Array.isArray(val)) {
  238. dump[key] = val.map(subDump)
  239. } else if (typeof val !== 'function') {
  240. dump[key] = val
  241. } else if (binding.isComputed) {
  242. dump[key] = val()
  243. }
  244. }
  245. return dump
  246. }
  247. /*
  248. * Binding class
  249. */
  250. function Binding (value) {
  251. this.value = value
  252. this.instances = []
  253. this.dependents = []
  254. }
  255. Binding.prototype.emitChange = function () {
  256. this.dependents.forEach(function (dept) {
  257. dept.refresh()
  258. })
  259. }
  260. // Helpers --------------------------------------------------------------------
  261. /*
  262. * determinScope()
  263. * determine which scope a key belongs to based on nesting symbols
  264. */
  265. function determinScope (key, scope) {
  266. if (key.nesting) {
  267. var levels = key.nesting
  268. while (scope.parentSeed && levels--) {
  269. scope = scope.parentSeed
  270. }
  271. } else if (key.root) {
  272. while (scope.parentSeed) {
  273. scope = scope.parentSeed
  274. }
  275. }
  276. return scope
  277. }
  278. /*
  279. * typeOf()
  280. * get accurate type of an object
  281. */
  282. var OtoString = Object.prototype.toString
  283. function typeOf (obj) {
  284. return OtoString.call(obj).slice(8, -1)
  285. }
  286. /*
  287. * watchArray()
  288. * augment an Array so that it emit events when mutated
  289. */
  290. var arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']
  291. var arrayAugmentations = {
  292. remove: function (scope) {
  293. this.splice(scope.$index, 1)
  294. },
  295. replace: function (index, data) {
  296. if (typeof index !== 'number') {
  297. index = index.$index
  298. }
  299. this.splice(index, 1, data)
  300. }
  301. }
  302. function watchArray (collection) {
  303. Emitter(collection)
  304. arrayMutators.forEach(function (method) {
  305. collection[method] = function () {
  306. var result = Array.prototype[method].apply(this, arguments)
  307. collection.emit('mutate', {
  308. method: method,
  309. args: Array.prototype.slice.call(arguments),
  310. result: result
  311. })
  312. }
  313. })
  314. for (var method in arrayAugmentations) {
  315. collection[method] = arrayAugmentations[method]
  316. }
  317. }
  318. Emitter(Seed.prototype)
  319. module.exports = Seed