repeat.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. var _ = require('../util')
  2. var isObject = _.isObject
  3. var isPlainObject = _.isPlainObject
  4. var textParser = require('../parsers/text')
  5. var expParser = require('../parsers/expression')
  6. var templateParser = require('../parsers/template')
  7. var compiler = require('../compiler')
  8. var uid = 0
  9. // async component resolution states
  10. var UNRESOLVED = 0
  11. var PENDING = 1
  12. var RESOLVED = 2
  13. var ABORTED = 3
  14. module.exports = {
  15. /**
  16. * Setup.
  17. */
  18. bind: function () {
  19. // uid as a cache identifier
  20. this.id = '__v_repeat_' + (++uid)
  21. // setup anchor nodes
  22. this.start = _.createAnchor('v-repeat-start')
  23. this.end = _.createAnchor('v-repeat-end')
  24. _.replace(this.el, this.end)
  25. _.before(this.start, this.end)
  26. // check if this is a block repeat
  27. this.template = _.isTemplate(this.el)
  28. ? templateParser.parse(this.el, true)
  29. : this.el
  30. // check other directives that need to be handled
  31. // at v-repeat level
  32. this.checkIf()
  33. this.checkRef()
  34. this.checkComponent()
  35. // check for trackby param
  36. this.idKey =
  37. this._checkParam('track-by') ||
  38. this._checkParam('trackby') // 0.11.0 compat
  39. // check for transition stagger
  40. var stagger = +this._checkParam('stagger')
  41. this.enterStagger = +this._checkParam('enter-stagger') || stagger
  42. this.leaveStagger = +this._checkParam('leave-stagger') || stagger
  43. this.cache = Object.create(null)
  44. },
  45. /**
  46. * Warn against v-if usage.
  47. */
  48. checkIf: function () {
  49. if (_.attr(this.el, 'if') !== null) {
  50. process.env.NODE_ENV !== 'production' && _.warn(
  51. 'Don\'t use v-if with v-repeat. ' +
  52. 'Use v-show or the "filterBy" filter instead.'
  53. )
  54. }
  55. },
  56. /**
  57. * Check if v-ref/ v-el is also present.
  58. */
  59. checkRef: function () {
  60. var refID = _.attr(this.el, 'ref')
  61. this.refID = refID
  62. ? this.vm.$interpolate(refID)
  63. : null
  64. var elId = _.attr(this.el, 'el')
  65. this.elId = elId
  66. ? this.vm.$interpolate(elId)
  67. : null
  68. },
  69. /**
  70. * Check the component constructor to use for repeated
  71. * instances. If static we resolve it now, otherwise it
  72. * needs to be resolved at build time with actual data.
  73. */
  74. checkComponent: function () {
  75. this.componentState = UNRESOLVED
  76. var options = this.vm.$options
  77. var id = _.checkComponent(this.el, options)
  78. if (!id) {
  79. // default constructor
  80. this.Ctor = _.Vue
  81. // inline repeats should inherit
  82. this.inherit = true
  83. // important: transclude with no options, just
  84. // to ensure block start and block end
  85. this.template = compiler.transclude(this.template)
  86. var copy = _.extend({}, options)
  87. copy._asComponent = false
  88. this._linkFn = compiler.compile(this.template, copy)
  89. } else {
  90. this.Ctor = null
  91. this.asComponent = true
  92. // check inline-template
  93. if (this._checkParam('inline-template') !== null) {
  94. // extract inline template as a DocumentFragment
  95. this.inlineTemplate = _.extractContent(this.el, true)
  96. }
  97. var tokens = textParser.parse(id)
  98. if (tokens) {
  99. // dynamic component to be resolved later
  100. var ctorExp = textParser.tokensToExp(tokens)
  101. this.ctorGetter = expParser.parse(ctorExp).get
  102. } else {
  103. // static
  104. this.componentId = id
  105. this.pendingData = null
  106. }
  107. }
  108. },
  109. resolveComponent: function () {
  110. this.componentState = PENDING
  111. this.vm._resolveComponent(this.componentId, _.bind(function (Ctor) {
  112. if (this.componentState === ABORTED) {
  113. return
  114. }
  115. this.Ctor = Ctor
  116. this.componentState = RESOLVED
  117. this.realUpdate(this.pendingData)
  118. this.pendingData = null
  119. }, this))
  120. },
  121. /**
  122. * Resolve a dynamic component to use for an instance.
  123. * The tricky part here is that there could be dynamic
  124. * components depending on instance data.
  125. *
  126. * @param {Object} data
  127. * @param {Object} meta
  128. * @return {Function}
  129. */
  130. resolveDynamicComponent: function (data, meta) {
  131. // create a temporary context object and copy data
  132. // and meta properties onto it.
  133. // use _.define to avoid accidentally overwriting scope
  134. // properties.
  135. var context = Object.create(this.vm)
  136. var key
  137. for (key in data) {
  138. _.define(context, key, data[key])
  139. }
  140. for (key in meta) {
  141. _.define(context, key, meta[key])
  142. }
  143. var id = this.ctorGetter.call(context, context)
  144. var Ctor = _.resolveAsset(this.vm.$options, 'components', id)
  145. if (process.env.NODE_ENV !== 'production') {
  146. _.assertAsset(Ctor, 'component', id)
  147. }
  148. if (!Ctor.options) {
  149. process.env.NODE_ENV !== 'production' && _.warn(
  150. 'Async resolution is not supported for v-repeat ' +
  151. '+ dynamic component. (component: ' + id + ')'
  152. )
  153. return _.Vue
  154. }
  155. return Ctor
  156. },
  157. /**
  158. * Update.
  159. * This is called whenever the Array mutates. If we have
  160. * a component, we might need to wait for it to resolve
  161. * asynchronously.
  162. *
  163. * @param {Array|Number|String} data
  164. */
  165. update: function (data) {
  166. if (this.componentId) {
  167. var state = this.componentState
  168. if (state === UNRESOLVED) {
  169. this.pendingData = data
  170. // once resolved, it will call realUpdate
  171. this.resolveComponent()
  172. } else if (state === PENDING) {
  173. this.pendingData = data
  174. } else if (state === RESOLVED) {
  175. this.realUpdate(data)
  176. }
  177. } else {
  178. this.realUpdate(data)
  179. }
  180. },
  181. /**
  182. * The real update that actually modifies the DOM.
  183. *
  184. * @param {Array|Number|String} data
  185. */
  186. realUpdate: function (data) {
  187. this.vms = this.diff(data, this.vms)
  188. // update v-ref
  189. if (this.refID) {
  190. this.vm.$[this.refID] = this.converted
  191. ? toRefObject(this.vms)
  192. : this.vms
  193. }
  194. if (this.elId) {
  195. this.vm.$$[this.elId] = this.vms.map(function (vm) {
  196. return vm.$el
  197. })
  198. }
  199. },
  200. /**
  201. * Diff, based on new data and old data, determine the
  202. * minimum amount of DOM manipulations needed to make the
  203. * DOM reflect the new data Array.
  204. *
  205. * The algorithm diffs the new data Array by storing a
  206. * hidden reference to an owner vm instance on previously
  207. * seen data. This allows us to achieve O(n) which is
  208. * better than a levenshtein distance based algorithm,
  209. * which is O(m * n).
  210. *
  211. * @param {Array} data
  212. * @param {Array} oldVms
  213. * @return {Array}
  214. */
  215. diff: function (data, oldVms) {
  216. var idKey = this.idKey
  217. var converted = this.converted
  218. var start = this.start
  219. var end = this.end
  220. var inDoc = _.inDoc(start)
  221. var alias = this.arg
  222. var init = !oldVms
  223. var vms = new Array(data.length)
  224. var obj, raw, vm, i, l, primitive
  225. // First pass, go through the new Array and fill up
  226. // the new vms array. If a piece of data has a cached
  227. // instance for it, we reuse it. Otherwise build a new
  228. // instance.
  229. for (i = 0, l = data.length; i < l; i++) {
  230. obj = data[i]
  231. raw = converted ? obj.$value : obj
  232. primitive = !isObject(raw)
  233. vm = !init && this.getVm(raw, i, converted ? obj.$key : null)
  234. if (vm) { // reusable instance
  235. vm._reused = true
  236. vm.$index = i // update $index
  237. // update data for track-by or object repeat,
  238. // since in these two cases the data is replaced
  239. // rather than mutated.
  240. if (idKey || converted || primitive) {
  241. if (alias) {
  242. vm[alias] = raw
  243. } else if (_.isPlainObject(raw)) {
  244. vm.$data = raw
  245. } else {
  246. vm.$value = raw
  247. }
  248. }
  249. } else { // new instance
  250. vm = this.build(obj, i, true)
  251. vm._reused = false
  252. }
  253. vms[i] = vm
  254. // insert if this is first run
  255. if (init) {
  256. vm.$before(end)
  257. }
  258. }
  259. // if this is the first run, we're done.
  260. if (init) {
  261. return vms
  262. }
  263. // Second pass, go through the old vm instances and
  264. // destroy those who are not reused (and remove them
  265. // from cache)
  266. var removalIndex = 0
  267. var totalRemoved = oldVms.length - vms.length
  268. for (i = 0, l = oldVms.length; i < l; i++) {
  269. vm = oldVms[i]
  270. if (!vm._reused) {
  271. this.uncacheVm(vm)
  272. vm.$destroy(false, true) // defer cleanup until removal
  273. this.remove(vm, removalIndex++, totalRemoved, inDoc)
  274. }
  275. }
  276. // final pass, move/insert new instances into the
  277. // right place.
  278. var targetPrev, prevEl, currentPrev
  279. var insertionIndex = 0
  280. for (i = 0, l = vms.length; i < l; i++) {
  281. vm = vms[i]
  282. // this is the vm that we should be after
  283. targetPrev = vms[i - 1]
  284. prevEl = targetPrev
  285. ? targetPrev._staggerCb
  286. ? targetPrev._staggerAnchor
  287. : targetPrev._blockEnd || targetPrev.$el
  288. : start
  289. if (vm._reused && !vm._staggerCb) {
  290. currentPrev = findPrevVm(vm, start, this.id)
  291. if (currentPrev !== targetPrev) {
  292. this.move(vm, prevEl)
  293. }
  294. } else {
  295. // new instance, or still in stagger.
  296. // insert with updated stagger index.
  297. this.insert(vm, insertionIndex++, prevEl, inDoc)
  298. }
  299. vm._reused = false
  300. }
  301. return vms
  302. },
  303. /**
  304. * Build a new instance and cache it.
  305. *
  306. * @param {Object} data
  307. * @param {Number} index
  308. * @param {Boolean} needCache
  309. */
  310. build: function (data, index, needCache) {
  311. var meta = { $index: index }
  312. if (this.converted) {
  313. meta.$key = data.$key
  314. }
  315. var raw = this.converted ? data.$value : data
  316. var alias = this.arg
  317. if (alias) {
  318. data = {}
  319. data[alias] = raw
  320. } else if (!isPlainObject(raw)) {
  321. // non-object values
  322. data = {}
  323. meta.$value = raw
  324. } else {
  325. // default
  326. data = raw
  327. }
  328. // resolve constructor
  329. var Ctor = this.Ctor || this.resolveDynamicComponent(data, meta)
  330. var parent = this._host || this.vm
  331. var vm = parent.$addChild({
  332. el: templateParser.clone(this.template),
  333. data: data,
  334. inherit: this.inherit,
  335. template: this.inlineTemplate,
  336. // repeater meta, e.g. $index, $key
  337. _meta: meta,
  338. // mark this as an inline-repeat instance
  339. _repeat: this.inherit,
  340. // is this a component?
  341. _asComponent: this.asComponent,
  342. // linker cachable if no inline-template
  343. _linkerCachable: !this.inlineTemplate && Ctor !== _.Vue,
  344. // pre-compiled linker for simple repeats
  345. _linkFn: this._linkFn,
  346. // identifier, shows that this vm belongs to this collection
  347. _repeatId: this.id,
  348. // transclusion content owner
  349. _context: this.vm
  350. }, Ctor)
  351. // cache instance
  352. if (needCache) {
  353. this.cacheVm(raw, vm, index, this.converted ? meta.$key : null)
  354. }
  355. // sync back changes for two-way bindings of primitive values
  356. var type = typeof raw
  357. var dir = this
  358. if (
  359. this.rawType === 'object' &&
  360. (type === 'string' || type === 'number')
  361. ) {
  362. vm.$watch(alias || '$value', function (val) {
  363. if (dir.filters) {
  364. process.env.NODE_ENV !== 'production' && _.warn(
  365. 'You seem to be mutating the $value reference of ' +
  366. 'a v-repeat instance (likely through v-model) ' +
  367. 'and filtering the v-repeat at the same time. ' +
  368. 'This will not work properly with an Array of ' +
  369. 'primitive values. Please use an Array of ' +
  370. 'Objects instead.'
  371. )
  372. }
  373. dir._withLock(function () {
  374. if (dir.converted) {
  375. dir.rawValue[vm.$key] = val
  376. } else {
  377. dir.rawValue.$set(vm.$index, val)
  378. }
  379. })
  380. })
  381. }
  382. return vm
  383. },
  384. /**
  385. * Unbind, teardown everything
  386. */
  387. unbind: function () {
  388. this.componentState = ABORTED
  389. if (this.refID) {
  390. this.vm.$[this.refID] = null
  391. }
  392. if (this.vms) {
  393. var i = this.vms.length
  394. var vm
  395. while (i--) {
  396. vm = this.vms[i]
  397. this.uncacheVm(vm)
  398. vm.$destroy()
  399. }
  400. }
  401. },
  402. /**
  403. * Cache a vm instance based on its data.
  404. *
  405. * If the data is an object, we save the vm's reference on
  406. * the data object as a hidden property. Otherwise we
  407. * cache them in an object and for each primitive value
  408. * there is an array in case there are duplicates.
  409. *
  410. * @param {Object} data
  411. * @param {Vue} vm
  412. * @param {Number} index
  413. * @param {String} [key]
  414. */
  415. cacheVm: function (data, vm, index, key) {
  416. var idKey = this.idKey
  417. var cache = this.cache
  418. var primitive = !isObject(data)
  419. var id
  420. if (key || idKey || primitive) {
  421. id = idKey
  422. ? idKey === '$index'
  423. ? index
  424. : data[idKey]
  425. : (key || index)
  426. if (!cache[id]) {
  427. cache[id] = vm
  428. } else if (!primitive && idKey !== '$index') {
  429. process.env.NODE_ENV !== 'production' && _.warn(
  430. 'Duplicate track-by key in v-repeat: ' + id
  431. )
  432. }
  433. } else {
  434. id = this.id
  435. if (data.hasOwnProperty(id)) {
  436. if (data[id] === null) {
  437. data[id] = vm
  438. } else {
  439. process.env.NODE_ENV !== 'production' && _.warn(
  440. 'Duplicate objects are not supported in v-repeat ' +
  441. 'when using components or transitions.'
  442. )
  443. }
  444. } else {
  445. _.define(data, id, vm)
  446. }
  447. }
  448. vm._raw = data
  449. },
  450. /**
  451. * Try to get a cached instance from a piece of data.
  452. *
  453. * @param {Object} data
  454. * @param {Number} index
  455. * @param {String} [key]
  456. * @return {Vue|undefined}
  457. */
  458. getVm: function (data, index, key) {
  459. var idKey = this.idKey
  460. var primitive = !isObject(data)
  461. if (key || idKey || primitive) {
  462. var id = idKey
  463. ? idKey === '$index'
  464. ? index
  465. : data[idKey]
  466. : (key || index)
  467. return this.cache[id]
  468. } else {
  469. return data[this.id]
  470. }
  471. },
  472. /**
  473. * Delete a cached vm instance.
  474. *
  475. * @param {Vue} vm
  476. */
  477. uncacheVm: function (vm) {
  478. var data = vm._raw
  479. var idKey = this.idKey
  480. var index = vm.$index
  481. // fix #948: avoid accidentally fall through to
  482. // a parent repeater which happens to have $key.
  483. var key = vm.hasOwnProperty('$key') && vm.$key
  484. var primitive = !isObject(data)
  485. if (idKey || key || primitive) {
  486. var id = idKey
  487. ? idKey === '$index'
  488. ? index
  489. : data[idKey]
  490. : (key || index)
  491. this.cache[id] = null
  492. } else {
  493. data[this.id] = null
  494. vm._raw = null
  495. }
  496. },
  497. /**
  498. * Insert an instance.
  499. *
  500. * @param {Vue} vm
  501. * @param {Number} index
  502. * @param {Node} prevEl
  503. * @param {Boolean} inDoc
  504. */
  505. insert: function (vm, index, prevEl, inDoc) {
  506. if (vm._staggerCb) {
  507. vm._staggerCb.cancel()
  508. vm._staggerCb = null
  509. }
  510. var staggerAmount = this.getStagger(vm, index, null, 'enter')
  511. if (inDoc && staggerAmount) {
  512. // create an anchor and insert it synchronously,
  513. // so that we can resolve the correct order without
  514. // worrying about some elements not inserted yet
  515. var anchor = vm._staggerAnchor
  516. if (!anchor) {
  517. anchor = vm._staggerAnchor = _.createAnchor('stagger-anchor')
  518. anchor.__vue__ = vm
  519. }
  520. _.after(anchor, prevEl)
  521. var op = vm._staggerCb = _.cancellable(function () {
  522. vm._staggerCb = null
  523. vm.$before(anchor)
  524. _.remove(anchor)
  525. })
  526. setTimeout(op, staggerAmount)
  527. } else {
  528. vm.$after(prevEl)
  529. }
  530. },
  531. /**
  532. * Move an already inserted instance.
  533. *
  534. * @param {Vue} vm
  535. * @param {Node} prevEl
  536. */
  537. move: function (vm, prevEl) {
  538. vm.$after(prevEl, null, false)
  539. },
  540. /**
  541. * Remove an instance.
  542. *
  543. * @param {Vue} vm
  544. * @param {Number} index
  545. * @param {Boolean} inDoc
  546. */
  547. remove: function (vm, index, total, inDoc) {
  548. if (vm._staggerCb) {
  549. vm._staggerCb.cancel()
  550. vm._staggerCb = null
  551. // it's not possible for the same vm to be removed
  552. // twice, so if we have a pending stagger callback,
  553. // it means this vm is queued for enter but removed
  554. // before its transition started. Since it is already
  555. // destroyed, we can just leave it in detached state.
  556. return
  557. }
  558. var staggerAmount = this.getStagger(vm, index, total, 'leave')
  559. if (inDoc && staggerAmount) {
  560. var op = vm._staggerCb = _.cancellable(function () {
  561. vm._staggerCb = null
  562. remove()
  563. })
  564. setTimeout(op, staggerAmount)
  565. } else {
  566. remove()
  567. }
  568. function remove () {
  569. vm.$remove(function () {
  570. vm._cleanup()
  571. })
  572. }
  573. },
  574. /**
  575. * Get the stagger amount for an insertion/removal.
  576. *
  577. * @param {Vue} vm
  578. * @param {Number} index
  579. * @param {String} type
  580. * @param {Number} total
  581. */
  582. getStagger: function (vm, index, total, type) {
  583. type = type + 'Stagger'
  584. var transition = vm.$el.__v_trans
  585. var hooks = transition && transition.hooks
  586. var hook = hooks && (hooks[type] || hooks.stagger)
  587. return hook
  588. ? hook.call(vm, index, total)
  589. : index * this[type]
  590. },
  591. /**
  592. * Pre-process the value before piping it through the
  593. * filters, and convert non-Array objects to arrays.
  594. *
  595. * This function will be bound to this directive instance
  596. * and passed into the watcher.
  597. *
  598. * @param {*} value
  599. * @return {Array}
  600. * @private
  601. */
  602. _preProcess: function (value) {
  603. // regardless of type, store the un-filtered raw value.
  604. this.rawValue = value
  605. var type = this.rawType = typeof value
  606. if (!isPlainObject(value)) {
  607. this.converted = false
  608. if (type === 'number') {
  609. value = range(value)
  610. } else if (type === 'string') {
  611. value = _.toArray(value)
  612. }
  613. return value || []
  614. } else {
  615. // convert plain object to array.
  616. var keys = Object.keys(value)
  617. var i = keys.length
  618. var res = new Array(i)
  619. var key
  620. while (i--) {
  621. key = keys[i]
  622. res[i] = {
  623. $key: key,
  624. $value: value[key]
  625. }
  626. }
  627. this.converted = true
  628. return res
  629. }
  630. },
  631. /**
  632. * Internal hook to do custom transform on the directive's
  633. * descriptor so that it can support special syntax.
  634. *
  635. * @param {Object} descriptor
  636. */
  637. _guard: function (descriptor) {
  638. var exp = descriptor.expression
  639. var match = exp.trim().match(/(.*) in (.*)/)
  640. if (match) {
  641. descriptor.arg = match[1]
  642. descriptor.expression = match[2]
  643. }
  644. }
  645. }
  646. /**
  647. * Helper to find the previous element that is an instance
  648. * root node. This is necessary because a destroyed vm's
  649. * element could still be lingering in the DOM before its
  650. * leaving transition finishes, but its __vue__ reference
  651. * should have been removed so we can skip them.
  652. *
  653. * If this is a block repeat, we want to make sure we only
  654. * return vm that is bound to this v-repeat. (see #929)
  655. *
  656. * @param {Vue} vm
  657. * @param {Comment|Text} anchor
  658. * @return {Vue}
  659. */
  660. function findPrevVm (vm, anchor, id) {
  661. var el = vm.$el.previousSibling
  662. /* istanbul ignore if */
  663. if (!el) return
  664. while (
  665. (!el.__vue__ || el.__vue__.$options._repeatId !== id) &&
  666. el !== anchor
  667. ) {
  668. el = el.previousSibling
  669. }
  670. return el.__vue__
  671. }
  672. /**
  673. * Create a range array from given number.
  674. *
  675. * @param {Number} n
  676. * @return {Array}
  677. */
  678. function range (n) {
  679. var i = -1
  680. var ret = new Array(n)
  681. while (++i < n) {
  682. ret[i] = i
  683. }
  684. return ret
  685. }
  686. /**
  687. * Convert a vms array to an object ref for v-ref on an
  688. * Object value.
  689. *
  690. * @param {Array} vms
  691. * @return {Object}
  692. */
  693. function toRefObject (vms) {
  694. var ref = {}
  695. for (var i = 0, l = vms.length; i < l; i++) {
  696. ref[vms[i].$key] = vms[i]
  697. }
  698. return ref
  699. }