var utils = require('../utils'), config = require('../config') /** * Binding that manages VMs based on an Array */ module.exports = { bind: function () { this.identifier = '$r' + this.id // a hash to cache the same expressions on repeated instances // so they don't have to be compiled for every single instance this.expCache = utils.hash() var el = this.el, ctn = this.container = el.parentNode // extract child Id, if any this.childId = this.compiler.eval(utils.attr(el, 'ref')) // create a comment node as a reference node for DOM insertions this.ref = document.createComment(config.prefix + '-repeat-' + this.key) ctn.insertBefore(this.ref, el) ctn.removeChild(el) this.collection = null this.vms = null }, update: function (collection) { if (!Array.isArray(collection)) { if (utils.isObject(collection)) { collection = utils.objectToArray(collection) } else { utils.warn('v-repeat only accepts Array or Object values.') } } // keep reference of old data and VMs // so we can reuse them if possible this.oldVMs = this.vms this.oldCollection = this.collection collection = this.collection = collection || [] var isObject = collection[0] && utils.isObject(collection[0]) this.vms = this.oldCollection ? this.diff(collection, isObject) : this.init(collection, isObject) if (this.childId) { this.vm.$[this.childId] = this.vms } }, init: function (collection, isObject) { var vm, vms = [] for (var i = 0, l = collection.length; i < l; i++) { vm = this.build(collection[i], i, isObject) vms.push(vm) if (this.compiler.init) { this.container.insertBefore(vm.$el, this.ref) } else { vm.$before(this.ref) } } return vms }, /** * Diff the new array with the old * and determine the minimum amount of DOM manipulations. */ diff: function (newCollection, isObject) { var i, l, item, vm, oldIndex, targetNext, currentNext, nextEl, ctn = this.container, oldVMs = this.oldVMs, vms = [] vms.length = newCollection.length // first pass, collect new reused and new created for (i = 0, l = newCollection.length; i < l; i++) { item = newCollection[i] if (isObject) { item.$index = i if (item.__emitter__ && item.__emitter__[this.identifier]) { // this piece of data is being reused. // record its final position in reused vms item.$reused = true } else { vms[i] = this.build(item, i, isObject) } } else { // we can't attach an identifier to primitive values // so have to do an indexOf... oldIndex = indexOf(oldVMs, item) if (oldIndex > -1) { // record the position on the existing vm oldVMs[oldIndex].$reused = true oldVMs[oldIndex].$data.$index = i } else { vms[i] = this.build(item, i, isObject) } } } // second pass, collect old reused and destroy unused for (i = 0, l = oldVMs.length; i < l; i++) { vm = oldVMs[i] item = this.arg ? vm.$data[this.arg] : vm.$data if (item.$reused) { vm.$reused = true delete item.$reused } if (vm.$reused) { // update the index to latest vm.$index = item.$index // the item could have had a new key if (item.$key && item.$key !== vm.$key) { vm.$key = item.$key } vms[vm.$index] = vm } else { // this one can be destroyed. if (item.__emitter__) { delete item.__emitter__[this.identifier] } vm.$destroy() } } // final pass, move/insert DOM elements i = vms.length while (i--) { vm = vms[i] item = vm.$data targetNext = vms[i + 1] if (vm.$reused) { nextEl = vm.$el.nextSibling // destroyed VMs' element might still be in the DOM // due to transitions while (!nextEl.vue_vm && nextEl !== this.ref) { nextEl = nextEl.nextSibling } currentNext = nextEl.vue_vm if (currentNext !== targetNext) { if (!targetNext) { ctn.insertBefore(vm.$el, this.ref) } else { nextEl = targetNext.$el // new VMs' element might not be in the DOM yet // due to transitions while (!nextEl.parentNode) { targetNext = vms[nextEl.vue_vm.$index + 1] nextEl = targetNext ? targetNext.$el : this.ref } ctn.insertBefore(vm.$el, nextEl) } } delete vm.$reused delete item.$index delete item.$key } else { // a new vm vm.$before(targetNext ? targetNext.$el : this.ref) } } return vms }, build: function (data, index, isObject) { // wrap non-object values var raw, alias, wrap = !isObject || this.arg if (wrap) { raw = data alias = this.arg || '$value' data = {} data[alias] = raw } data.$index = index var el = this.el.cloneNode(true), Ctor = this.compiler.resolveComponent(el, data), vm = new Ctor({ el: el, data: data, parent: this.vm, compilerOptions: { repeat: true, expCache: this.expCache } }) if (isObject) { // attach an ienumerable identifier to the raw data (raw || data).__emitter__[this.identifier] = true } return vm }, unbind: function () { if (this.childId) { delete this.vm.$[this.childId] } if (this.vms) { var i = this.vms.length while (i--) { this.vms[i].$destroy() } } } } // Helpers -------------------------------------------------------------------- /** * Find an object or a wrapped data object * from an Array */ function indexOf (vms, obj) { for (var vm, i = 0, l = vms.length; i < l; i++) { vm = vms[i] if (!vm.$reused && vm.$value === obj) { return i } } return -1 }