var _ = require('../util') var isObject = _.isObject var textParser = require('../parsers/text') var expParser = require('../parsers/expression') var templateParser = require('../parsers/template') var compile = require('../compiler/compile') var transclude = require('../compiler/transclude') var mergeOptions = require('../util/merge-option') var uid = 0 module.exports = { /** * Setup. */ bind: function () { // uid as a cache identifier this.id = '__v_repeat_' + (++uid) // we need to insert the objToArray converter // as the first read filter. if (!this.filters) { this.filters = {} } // add the object -> array convert filter var objectConverter = _.bind(objToArray, this) if (!this.filters.read) { this.filters.read = [objectConverter] } else { this.filters.read.unshift(objectConverter) } // setup ref node this.ref = document.createComment('v-repeat') _.replace(this.el, this.ref) // check if this is a block repeat this.template = this.el.tagName === 'TEMPLATE' ? templateParser.parse(this.el, true) : this.el // check other directives that need to be handled // at v-repeat level this.checkIf() this.checkRef() this.checkComponent() // check for trackby param this.idKey = this._checkParam('track-by') || this._checkParam('trackby') // 0.11.0 compat // cache for primitive value instances this.cache = Object.create(null) }, /** * Warn against v-if usage. */ checkIf: function () { if (_.attr(this.el, 'if') !== null) { _.warn( 'Don\'t use v-if with v-repeat. ' + 'Use v-show or the "filterBy" filter instead.' ) } }, /** * Check if v-ref/ v-el is also present. */ checkRef: function () { var childId = _.attr(this.el, 'ref') this.childId = childId ? this.vm.$interpolate(childId) : null var elId = _.attr(this.el, 'el') this.elId = elId ? this.vm.$interpolate(elId) : null }, /** * Check the component constructor to use for repeated * instances. If static we resolve it now, otherwise it * needs to be resolved at build time with actual data. */ checkComponent: function () { var id = _.attr(this.el, 'component') var options = this.vm.$options if (!id) { this.Ctor = _.Vue // default constructor this.inherit = true // inline repeats should inherit // important: transclude with no options, just // to ensure block start and block end this.template = transclude(this.template) this._linker = compile(this.template, options) } else { var tokens = textParser.parse(id) if (!tokens) { // static component var Ctor = this.Ctor = options.components[id] _.assertAsset(Ctor, 'component', id) if (Ctor) { // merge an empty object with owner vm as parent // so child vms can access parent assets. var merged = mergeOptions( Ctor.options, {}, { $parent: this.vm } ) this.template = transclude(this.template, merged) this._linker = compile(this.template, merged) } } else { // to be resolved later var ctorExp = textParser.tokensToExp(tokens) this.ctorGetter = expParser.parse(ctorExp).get } } }, /** * Update. * This is called whenever the Array mutates. * * @param {Array} data */ update: function (data) { if (typeof data === 'number') { data = range(data) } this.vms = this.diff(data || [], this.vms) // update v-ref if (this.childId) { this.vm.$[this.childId] = this.vms } if (this.elId) { this.vm.$$[this.elId] = this.vms.map(function (vm) { return vm.$el }) } }, /** * Diff, based on new data and old data, determine the * minimum amount of DOM manipulations needed to make the * DOM reflect the new data Array. * * The algorithm diffs the new data Array by storing a * hidden reference to an owner vm instance on previously * seen data. This allows us to achieve O(n) which is * better than a levenshtein distance based algorithm, * which is O(m * n). * * @param {Array} data * @param {Array} oldVms * @return {Array} */ diff: function (data, oldVms) { var idKey = this.idKey var converted = this.converted var ref = this.ref var alias = this.arg var init = !oldVms var vms = new Array(data.length) var obj, raw, vm, i, l // First pass, go through the new Array and fill up // the new vms array. If a piece of data has a cached // instance for it, we reuse it. Otherwise build a new // instance. for (i = 0, l = data.length; i < l; i++) { obj = data[i] raw = converted ? obj.value : obj vm = !init && this.getVm(raw) if (vm) { // reusable instance vm._reused = true vm.$index = i // update $index if (converted) { vm.$key = obj.key // update $key } if (idKey) { // swap track by id data if (alias) { vm[alias] = raw } else { vm._setData(raw) } } } else { // new instance vm = this.build(obj, i) vm._new = true } vms[i] = vm // insert if this is first run if (init) { vm.$before(ref) } } // if this is the first run, we're done. if (init) { return vms } // Second pass, go through the old vm instances and // destroy those who are not reused (and remove them // from cache) for (i = 0, l = oldVms.length; i < l; i++) { vm = oldVms[i] if (!vm._reused) { this.uncacheVm(vm) vm.$destroy(true) } } // final pass, move/insert new instances into the // right place. We're going in reverse here because // insertBefore relies on the next sibling to be // resolved. var targetNext, currentNext i = vms.length while (i--) { vm = vms[i] // this is the vm that we should be in front of targetNext = vms[i + 1] if (!targetNext) { // This is the last item. If it's reused then // everything else will eventually be in the right // place, so no need to touch it. Otherwise, insert // it. if (!vm._reused) { vm.$before(ref) } } else { if (vm._reused) { // this is the vm we are actually in front of currentNext = findNextVm(vm, ref) // we only need to move if we are not in the right // place already. if (currentNext !== targetNext) { vm.$before(targetNext.$el, null, false) } } else { // new instance, insert to existing next vm.$before(targetNext.$el) } } vm._new = false vm._reused = false } return vms }, /** * Build a new instance and cache it. * * @param {Object} data * @param {Number} index */ build: function (data, index) { var original = data var meta = { $index: index } if (this.converted) { meta.$key = original.key } var raw = this.converted ? data.value : data var alias = this.arg var hasAlias = !isObject(raw) || alias // wrap the raw data with alias data = hasAlias ? {} : raw if (alias) { data[alias] = raw } else if (hasAlias) { meta.$value = raw } // resolve constructor var Ctor = this.Ctor || this.resolveCtor(data, meta) var vm = this.vm.$addChild({ el: templateParser.clone(this.template), _linker: this._linker, _meta: meta, data: data, inherit: this.inherit }, Ctor) // cache instance this.cacheVm(raw, vm) return vm }, /** * Resolve a contructor to use for an instance. * The tricky part here is that there could be dynamic * components depending on instance data. * * @param {Object} data * @param {Object} meta * @return {Function} */ resolveCtor: function (data, meta) { // create a temporary context object and copy data // and meta properties onto it. // use _.define to avoid accidentally overwriting scope // properties. var context = Object.create(this.vm) var key for (key in data) { _.define(context, key, data[key]) } for (key in meta) { _.define(context, key, meta[key]) } var id = this.ctorGetter.call(context, context) var Ctor = this.vm.$options.components[id] _.assertAsset(Ctor, 'component', id) return Ctor }, /** * Unbind, teardown everything */ unbind: function () { if (this.childId) { delete this.vm.$[this.childId] } if (this.vms) { var i = this.vms.length var vm while (i--) { vm = this.vms[i] this.uncacheVm(vm) vm.$destroy() } } }, /** * Cache a vm instance based on its data. * * If the data is an object, we save the vm's reference on * the data object as a hidden property. Otherwise we * cache them in an object and for each primitive value * there is an array in case there are duplicates. * * @param {Object} data * @param {Vue} vm */ cacheVm: function (data, vm) { var idKey = this.idKey var cache = this.cache var id if (idKey) { id = data[idKey] if (!cache[id]) { cache[id] = vm } else { _.warn('Duplicate ID in v-repeat: ' + id) } } else if (isObject(data)) { id = this.id if (data.hasOwnProperty(id)) { if (data[id] === null) { data[id] = vm } else { _.warn( 'Duplicate objects are not supported in v-repeat.' ) } } else { _.define(data, this.id, vm) } } else { if (!cache[data]) { cache[data] = [vm] } else { cache[data].push(vm) } } vm._raw = data }, /** * Try to get a cached instance from a piece of data. * * @param {Object} data * @return {Vue|undefined} */ getVm: function (data) { if (this.idKey) { return this.cache[data[this.idKey]] } else if (isObject(data)) { return data[this.id] } else { var cached = this.cache[data] if (cached) { var i = 0 var vm = cached[i] // since duplicated vm instances might be a reused // one OR a newly created one, we need to return the // first instance that is neither of these. while (vm && (vm._reused || vm._new)) { vm = cached[++i] } return vm } } }, /** * Delete a cached vm instance. * * @param {Vue} vm */ uncacheVm: function (vm) { var data = vm._raw if (this.idKey) { this.cache[data[this.idKey]] = null } else if (isObject(data)) { data[this.id] = null vm._raw = null } else { this.cache[data].pop() } } } /** * Helper to find the next element that is an instance * root node. This is necessary because a destroyed vm's * element could still be lingering in the DOM before its * leaving transition finishes, but its __vue__ reference * should have been removed so we can skip them. * * @param {Vue} vm * @param {CommentNode} ref * @return {Vue} */ function findNextVm (vm, ref) { var el = (vm._blockEnd || vm.$el).nextSibling while (!el.__vue__ && el !== ref) { el = el.nextSibling } return el.__vue__ } /** * Attempt to convert non-Array objects to array. * This is the default filter installed to every v-repeat * directive. * * It will be called with **the directive** as `this` * context so that we can mark the repeat array as converted * from an object. * * @param {*} obj * @return {Array} * @private */ function objToArray (obj) { if (!_.isPlainObject(obj)) { return obj } var keys = Object.keys(obj) var i = keys.length var res = new Array(i) var key while (i--) { key = keys[i] res[i] = { key: key, value: obj[key] } } // `this` points to the repeat directive instance this.converted = true return res } /** * Create a range array from given number. * * @param {Number} n * @return {Array} */ function range (n) { var i = -1 var ret = new Array(n) while (++i < n) { ret[i] = i } return ret }