repeat.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. var Observer = require('../observer'),
  2. utils = require('../utils'),
  3. config = require('../config'),
  4. def = utils.defProtected,
  5. ViewModel // lazy def to avoid circular dependency
  6. /**
  7. * Mathods that perform precise DOM manipulation
  8. * based on mutator method triggered
  9. */
  10. var mutationHandlers = {
  11. push: function (m) {
  12. var i = 0, l = m.args.length, vm,
  13. base = this.collection.length - l
  14. for (; i < l; i++) {
  15. vm = this.buildItem(m.args[i], base + i)
  16. this.updateObject(vm, 1)
  17. }
  18. },
  19. pop: function () {
  20. var vm = this.vms.pop()
  21. if (vm) {
  22. vm.$destroy()
  23. this.updateObject(vm, -1)
  24. }
  25. },
  26. unshift: function (m) {
  27. var i = 0, l = m.args.length, vm
  28. for (; i < l; i++) {
  29. vm = this.buildItem(m.args[i], i)
  30. this.updateObject(vm, 1)
  31. }
  32. },
  33. shift: function () {
  34. var vm = this.vms.shift()
  35. if (vm) {
  36. vm.$destroy()
  37. this.updateObject(vm, -1)
  38. }
  39. },
  40. splice: function (m) {
  41. var i, l, vm,
  42. index = m.args[0],
  43. removed = m.args[1],
  44. added = m.args.length - 2,
  45. removedVMs = removed === undefined
  46. ? this.vms.splice(index)
  47. : this.vms.splice(index, removed)
  48. for (i = 0, l = removedVMs.length; i < l; i++) {
  49. removedVMs[i].$destroy()
  50. this.updateObject(removedVMs[i], -1)
  51. }
  52. for (i = 0; i < added; i++) {
  53. vm = this.buildItem(m.args[i + 2], index + i)
  54. this.updateObject(vm, 1)
  55. }
  56. },
  57. sort: function () {
  58. var vms = this.vms,
  59. col = this.collection,
  60. l = col.length,
  61. sorted = new Array(l),
  62. i, j, vm, data
  63. for (i = 0; i < l; i++) {
  64. data = col[i]
  65. for (j = 0; j < l; j++) {
  66. vm = vms[j]
  67. if (vm.$data === data) {
  68. sorted[i] = vm
  69. break
  70. }
  71. }
  72. }
  73. for (i = 0; i < l; i++) {
  74. this.container.insertBefore(sorted[i].$el, this.ref)
  75. }
  76. this.vms = sorted
  77. },
  78. reverse: function () {
  79. var vms = this.vms
  80. vms.reverse()
  81. for (var i = 0, l = vms.length; i < l; i++) {
  82. this.container.insertBefore(vms[i].$el, this.ref)
  83. }
  84. }
  85. }
  86. /**
  87. * Convert an Object to a v-repeat friendly Array
  88. */
  89. function objectToArray (obj) {
  90. var res = [], val, data
  91. for (var key in obj) {
  92. val = obj[key]
  93. data = utils.typeOf(val) === 'Object'
  94. ? val
  95. : { $value: val }
  96. def(data, '$key', key)
  97. res.push(data)
  98. }
  99. return res
  100. }
  101. /**
  102. * Find an object or a wrapped data object
  103. * from an Array
  104. */
  105. function indexOf (arr, obj) {
  106. for (var i = 0, l = arr.length; i < l; i++) {
  107. if (arr[i] === obj || (obj.$value && arr[i].$value === obj.$value)) {
  108. return i
  109. }
  110. }
  111. return -1
  112. }
  113. module.exports = {
  114. bind: function () {
  115. var el = this.el,
  116. ctn = this.container = el.parentNode
  117. // extract child VM information, if any
  118. ViewModel = ViewModel || require('../viewmodel')
  119. this.Ctor = this.Ctor || ViewModel
  120. // extract child Id, if any
  121. this.childId = utils.attr(el, 'ref')
  122. // create a comment node as a reference node for DOM insertions
  123. this.ref = document.createComment(config.prefix + '-repeat-' + this.key)
  124. ctn.insertBefore(this.ref, el)
  125. ctn.removeChild(el)
  126. this.initiated = false
  127. this.collection = null
  128. this.vms = null
  129. var self = this
  130. this.mutationListener = function (path, arr, mutation) {
  131. var method = mutation.method
  132. mutationHandlers[method].call(self, mutation)
  133. if (method !== 'push' && method !== 'pop') {
  134. // update index
  135. var i = arr.length
  136. while (i--) {
  137. self.vms[i].$index = i
  138. }
  139. }
  140. if (method === 'push' || method === 'unshift' || method === 'splice') {
  141. // recalculate dependency
  142. self.changed()
  143. }
  144. }
  145. },
  146. update: function (collection, init) {
  147. if (
  148. collection === this.collection ||
  149. collection === this.object
  150. ) return
  151. if (utils.typeOf(collection) === 'Object') {
  152. collection = this.convertObject(collection)
  153. }
  154. this.reset()
  155. // if initiating with an empty collection, we need to
  156. // force a compile so that we get all the bindings for
  157. // dependency extraction.
  158. if (!this.initiated && (!collection || !collection.length)) {
  159. this.buildItem()
  160. this.initiated = true
  161. }
  162. // keep reference of old data and VMs
  163. // so we can reuse them if possible
  164. this.old = this.collection
  165. var oldVMs = this.oldVMs = this.vms
  166. collection = this.collection = collection || []
  167. this.vms = []
  168. if (this.childId) {
  169. this.vm.$[this.childId] = this.vms
  170. }
  171. // listen for collection mutation events
  172. // the collection has been augmented during Binding.set()
  173. if (!collection.__emitter__) Observer.watchArray(collection)
  174. collection.__emitter__.on('mutate', this.mutationListener)
  175. // create new VMs and append to DOM
  176. if (collection.length) {
  177. collection.forEach(this.buildItem, this)
  178. if (!init) this.changed()
  179. }
  180. // destroy unused old VMs
  181. if (oldVMs) {
  182. var i = oldVMs.length, vm
  183. while (i--) {
  184. vm = oldVMs[i]
  185. if (vm.$reused) {
  186. vm.$reused = false
  187. } else {
  188. vm.$destroy()
  189. }
  190. }
  191. }
  192. this.old = this.oldVMs = null
  193. },
  194. /**
  195. * Notify parent compiler that new items
  196. * have been added to the collection, it needs
  197. * to re-calculate computed property dependencies.
  198. * Batched to ensure it's called only once every event loop.
  199. */
  200. changed: function () {
  201. if (this.queued) return
  202. this.queued = true
  203. var self = this
  204. utils.nextTick(function () {
  205. if (!self.compiler) return
  206. self.compiler.parseDeps()
  207. self.queued = false
  208. })
  209. },
  210. /**
  211. * Create a new child VM from a data object
  212. * passing along compiler options indicating this
  213. * is a v-repeat item.
  214. */
  215. buildItem: function (data, index) {
  216. var ctn = this.container,
  217. vms = this.vms,
  218. col = this.collection,
  219. el, i, existing, ref, item, primitive, detached
  220. // append node into DOM first
  221. // so v-if can get access to parentNode
  222. // TODO: logic here is a total mess.
  223. if (data) {
  224. if (this.old) {
  225. i = indexOf(this.old, data)
  226. }
  227. existing = i > -1
  228. if (existing) { // existing, reuse the old VM
  229. item = this.oldVMs[i]
  230. // mark, so it won't be destroyed
  231. item.$reused = true
  232. el = item.$el
  233. // existing VM's el can possibly be detached by v-if.
  234. // in that case don't insert.
  235. detached = !el.parentNode
  236. } else { // new data, need to create new VM
  237. el = this.el.cloneNode(true)
  238. // process transition info before appending
  239. el.vue_trans = utils.attr(el, 'transition', true)
  240. el.vue_anim = utils.attr(el, 'animation', true)
  241. el.vue_effect = utils.attr(el, 'effect', true)
  242. // wrap primitive element in an object
  243. if (utils.typeOf(data) !== 'Object') {
  244. primitive = true
  245. data = { $value: data }
  246. }
  247. }
  248. ref = vms.length > index
  249. ? vms[index].$el
  250. : this.ref
  251. // if ref VM's el is detached by v-if
  252. // use its v-if ref node instead
  253. if (!ref.parentNode) {
  254. ref = ref.vue_if_ref
  255. }
  256. if (existing) {
  257. // existing node
  258. // if not detached, just re-insert to new location
  259. // else re-insert its v-if ref node
  260. ctn.insertBefore(detached ? el.vue_if_ref : el, ref)
  261. } else {
  262. // new node, prepare it for v-if
  263. el.vue_if_parent = ctn
  264. el.vue_if_ref = ref
  265. }
  266. // set index so vm can init with it
  267. // and do not trigger stuff early
  268. data.$index = index
  269. }
  270. item = item || new this.Ctor({
  271. el: el,
  272. data: data,
  273. compilerOptions: {
  274. repeat: true,
  275. parentCompiler: this.compiler
  276. }
  277. })
  278. item.$index = index
  279. if (!data) {
  280. // this is a forced compile for an empty collection.
  281. // let's remove it...
  282. item.$destroy()
  283. } else {
  284. vms.splice(index, 0, item)
  285. // for primitive values, listen for value change
  286. if (primitive) {
  287. item.$compiler.observer.on('set', function (key, val) {
  288. if (key === '$value') {
  289. col[item.$index] = val
  290. }
  291. })
  292. }
  293. // new instance and v-if doesn't want it detached
  294. // good to insert.
  295. if (!existing && el.vue_if !== false) {
  296. if (this.compiler.init) {
  297. // do not transition on initial compile.
  298. ctn.insertBefore(item.$el, ref)
  299. item.$compiler.execHook('attached')
  300. } else {
  301. // transition in...
  302. item.$before(ref)
  303. }
  304. }
  305. }
  306. return item
  307. },
  308. /**
  309. * Convert an object to a repeater Array
  310. * and make sure changes in the object are synced to the repeater
  311. */
  312. convertObject: function (object) {
  313. if (this.object) {
  314. this.object.__emitter__.off('set', this.updateRepeater)
  315. }
  316. this.object = object
  317. var collection = object.$repeater || objectToArray(object)
  318. if (!object.$repeater) {
  319. def(object, '$repeater', collection)
  320. }
  321. var self = this
  322. this.updateRepeater = function (key, val) {
  323. if (key.indexOf('.') === -1) {
  324. var i = self.vms.length, item
  325. while (i--) {
  326. item = self.vms[i]
  327. if (item.$key === key) {
  328. if (item.$data !== val && item.$value !== val) {
  329. if ('$value' in item) {
  330. item.$value = val
  331. } else {
  332. item.$data = val
  333. }
  334. }
  335. break
  336. }
  337. }
  338. }
  339. }
  340. object.__emitter__.on('set', this.updateRepeater)
  341. return collection
  342. },
  343. /**
  344. * Sync changes from the $repeater Array
  345. * back to the represented Object
  346. */
  347. updateObject: function (vm, action) {
  348. var obj = this.object
  349. if (obj && vm.$key) {
  350. var key = vm.$key,
  351. val = vm.$value || vm.$data
  352. if (action > 0) { // new property
  353. // make key ienumerable
  354. delete vm.$data.$key
  355. obj[key] = val
  356. Observer.convert(obj, key)
  357. } else {
  358. delete obj[key]
  359. }
  360. obj.__emitter__.emit('set', key, val, true)
  361. }
  362. },
  363. reset: function (destroyAll) {
  364. if (this.childId) {
  365. delete this.vm.$[this.childId]
  366. }
  367. if (this.collection) {
  368. this.collection.__emitter__.off('mutate', this.mutationListener)
  369. if (destroyAll) {
  370. var i = this.vms.length
  371. while (i--) {
  372. this.vms[i].$destroy()
  373. }
  374. }
  375. }
  376. },
  377. unbind: function () {
  378. this.reset(true)
  379. }
  380. }