vue2/src/directives/repeat.js

780 lines
20 KiB
JavaScript

var _ = require('../util')
var config = require('../config')
var isObject = _.isObject
var isPlainObject = _.isPlainObject
var transition = require('../transition')
var textParser = require('../parsers/text')
var expParser = require('../parsers/expression')
var templateParser = require('../parsers/template')
var compiler = require('../compiler')
var uid = 0
// async component resolution states
var UNRESOLVED = 0
var PENDING = 1
var RESOLVED = 2
var ABORTED = 3
module.exports = {
priority: 2000,
/**
* Setup.
*/
bind: function () {
// some helpful tips...
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' &&
this.el.tagName === 'OPTION' &&
this.el.parentNode && this.el.parentNode.__v_model
) {
_.warn(
'Don\'t use v-repeat for v-model options; ' +
'use the `options` param instead: ' +
'http://vuejs.org/guide/forms.html#Dynamic_Select_Options'
)
}
if (process.env.NODE_ENV !== 'production') {
_.deprecation.REPEAT()
}
// support for item in array syntax
var inMatch = this.expression.match(/(.*) in (.*)/)
if (inMatch) {
this.arg = inMatch[1]
this._watcherExp = inMatch[2]
}
// uid as a cache identifier
this.id = '__v_repeat_' + (++uid)
// setup anchor nodes
this.start = _.createAnchor('v-repeat-start')
this.end = _.createAnchor('v-repeat-end')
_.replace(this.el, this.end)
_.before(this.start, this.end)
// check if this is a block repeat
this.template = _.isTemplate(this.el)
? templateParser.parse(this.el, true)
: this.el
// check for trackby param
this.idKey = this.param('track-by')
// check for transition stagger
var stagger = +this.param('stagger')
this.enterStagger = +this.param('enter-stagger') || stagger
this.leaveStagger = +this.param('leave-stagger') || stagger
// check for v-ref/v-el
this.refId = this.param(config.prefix + 'ref')
this.elID = this.param(config.prefix + 'el')
if (process.env.NODE_ENV !== 'production') {
if (this.refId) _.deprecation.V_REF()
if (this.elID) _.deprecation.V_EL()
}
this.refId = this.refId || this.param('ref')
// check other directives that need to be handled
// at v-repeat level
this.checkIf()
this.checkComponent()
// create cache object
this.cache = Object.create(null)
},
/**
* Warn against v-if usage.
*/
checkIf: function () {
if (_.attr(this.el, 'if') !== null) {
process.env.NODE_ENV !== 'production' && _.warn(
'Don\'t use v-if with v-repeat. ' +
'Use v-show or the "filterBy" filter instead.'
)
}
},
/**
* 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.Component = _.Vue
// inline repeats should inherit
this.inline = true
// important: transclude with no options, just
// to ensure block start and block end
this.template = compiler.transclude(this.template)
var copy = _.extend({}, options)
copy._asComponent = false
this._linkFn = compiler.compile(this.template, copy)
} else {
this.Component = null
this.asComponent = true
// check inline-template
if (this.param('inline-template') !== null) {
// extract inline template as a DocumentFragment
this.inlineTemplate = _.extractContent(this.el, true)
}
var tokens = textParser.parse(id)
if (tokens) {
// dynamic component to be resolved later
var componentExp = textParser.tokensToExp(tokens)
this.componentGetter = expParser.parse(componentExp).get
} else {
// static
this.componentId = id
this.pendingData = null
}
}
},
resolveComponent: function () {
this.componentState = PENDING
this.vm._resolveComponent(this.componentId, _.bind(function (Component) {
if (this.componentState === ABORTED) {
return
}
this.Component = Component
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.componentGetter.call(context, context)
var Component = _.resolveAsset(this.vm.$options, 'components', id)
if (process.env.NODE_ENV !== 'production') {
_.assertAsset(Component, 'component', id)
}
if (!Component.options) {
process.env.NODE_ENV !== 'production' && _.warn(
'Async resolution is not supported for v-repeat ' +
'+ dynamic component. (component: ' + id + ')'
)
return _.Vue
}
return Component
},
/**
* 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) {
this.vms = this.diff(data, this.vms)
// update v-ref
if (this.refId) {
this.vm.$[this.refId] = this.converted
? toRefObject(this.vms)
: 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 start = this.start
var end = this.end
var inDoc = _.inDoc(start)
var alias = this.arg
var init = !oldVms
var vms = new Array(data.length)
var obj, raw, vm, i, l, primitive
// 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
primitive = !isObject(raw)
vm = !init && this.getVm(raw, i, converted ? obj.$key : null)
if (vm) { // reusable instance
if (process.env.NODE_ENV !== 'production' && vm._reused) {
_.warn(
'Duplicate objects found in v-repeat="' + this.expression + '": ' +
JSON.stringify(raw)
)
}
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 || primitive) {
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)
vm._reused = false
}
vms[i] = vm
// insert if this is first run
if (init) {
vm.$before(end)
}
}
// 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)
var removalIndex = 0
var totalRemoved = oldVms.length - vms.length
for (i = 0, l = oldVms.length; i < l; i++) {
vm = oldVms[i]
if (!vm._reused) {
this.uncacheVm(vm)
vm.$destroy(false, true) // defer cleanup until removal
this.remove(vm, removalIndex++, totalRemoved, inDoc)
}
}
// final pass, move/insert new instances into the
// right place.
var targetPrev, prevEl, currentPrev
var insertionIndex = 0
for (i = 0, l = vms.length; i < l; i++) {
vm = vms[i]
// this is the vm that we should be after
targetPrev = vms[i - 1]
prevEl = targetPrev
? targetPrev._staggerCb
? targetPrev._staggerAnchor
: targetPrev._fragmentEnd || targetPrev.$el
: start
if (vm._reused && !vm._staggerCb) {
currentPrev = findPrevVm(vm, start, this.id)
if (currentPrev !== targetPrev) {
this.move(vm, prevEl)
}
} else {
// new instance, or still in stagger.
// insert with updated stagger index.
this.insert(vm, insertionIndex++, prevEl, inDoc)
}
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 Component = this.Component || this.resolveDynamicComponent(data, meta)
var parent = this._host || this.vm
var vm = parent.$addChild({
el: templateParser.clone(this.template),
data: data,
inherit: this.inline,
template: this.inlineTemplate,
// repeater meta, e.g. $index, $key
_meta: meta,
// mark this as an inline-repeat instance
_repeat: this.inline,
// is this a component?
_asComponent: this.asComponent,
// linker cachable if no inline-template
_linkerCachable: !this.inlineTemplate && Component !== _.Vue,
// pre-compiled linker for simple repeats
_linkFn: this._linkFn,
// identifier, shows that this vm belongs to this collection
_repeatId: this.id,
// transclusion content owner
_context: this.vm,
// cotnext fragment
_frag: this._frag
}, Component)
// cache instance
if (needCache) {
this.cacheVm(raw, vm, index, this.converted ? meta.$key : null)
}
// sync back changes for two-way bindings of primitive values
var dir = this
if (this.rawType === 'object' && isPrimitive(raw)) {
vm.$watch(alias || '$value', function (val) {
if (dir.filters) {
process.env.NODE_ENV !== 'production' && _.warn(
'You seem to be mutating the $value reference of ' +
'a v-repeat instance (likely through v-model) ' +
'and filtering the v-repeat at the same time. ' +
'This will not work properly with an Array of ' +
'primitive values. Please use an Array of ' +
'Objects instead.'
)
}
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 {Number} index
* @param {String} [key]
*/
cacheVm: function (data, vm, index, key) {
var idKey = this.idKey
var cache = this.cache
var primitive = !isObject(data)
var id
if (key || idKey || primitive) {
id = idKey
? idKey === '$index'
? index
: data[idKey]
: (key || index)
if (!cache[id]) {
cache[id] = vm
} else if (!primitive && idKey !== '$index') {
process.env.NODE_ENV !== 'production' && _.warn(
'Duplicate objects with the same track-by key in v-repeat: ' + id
)
}
} else {
id = this.id
if (data.hasOwnProperty(id)) {
if (data[id] === null) {
data[id] = vm
} else {
process.env.NODE_ENV !== 'production' && _.warn(
'Duplicate objects found in v-repeat="' + this.expression + '": ' +
JSON.stringify(data)
)
}
} else {
_.define(data, id, vm)
}
}
vm._raw = data
},
/**
* Try to get a cached instance from a piece of data.
*
* @param {Object} data
* @param {Number} index
* @param {String} [key]
* @return {Vue|undefined}
*/
getVm: function (data, index, key) {
var idKey = this.idKey
var primitive = !isObject(data)
if (key || idKey || primitive) {
var id = idKey
? idKey === '$index'
? index
: data[idKey]
: (key || index)
return this.cache[id]
} else {
return data[this.id]
}
},
/**
* Delete a cached vm instance.
*
* @param {Vue} vm
*/
uncacheVm: function (vm) {
var data = vm._raw
var idKey = this.idKey
var index = vm.$index
// fix #948: avoid accidentally fall through to
// a parent repeater which happens to have $key.
var key = vm.hasOwnProperty('$key') && vm.$key
var primitive = !isObject(data)
if (idKey || key || primitive) {
var id = idKey
? idKey === '$index'
? index
: data[idKey]
: (key || index)
this.cache[id] = null
} else {
data[this.id] = null
vm._raw = null
}
},
/**
* Insert an instance.
*
* @param {Vue} vm
* @param {Number} index
* @param {Node} prevEl
* @param {Boolean} inDoc
*/
insert: function (vm, index, prevEl, inDoc) {
if (vm._staggerCb) {
vm._staggerCb.cancel()
vm._staggerCb = null
}
var staggerAmount = this.getStagger(vm, index, null, 'enter')
if (inDoc && staggerAmount) {
// create an anchor and insert it synchronously,
// so that we can resolve the correct order without
// worrying about some elements not inserted yet
var anchor = vm._staggerAnchor
if (!anchor) {
anchor = vm._staggerAnchor = _.createAnchor('stagger-anchor')
anchor.__vue__ = vm
}
_.after(anchor, prevEl)
var op = vm._staggerCb = _.cancellable(function () {
vm._staggerCb = null
vm.$before(anchor)
_.remove(anchor)
})
setTimeout(op, staggerAmount)
} else {
vm.$after(prevEl)
}
},
/**
* Move an already inserted instance.
*
* @param {Vue} vm
* @param {Node} prevEl
*/
move: function (vm, prevEl) {
vm.$after(prevEl, null, false)
},
/**
* Remove an instance.
*
* @param {Vue} vm
* @param {Number} index
* @param {Boolean} inDoc
*/
remove: function (vm, index, total, inDoc) {
if (vm._staggerCb) {
vm._staggerCb.cancel()
vm._staggerCb = null
// it's not possible for the same vm to be removed
// twice, so if we have a pending stagger callback,
// it means this vm is queued for enter but removed
// before its transition started. Since it is already
// destroyed, we can just leave it in detached state.
return
}
var staggerAmount = this.getStagger(vm, index, total, 'leave')
if (inDoc && staggerAmount) {
var op = vm._staggerCb = _.cancellable(function () {
vm._staggerCb = null
remove()
})
setTimeout(op, staggerAmount)
} else {
remove()
}
function remove () {
vm.$remove(function () {
vm._cleanup()
})
}
},
/**
* Get the stagger amount for an insertion/removal.
*
* @param {Vue} vm
* @param {Number} index
* @param {String} type
* @param {Number} total
*/
getStagger: function (vm, index, total, type) {
type = type + 'Stagger'
var trans = transition.get(vm.$el, vm)
var hooks = trans && trans.hooks
var hook = hooks && (hooks[type] || hooks.stagger)
return hook
? hook.call(vm, index, total)
: index * this[type]
},
/**
* Pre-process the value before piping it through the
* filters, and convert non-Array objects to arrays.
*
* This function will be bound to this directive instance
* and passed into the watcher.
*
* @param {*} value
* @return {Array}
* @private
*/
_preProcess: function (value) {
// regardless of type, store the un-filtered raw value.
this.rawValue = value
var type = this.rawType = typeof value
if (!isPlainObject(value)) {
this.converted = false
if (type === 'number') {
value = range(value)
} else if (type === 'string') {
value = _.toArray(value)
}
return value || []
} else {
// convert plain object to array.
var keys = Object.keys(value)
var i = keys.length
var res = new Array(i)
var key
while (i--) {
key = keys[i]
res[i] = {
$key: key,
$value: value[key]
}
}
this.converted = true
return res
}
}
}
/**
* Helper to find the previous 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.
*
* If this is a block repeat, we want to make sure we only
* return vm that is bound to this v-repeat. (see #929)
*
* @param {Vue} vm
* @param {Comment|Text} anchor
* @return {Vue}
*/
function findPrevVm (vm, anchor, id) {
var el = vm.$el.previousSibling
/* istanbul ignore if */
if (!el) return
while (
(!el.__vue__ || el.__vue__.$options._repeatId !== id) &&
el !== anchor
) {
el = el.previousSibling
}
return el.__vue__
}
/**
* 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
}
/**
* Convert a vms array to an object ref for v-ref on an
* Object value.
*
* @param {Array} vms
* @return {Object}
*/
function toRefObject (vms) {
var ref = {}
for (var i = 0, l = vms.length; i < l; i++) {
ref[vms[i].$key] = vms[i]
}
return ref
}
/**
* Check if a value is a primitive one:
* String, Number, Boolean, null or undefined.
*
* @param {*} value
* @return {Boolean}
*/
function isPrimitive (value) {
var type = typeof value
return value == null ||
type === 'string' ||
type === 'number' ||
type === 'boolean'
}