repeat.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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 compile = require('../compiler/compile')
  8. var transclude = require('../compiler/transclude')
  9. var mergeOptions = require('../util/merge-option')
  10. var uid = 0
  11. // async component resolution states
  12. var UNRESOLVED = 0
  13. var PENDING = 1
  14. var RESOLVED = 2
  15. var ABORTED = 3
  16. module.exports = {
  17. /**
  18. * Setup.
  19. */
  20. bind: function () {
  21. // uid as a cache identifier
  22. this.id = '__v_repeat_' + (++uid)
  23. // we need to insert the objToArray converter
  24. // as the first read filter, because it has to be invoked
  25. // before any user filters. (can't do it in `update`)
  26. if (!this.filters) {
  27. this.filters = {}
  28. }
  29. // add the object -> array convert filter
  30. var objectConverter = _.bind(objToArray, this)
  31. if (!this.filters.read) {
  32. this.filters.read = [objectConverter]
  33. } else {
  34. this.filters.read.unshift(objectConverter)
  35. }
  36. // setup ref node
  37. this.ref = document.createComment('v-repeat')
  38. _.replace(this.el, this.ref)
  39. // check if this is a block repeat
  40. this.template = this.el.tagName === 'TEMPLATE'
  41. ? templateParser.parse(this.el, true)
  42. : this.el
  43. // check other directives that need to be handled
  44. // at v-repeat level
  45. this.checkIf()
  46. this.checkRef()
  47. this.checkComponent()
  48. // check for trackby param
  49. this.idKey =
  50. this._checkParam('track-by') ||
  51. this._checkParam('trackby') // 0.11.0 compat
  52. this.cache = Object.create(null)
  53. },
  54. /**
  55. * Warn against v-if usage.
  56. */
  57. checkIf: function () {
  58. if (_.attr(this.el, 'if') !== null) {
  59. _.warn(
  60. 'Don\'t use v-if with v-repeat. ' +
  61. 'Use v-show or the "filterBy" filter instead.'
  62. )
  63. }
  64. },
  65. /**
  66. * Check if v-ref/ v-el is also present.
  67. */
  68. checkRef: function () {
  69. var refID = _.attr(this.el, 'ref')
  70. this.refID = refID
  71. ? this.vm.$interpolate(refID)
  72. : null
  73. var elId = _.attr(this.el, 'el')
  74. this.elId = elId
  75. ? this.vm.$interpolate(elId)
  76. : null
  77. },
  78. /**
  79. * Check the component constructor to use for repeated
  80. * instances. If static we resolve it now, otherwise it
  81. * needs to be resolved at build time with actual data.
  82. */
  83. checkComponent: function () {
  84. this.componentState = UNRESOLVED
  85. var id = _.attr(this.el, 'component')
  86. var options = this.vm.$options
  87. if (!id) {
  88. // default constructor
  89. this.Ctor = _.Vue
  90. // inline repeats should inherit
  91. this.inherit = true
  92. // important: transclude with no options, just
  93. // to ensure block start and block end
  94. this.template = transclude(this.template)
  95. var copy = _.extend({}, options)
  96. copy._asComponent = false
  97. this._linkFn = compile(this.template, copy)
  98. } else {
  99. this.asComponent = true
  100. // check inline-template
  101. if (this._checkParam('inline-template') !== null) {
  102. // extract inline template as a DocumentFragment
  103. this.inlineTempalte = _.extractContent(this.el, true)
  104. }
  105. var tokens = textParser.parse(id)
  106. if (tokens) {
  107. // dynamic component to be resolved later
  108. var ctorExp = textParser.tokensToExp(tokens)
  109. this.ctorGetter = expParser.parse(ctorExp).get
  110. } else {
  111. // static
  112. this.componentId = id
  113. this.pendingData = null
  114. }
  115. }
  116. },
  117. resolveComponent: function () {
  118. this.componentState = PENDING
  119. this.vm._resolveComponent(this.componentId, _.bind(function (Ctor) {
  120. if (this.componentState === ABORTED) {
  121. return
  122. }
  123. this.Ctor = Ctor
  124. var merged = mergeOptions(Ctor.options, {}, {
  125. $parent: this.vm
  126. })
  127. merged.template = this.inlineTempalte || merged.template
  128. merged._asComponent = true
  129. merged._parent = this.vm
  130. this.template = transclude(this.template, merged)
  131. // Important: mark the template as a root node so that
  132. // custom element components don't get compiled twice.
  133. // fixes #822
  134. this.template.__vue__ = true
  135. this._linkFn = compile(this.template, merged)
  136. this.componentState = RESOLVED
  137. this.realUpdate(this.pendingData)
  138. this.pendingData = null
  139. }, this))
  140. },
  141. /**
  142. * Resolve a dynamic component to use for an instance.
  143. * The tricky part here is that there could be dynamic
  144. * components depending on instance data.
  145. *
  146. * @param {Object} data
  147. * @param {Object} meta
  148. * @return {Function}
  149. */
  150. resolveDynamicComponent: function (data, meta) {
  151. // create a temporary context object and copy data
  152. // and meta properties onto it.
  153. // use _.define to avoid accidentally overwriting scope
  154. // properties.
  155. var context = Object.create(this.vm)
  156. var key
  157. for (key in data) {
  158. _.define(context, key, data[key])
  159. }
  160. for (key in meta) {
  161. _.define(context, key, meta[key])
  162. }
  163. var id = this.ctorGetter.call(context, context)
  164. var Ctor = this.vm.$options.components[id]
  165. _.assertAsset(Ctor, 'component', id)
  166. if (!Ctor.options) {
  167. _.warn(
  168. 'Async resolution is not supported for v-repeat ' +
  169. '+ dynamic component. (component: ' + id + ')'
  170. )
  171. }
  172. return Ctor
  173. },
  174. /**
  175. * Update.
  176. * This is called whenever the Array mutates. If we have
  177. * a component, we might need to wait for it to resolve
  178. * asynchronously.
  179. *
  180. * @param {Array|Number|String} data
  181. */
  182. update: function (data) {
  183. if (this.componentId) {
  184. var state = this.componentState
  185. if (state === UNRESOLVED) {
  186. this.pendingData = data
  187. // once resolved, it will call realUpdate
  188. this.resolveComponent()
  189. } else if (state === PENDING) {
  190. this.pendingData = data
  191. } else if (state === RESOLVED) {
  192. this.realUpdate(data)
  193. }
  194. } else {
  195. this.realUpdate(data)
  196. }
  197. },
  198. /**
  199. * The real update that actually modifies the DOM.
  200. *
  201. * @param {Array|Number|String} data
  202. */
  203. realUpdate: function (data) {
  204. data = data || []
  205. var type = typeof data
  206. if (type === 'number') {
  207. data = range(data)
  208. } else if (type === 'string') {
  209. data = _.toArray(data)
  210. }
  211. this.vms = this.diff(data, this.vms)
  212. // update v-ref
  213. if (this.refID) {
  214. this.vm.$[this.refID] = this.vms
  215. }
  216. if (this.elId) {
  217. this.vm.$$[this.elId] = this.vms.map(function (vm) {
  218. return vm.$el
  219. })
  220. }
  221. },
  222. /**
  223. * Diff, based on new data and old data, determine the
  224. * minimum amount of DOM manipulations needed to make the
  225. * DOM reflect the new data Array.
  226. *
  227. * The algorithm diffs the new data Array by storing a
  228. * hidden reference to an owner vm instance on previously
  229. * seen data. This allows us to achieve O(n) which is
  230. * better than a levenshtein distance based algorithm,
  231. * which is O(m * n).
  232. *
  233. * @param {Array} data
  234. * @param {Array} oldVms
  235. * @return {Array}
  236. */
  237. diff: function (data, oldVms) {
  238. var idKey = this.idKey
  239. var converted = this.converted
  240. var ref = this.ref
  241. var alias = this.arg
  242. var init = !oldVms
  243. var vms = new Array(data.length)
  244. var obj, raw, vm, i, l
  245. // First pass, go through the new Array and fill up
  246. // the new vms array. If a piece of data has a cached
  247. // instance for it, we reuse it. Otherwise build a new
  248. // instance.
  249. for (i = 0, l = data.length; i < l; i++) {
  250. obj = data[i]
  251. raw = converted ? obj.$value : obj
  252. vm = !init && this.getVm(raw)
  253. if (vm) { // reusable instance
  254. vm._reused = true
  255. vm.$index = i // update $index
  256. if (converted) {
  257. vm.$key = obj.$key // update $key
  258. }
  259. if (idKey) { // swap track by id data
  260. if (alias) {
  261. vm[alias] = raw
  262. } else {
  263. vm._setData(raw)
  264. }
  265. }
  266. } else { // new instance
  267. vm = this.build(obj, i, true)
  268. vm._new = true
  269. vm._reused = false
  270. }
  271. vms[i] = vm
  272. // insert if this is first run
  273. if (init) {
  274. vm.$before(ref)
  275. }
  276. }
  277. // if this is the first run, we're done.
  278. if (init) {
  279. return vms
  280. }
  281. // Second pass, go through the old vm instances and
  282. // destroy those who are not reused (and remove them
  283. // from cache)
  284. for (i = 0, l = oldVms.length; i < l; i++) {
  285. vm = oldVms[i]
  286. if (!vm._reused) {
  287. this.uncacheVm(vm)
  288. vm.$destroy(true)
  289. }
  290. }
  291. // final pass, move/insert new instances into the
  292. // right place. We're going in reverse here because
  293. // insertBefore relies on the next sibling to be
  294. // resolved.
  295. var targetNext, currentNext
  296. i = vms.length
  297. while (i--) {
  298. vm = vms[i]
  299. // this is the vm that we should be in front of
  300. targetNext = vms[i + 1]
  301. if (!targetNext) {
  302. // This is the last item. If it's reused then
  303. // everything else will eventually be in the right
  304. // place, so no need to touch it. Otherwise, insert
  305. // it.
  306. if (!vm._reused) {
  307. vm.$before(ref)
  308. }
  309. } else {
  310. var nextEl = targetNext.$el
  311. if (vm._reused) {
  312. // this is the vm we are actually in front of
  313. currentNext = findNextVm(vm, ref)
  314. // we only need to move if we are not in the right
  315. // place already.
  316. if (currentNext !== targetNext) {
  317. vm.$before(nextEl, null, false)
  318. }
  319. } else {
  320. // new instance, insert to existing next
  321. vm.$before(nextEl)
  322. }
  323. }
  324. vm._new = false
  325. vm._reused = false
  326. }
  327. return vms
  328. },
  329. /**
  330. * Build a new instance and cache it.
  331. *
  332. * @param {Object} data
  333. * @param {Number} index
  334. * @param {Boolean} needCache
  335. */
  336. build: function (data, index, needCache) {
  337. var meta = { $index: index }
  338. if (this.converted) {
  339. meta.$key = data.$key
  340. }
  341. var raw = this.converted ? data.$value : data
  342. var alias = this.arg
  343. if (alias) {
  344. data = {}
  345. data[alias] = raw
  346. } else if (!isPlainObject(raw)) {
  347. // non-object values
  348. data = {}
  349. meta.$value = raw
  350. } else {
  351. // default
  352. data = raw
  353. }
  354. // resolve constructor
  355. var Ctor = this.Ctor || this.resolveDynamicComponent(data, meta)
  356. var vm = this.vm.$addChild({
  357. el: templateParser.clone(this.template),
  358. _asComponent: this.asComponent,
  359. _host: this._host,
  360. _linkFn: this._linkFn,
  361. _meta: meta,
  362. data: data,
  363. inherit: this.inherit,
  364. template: this.inlineTempalte
  365. }, Ctor)
  366. // flag this instance as a repeat instance
  367. // so that we can skip it in vm._digest
  368. vm._repeat = true
  369. // cache instance
  370. if (needCache) {
  371. this.cacheVm(raw, vm)
  372. }
  373. // sync back changes for $value, particularly for
  374. // two-way bindings of primitive values
  375. var self = this
  376. vm.$watch('$value', function (val) {
  377. if (self.converted) {
  378. self.rawValue[vm.$key] = val
  379. } else {
  380. self.rawValue.$set(vm.$index, val)
  381. }
  382. })
  383. return vm
  384. },
  385. /**
  386. * Unbind, teardown everything
  387. */
  388. unbind: function () {
  389. this.componentState = ABORTED
  390. if (this.refID) {
  391. this.vm.$[this.refID] = null
  392. }
  393. if (this.vms) {
  394. var i = this.vms.length
  395. var vm
  396. while (i--) {
  397. vm = this.vms[i]
  398. this.uncacheVm(vm)
  399. vm.$destroy()
  400. }
  401. }
  402. },
  403. /**
  404. * Cache a vm instance based on its data.
  405. *
  406. * If the data is an object, we save the vm's reference on
  407. * the data object as a hidden property. Otherwise we
  408. * cache them in an object and for each primitive value
  409. * there is an array in case there are duplicates.
  410. *
  411. * @param {Object} data
  412. * @param {Vue} vm
  413. */
  414. cacheVm: function (data, vm) {
  415. var idKey = this.idKey
  416. var cache = this.cache
  417. var id
  418. if (idKey) {
  419. id = data[idKey]
  420. if (!cache[id]) {
  421. cache[id] = vm
  422. } else {
  423. _.warn('Duplicate track-by key in v-repeat: ' + id)
  424. }
  425. } else if (isObject(data)) {
  426. id = this.id
  427. if (data.hasOwnProperty(id)) {
  428. if (data[id] === null) {
  429. data[id] = vm
  430. } else {
  431. _.warn(
  432. 'Duplicate objects are not supported in v-repeat ' +
  433. 'when using components or transitions.'
  434. )
  435. }
  436. } else {
  437. _.define(data, id, vm)
  438. }
  439. } else {
  440. if (!cache[data]) {
  441. cache[data] = [vm]
  442. } else {
  443. cache[data].push(vm)
  444. }
  445. }
  446. vm._raw = data
  447. },
  448. /**
  449. * Try to get a cached instance from a piece of data.
  450. *
  451. * @param {Object} data
  452. * @return {Vue|undefined}
  453. */
  454. getVm: function (data) {
  455. if (this.idKey) {
  456. return this.cache[data[this.idKey]]
  457. } else if (isObject(data)) {
  458. return data[this.id]
  459. } else {
  460. var cached = this.cache[data]
  461. if (cached) {
  462. var i = 0
  463. var vm = cached[i]
  464. // since duplicated vm instances might be a reused
  465. // one OR a newly created one, we need to return the
  466. // first instance that is neither of these.
  467. while (vm && (vm._reused || vm._new)) {
  468. vm = cached[++i]
  469. }
  470. return vm
  471. }
  472. }
  473. },
  474. /**
  475. * Delete a cached vm instance.
  476. *
  477. * @param {Vue} vm
  478. */
  479. uncacheVm: function (vm) {
  480. var data = vm._raw
  481. if (this.idKey) {
  482. this.cache[data[this.idKey]] = null
  483. } else if (isObject(data)) {
  484. data[this.id] = null
  485. vm._raw = null
  486. } else {
  487. this.cache[data].pop()
  488. }
  489. }
  490. }
  491. /**
  492. * Helper to find the next element that is an instance
  493. * root node. This is necessary because a destroyed vm's
  494. * element could still be lingering in the DOM before its
  495. * leaving transition finishes, but its __vue__ reference
  496. * should have been removed so we can skip them.
  497. *
  498. * @param {Vue} vm
  499. * @param {CommentNode} ref
  500. * @return {Vue}
  501. */
  502. function findNextVm (vm, ref) {
  503. var el = (vm._blockEnd || vm.$el).nextSibling
  504. while (!el.__vue__ && el !== ref) {
  505. el = el.nextSibling
  506. }
  507. return el.__vue__
  508. }
  509. /**
  510. * Attempt to convert non-Array objects to array.
  511. * This is the default filter installed to every v-repeat
  512. * directive.
  513. *
  514. * It will be called with **the directive** as `this`
  515. * context so that we can mark the repeat array as converted
  516. * from an object.
  517. *
  518. * @param {*} obj
  519. * @return {Array}
  520. * @private
  521. */
  522. function objToArray (obj) {
  523. // regardless of type, store the un-filtered raw value.
  524. this.rawValue = obj
  525. if (!isPlainObject(obj)) {
  526. return obj
  527. }
  528. var keys = Object.keys(obj)
  529. var i = keys.length
  530. var res = new Array(i)
  531. var key
  532. while (i--) {
  533. key = keys[i]
  534. res[i] = {
  535. $key: key,
  536. $value: obj[key]
  537. }
  538. }
  539. // `this` points to the repeat directive instance
  540. this.converted = true
  541. return res
  542. }
  543. /**
  544. * Create a range array from given number.
  545. *
  546. * @param {Number} n
  547. * @return {Array}
  548. */
  549. function range (n) {
  550. var i = -1
  551. var ret = new Array(n)
  552. while (++i < n) {
  553. ret[i] = i
  554. }
  555. return ret
  556. }