var _ = require('../util') var isObject = _.isObject var isPlainObject = _.isPlainObject 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 uid = 0 // async component resolution states var UNRESOLVED = 0 var PENDING = 1 var RESOLVED = 2 var ABORTED = 3 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, because it has to be invoked // before any user filters. (can't do it in `update`) 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 anchor node this.anchor = _.createAnchor('v-repeat') _.replace(this.el, this.anchor) // 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 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 refID = _.attr(this.el, 'ref') this.refID = refID ? this.vm.$interpolate(refID) : 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 () { this.componentState = UNRESOLVED var options = this.vm.$options var id = _.checkComponent(this.el, options) if (!id) { // default constructor this.Ctor = _.Vue // inline repeats should inherit this.inherit = true // important: transclude with no options, just // to ensure block start and block end this.template = transclude(this.template) var copy = _.extend({}, options) copy._asComponent = false this._linkFn = compile(this.template, copy) } else { this.Ctor = null this.asComponent = true // check inline-template if (this._checkParam('inline-template') !== null) { // extract inline template as a DocumentFragment this.inlineTempalte = _.extractContent(this.el, true) } var tokens = textParser.parse(id) if (tokens) { // dynamic component to be resolved later var ctorExp = textParser.tokensToExp(tokens) this.ctorGetter = expParser.parse(ctorExp).get } else { // static this.componentId = id this.pendingData = null } } }, resolveComponent: function () { this.componentState = PENDING this.vm._resolveComponent(this.componentId, _.bind(function (Ctor) { if (this.componentState === ABORTED) { return } this.Ctor = Ctor var merged = _.mergeOptions(Ctor.options, {}, { $parent: this.vm }) merged.template = this.inlineTempalte || merged.template merged._asComponent = true merged._parent = this.vm this.template = transclude(this.template, merged) // Important: mark the template as a root node so that // custom element components don't get compiled twice. // fixes #822 this.template.__vue__ = true this._linkFn = compile(this.template, merged) this.componentState = RESOLVED this.realUpdate(this.pendingData) this.pendingData = null }, this)) }, /** * Resolve a dynamic component 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} */ resolveDynamicComponent: 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 = _.resolveAsset(this.vm.$options, 'components', id) _.assertAsset(Ctor, 'component', id) if (!Ctor.options) { _.warn( 'Async resolution is not supported for v-repeat ' + '+ dynamic component. (component: ' + id + ')' ) return _.Vue } return Ctor }, /** * Update. * This is called whenever the Array mutates. If we have * a component, we might need to wait for it to resolve * asynchronously. * * @param {Array|Number|String} data */ update: function (data) { if (this.componentId) { var state = this.componentState if (state === UNRESOLVED) { this.pendingData = data // once resolved, it will call realUpdate this.resolveComponent() } else if (state === PENDING) { this.pendingData = data } else if (state === RESOLVED) { this.realUpdate(data) } } else { this.realUpdate(data) } }, /** * The real update that actually modifies the DOM. * * @param {Array|Number|String} data */ realUpdate: function (data) { data = data || [] var type = typeof data if (type === 'number') { data = range(data) } else if (type === 'string') { data = _.toArray(data) } this.vms = this.diff(data, this.vms) // update v-ref if (this.refID) { this.vm.$[this.refID] = 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 anchor = this.anchor 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, converted ? obj.$key : null) if (vm) { // reusable instance vm._reused = true vm.$index = i // update $index // update data for track-by or object repeat, // since in these two cases the data is replaced // rather than mutated. if (idKey || converted) { if (alias) { vm[alias] = raw } else if (_.isPlainObject(raw)) { vm.$data = raw } else { vm.$value = raw } } } else { // new instance vm = this.build(obj, i, true) // the _new flag is used in the second pass for // vm cache retrival, but if this is the init phase // the flag can just be set to false directly. vm._new = !init vm._reused = false } vms[i] = vm // insert if this is first run if (init) { vm.$before(anchor) } } // 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(anchor) } } else { var nextEl = targetNext.$el if (vm._reused) { // this is the vm we are actually in front of currentNext = findNextVm(vm, anchor) // we only need to move if we are not in the right // place already. if (currentNext !== targetNext) { vm.$before(nextEl, null, false) } } else { // new instance, insert to existing next vm.$before(nextEl) } } vm._new = false vm._reused = false } return vms }, /** * Build a new instance and cache it. * * @param {Object} data * @param {Number} index * @param {Boolean} needCache */ build: function (data, index, needCache) { var meta = { $index: index } if (this.converted) { meta.$key = data.$key } var raw = this.converted ? data.$value : data var alias = this.arg if (alias) { data = {} data[alias] = raw } else if (!isPlainObject(raw)) { // non-object values data = {} meta.$value = raw } else { // default data = raw } // resolve constructor var Ctor = this.Ctor || this.resolveDynamicComponent(data, meta) var vm = this.vm.$addChild({ el: templateParser.clone(this.template), _asComponent: this.asComponent, _host: this._host, _linkFn: this._linkFn, _meta: meta, data: data, inherit: this.inherit, template: this.inlineTempalte }, Ctor) // flag this instance as a repeat instance // so that we can skip it in vm._digest vm._repeat = true // cache instance if (needCache) { this.cacheVm(raw, vm, this.converted ? meta.$key : null) } // sync back changes for two-way bindings of primitive values var type = typeof raw if (type === 'string' || type === 'number') { var dir = this vm.$watch(alias || '$value', function (val) { dir._withLock(function () { if (dir.converted) { dir.rawValue[vm.$key] = val } else { dir.rawValue.$set(vm.$index, val) } }) }) } return vm }, /** * Unbind, teardown everything */ unbind: function () { this.componentState = ABORTED if (this.refID) { this.vm.$[this.refID] = null } 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 * @param {String} [key] */ cacheVm: function (data, vm, key) { var idKey = this.idKey var cache = this.cache var id if (key || idKey) { id = idKey ? data[idKey] : key if (!cache[id]) { cache[id] = vm } else { _.warn('Duplicate track-by key 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 ' + 'when using components or transitions.' ) } } else { _.define(data, 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 * @param {String} [key] * @return {Vue|undefined} */ getVm: function (data, key) { var idKey = this.idKey if (key || idKey) { var id = idKey ? data[idKey] : key return this.cache[id] } 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 var idKey = this.idKey if (idKey || this.converted) { var id = idKey ? data[idKey] : vm.$key this.cache[id] = 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 {Comment|Text} anchor * @return {Vue} */ function findNextVm (vm, anchor) { var el = (vm._blockEnd || vm.$el).nextSibling while (!el.__vue__ && el !== anchor) { 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) { // regardless of type, store the un-filtered raw value. this.rawValue = 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 }