seed.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. var config = require('./config'),
  2. Scope = require('./scope'),
  3. Binding = require('./binding'),
  4. DirectiveParser = require('./directive-parser'),
  5. TextParser = require('./text-parser'),
  6. depsParser = require('./deps-parser')
  7. var slice = Array.prototype.slice,
  8. ctrlAttr = config.prefix + '-controller',
  9. eachAttr = config.prefix + '-each'
  10. /*
  11. * The main ViewModel class
  12. * scans a node and parse it to populate data bindings
  13. */
  14. function Seed (el, options) {
  15. config.log('\ncreated new Seed instance.\n')
  16. if (typeof el === 'string') {
  17. el = document.querySelector(el)
  18. }
  19. this.el = el
  20. el.seed = this
  21. this._bindings = {}
  22. this._computed = []
  23. // copy options
  24. options = options || {}
  25. for (var op in options) {
  26. this[op] = options[op]
  27. }
  28. // check if there's passed in data
  29. var dataAttr = config.prefix + '-data',
  30. dataId = el.getAttribute(dataAttr),
  31. data = (options && options.data) || config.datum[dataId]
  32. if (dataId && !data) {
  33. config.warn('data "' + dataId + '" is not defined.')
  34. }
  35. data = data || {}
  36. el.removeAttribute(dataAttr)
  37. // if the passed in data is the scope of a Seed instance,
  38. // make a copy from it
  39. if (data.$seed instanceof Seed) {
  40. data = data.$dump()
  41. }
  42. // initialize the scope object
  43. var key,
  44. scope = this.scope = new Scope(this, options)
  45. // copy data
  46. for (key in data) {
  47. scope[key] = data[key]
  48. }
  49. // if has controller function, apply it so we have all the user definitions
  50. var ctrlID = el.getAttribute(ctrlAttr)
  51. if (ctrlID) {
  52. el.removeAttribute(ctrlAttr)
  53. var factory = config.controllers[ctrlID]
  54. if (factory) {
  55. factory(this.scope)
  56. } else {
  57. config.warn('controller "' + ctrlID + '" is not defined.')
  58. }
  59. }
  60. // now parse the DOM
  61. this._compileNode(el, true)
  62. // for anything in scope but not binded in DOM, create bindings for them
  63. for (key in scope) {
  64. if (key.charAt(0) !== '$' && !this._bindings[key]) {
  65. this._createBinding(key)
  66. }
  67. }
  68. // extract dependencies for computed properties
  69. if (this._computed.length) depsParser.parse(this._computed)
  70. delete this._computed
  71. }
  72. // for better compression
  73. var SeedProto = Seed.prototype
  74. /*
  75. * Compile a DOM node (recursive)
  76. */
  77. SeedProto._compileNode = function (node, root) {
  78. var seed = this
  79. if (node.nodeType === 3) { // text node
  80. seed._compileTextNode(node)
  81. } else if (node.nodeType === 1) {
  82. var eachExp = node.getAttribute(eachAttr),
  83. ctrlExp = node.getAttribute(ctrlAttr)
  84. if (eachExp) { // each block
  85. var directive = DirectiveParser.parse(eachAttr, eachExp)
  86. if (directive) {
  87. directive.el = node
  88. seed._bind(directive)
  89. }
  90. } else if (ctrlExp && !root) { // nested controllers
  91. new Seed(node, {
  92. child: true,
  93. parentSeed: seed
  94. })
  95. } else { // normal node
  96. // parse if has attributes
  97. if (node.attributes && node.attributes.length) {
  98. // forEach vs for loop perf comparison: http://jsperf.com/for-vs-foreach-case
  99. // takeaway: not worth it to wrtie manual loops.
  100. slice.call(node.attributes).forEach(function (attr) {
  101. if (attr.name === ctrlAttr) return
  102. var valid = false
  103. attr.value.split(',').forEach(function (exp) {
  104. var directive = DirectiveParser.parse(attr.name, exp)
  105. if (directive) {
  106. valid = true
  107. directive.el = node
  108. seed._bind(directive)
  109. }
  110. })
  111. if (valid) node.removeAttribute(attr.name)
  112. })
  113. }
  114. // recursively compile childNodes
  115. if (node.childNodes.length) {
  116. slice.call(node.childNodes).forEach(seed._compileNode, seed)
  117. }
  118. }
  119. }
  120. }
  121. /*
  122. * Compile a text node
  123. */
  124. SeedProto._compileTextNode = function (node) {
  125. var tokens = TextParser.parse(node)
  126. if (!tokens) return
  127. var seed = this,
  128. dirname = config.prefix + '-text',
  129. el, token, directive
  130. for (var i = 0, l = tokens.length; i < l; i++) {
  131. token = tokens[i]
  132. el = document.createTextNode()
  133. if (token.key) {
  134. directive = DirectiveParser.parse(dirname, token.key)
  135. if (directive) {
  136. directive.el = el
  137. seed._bind(directive)
  138. }
  139. } else {
  140. el.nodeValue = token
  141. }
  142. node.parentNode.insertBefore(el, node)
  143. }
  144. node.parentNode.removeChild(node)
  145. }
  146. /*
  147. * Add a directive instance to the correct binding & scope
  148. */
  149. SeedProto._bind = function (directive) {
  150. var key = directive.key,
  151. seed = directive.seed = this
  152. // deal with each block
  153. if (this.each) {
  154. if (key.indexOf(this.eachPrefix) === 0) {
  155. key = directive.key = key.replace(this.eachPrefix, '')
  156. } else {
  157. seed = this.parentSeed
  158. }
  159. }
  160. // deal with nesting
  161. seed = traceOwnerSeed(directive, seed)
  162. var binding = seed._bindings[key] || seed._createBinding(key)
  163. // add directive to this binding
  164. binding.instances.push(directive)
  165. directive.binding = binding
  166. // invoke bind hook if exists
  167. if (directive.bind) {
  168. directive.bind(binding.value)
  169. }
  170. // set initial value
  171. directive.update(binding.value)
  172. if (binding.isComputed) {
  173. directive.refresh()
  174. }
  175. }
  176. /*
  177. * Create binding and attach getter/setter for a key to the scope object
  178. */
  179. SeedProto._createBinding = function (key) {
  180. config.log(' created binding: ' + key)
  181. var binding = new Binding(this, key)
  182. this._bindings[key] = binding
  183. if (binding.isComputed) this._computed.push(binding)
  184. return binding
  185. }
  186. /*
  187. * Call unbind() of all directive instances
  188. * to remove event listeners, destroy child seeds, etc.
  189. */
  190. SeedProto._unbind = function () {
  191. var i, ins
  192. for (var key in this._bindings) {
  193. ins = this._bindings[key].instances
  194. i = ins.length
  195. while (i--) {
  196. if (ins[i].unbind) ins[i].unbind()
  197. }
  198. }
  199. }
  200. /*
  201. * Unbind and remove element
  202. */
  203. SeedProto._destroy = function () {
  204. this._unbind()
  205. this.el.parentNode.removeChild(this.el)
  206. }
  207. // Helpers --------------------------------------------------------------------
  208. /*
  209. * determine which scope a key belongs to based on nesting symbols
  210. */
  211. function traceOwnerSeed (key, seed) {
  212. if (key.nesting) {
  213. var levels = key.nesting
  214. while (seed.parentSeed && levels--) {
  215. seed = seed.parentSeed
  216. }
  217. } else if (key.root) {
  218. while (seed.parentSeed) {
  219. seed = seed.parentSeed
  220. }
  221. }
  222. return seed
  223. }
  224. module.exports = Seed