From 443ef1c26c299856709ff59aa2f9e7816fbd7908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E9=93=84=E8=BF=90?= Date: Thu, 13 Oct 2016 01:22:21 +0800 Subject: [PATCH 01/30] Update CONTRIBUTING.md's jdfiddle template, and vue's version inside ISSUE_TEMPLATE.md (#3921) * Update CONTRIBUTING.md's jdfiddle template * Update ISSUE_TEMPLATE.md --- .github/CONTRIBUTING.md | 2 +- .github/ISSUE_TEMPLATE.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f649df9aecf..0dc1de13045 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -22,7 +22,7 @@ Hi! I’m really excited that you are interested in contributing to Vue.js. Befo - It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed. -- It is recommended that you make a JSFiddle/JSBin/Codepen to demonstrate your issue. You could start with [this template](http://jsfiddle.net/5sH6A/) that already includes the latest version of Vue. +- It is recommended that you make a JSFiddle/JSBin/Codepen to demonstrate your issue. You could start with [this template](http://jsfiddle.net/df4Lnuw6/) that already includes the latest version of Vue. - For bugs that involves build setups, you can create a reproduction repository with steps in the README. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c12caedd647..8026bb63f0f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -35,10 +35,11 @@ Remove the template from below and provide thoughtful commentary *and code sampl ### Vue.js version -2.0.0-rc.1 +2.0.2 ### Reproduction Link + ### Steps to reproduce From a9417e4e4fcb91d445c15d79b6eec684d7e5c9c7 Mon Sep 17 00:00:00 2001 From: defcc Date: Thu, 13 Oct 2016 01:26:42 +0800 Subject: [PATCH 02/30] select change event fix (#3922) * if select binding not changed, then needRest should be set to false, and no change event should be emitted * update code style --- src/platforms/web/runtime/directives/model.js | 2 +- .../features/directives/model-select.spec.js | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/platforms/web/runtime/directives/model.js b/src/platforms/web/runtime/directives/model.js index 0c67c9abf46..f6c24eea3c3 100644 --- a/src/platforms/web/runtime/directives/model.js +++ b/src/platforms/web/runtime/directives/model.js @@ -60,7 +60,7 @@ export default { // option in the DOM. const needReset = el.multiple ? binding.value.some(v => hasNoMatchingOption(v, el.options)) - : hasNoMatchingOption(binding.value, el.options) + : binding.value === binding.oldValue ? false : hasNoMatchingOption(binding.value, el.options) if (needReset) { trigger(el, 'change') } diff --git a/test/unit/features/directives/model-select.spec.js b/test/unit/features/directives/model-select.spec.js index 9b6092f26e1..dad451687d3 100644 --- a/test/unit/features/directives/model-select.spec.js +++ b/test/unit/features/directives/model-select.spec.js @@ -161,6 +161,32 @@ describe('Directive v-model select', () => { }).then(done) }) + it('should work with select which has no default selected options', (done) => { + const spy = jasmine.createSpy() + const vm = new Vue({ + data: { + id: 4, + list: [1, 2, 3], + testChange: 5 + }, + template: + '
' + + '' + + '{{testChange}}' + + '
', + methods: { + test: spy + } + }).$mount() + document.body.appendChild(vm.$el) + vm.testChange = 10 + waitForUpdate(() => { + expect(spy.calls.count()).toBe(0) + }).then(done) + }) + it('multiple', done => { const vm = new Vue({ data: { From 7ca58b6cdf985da1751c97290fb1e2fc9fb08f41 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Oct 2016 13:28:56 -0400 Subject: [PATCH 03/30] small tweak --- src/platforms/web/runtime/directives/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platforms/web/runtime/directives/model.js b/src/platforms/web/runtime/directives/model.js index f6c24eea3c3..8c66721a2a4 100644 --- a/src/platforms/web/runtime/directives/model.js +++ b/src/platforms/web/runtime/directives/model.js @@ -60,7 +60,7 @@ export default { // option in the DOM. const needReset = el.multiple ? binding.value.some(v => hasNoMatchingOption(v, el.options)) - : binding.value === binding.oldValue ? false : hasNoMatchingOption(binding.value, el.options) + : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, el.options) if (needReset) { trigger(el, 'change') } From ceab0b71d0dd8247220564b2a651f7bea0a796d6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Oct 2016 13:31:39 -0400 Subject: [PATCH 04/30] fix functional components that return string or nothing (fix #3919) --- src/core/vdom/create-component.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/vdom/create-component.js b/src/core/vdom/create-component.js index 0a2721cb1f6..44f7975b4bb 100644 --- a/src/core/vdom/create-component.js +++ b/src/core/vdom/create-component.js @@ -113,9 +113,11 @@ function createFunctionalComponent ( slots: () => resolveSlots(children, context) } ) - vnode.functionalContext = context - if (data.slot) { - (vnode.data || (vnode.data = {})).slot = data.slot + if (vnode instanceof VNode) { + vnode.functionalContext = context + if (data.slot) { + (vnode.data || (vnode.data = {})).slot = data.slot + } } return vnode } From e774ce2353e28c813728f508a5a83d8236f5e36e Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Oct 2016 14:07:45 -0400 Subject: [PATCH 05/30] refactor: split vdom helpers into separate files --- src/core/components/keep-alive.js | 2 +- src/core/instance/events.js | 2 +- src/core/instance/render.js | 2 +- src/core/vdom/create-component.js | 2 +- src/core/vdom/create-element.js | 2 +- src/core/vdom/helpers.js | 148 ------------------ src/core/vdom/helpers/index.js | 9 ++ src/core/vdom/helpers/merge-hook.js | 18 +++ src/core/vdom/helpers/normalize-children.js | 62 ++++++++ src/core/vdom/helpers/update-listeners.js | 68 ++++++++ src/core/vdom/modules/directives.js | 2 +- .../web/runtime/components/transition.js | 2 +- src/platforms/web/runtime/modules/events.js | 2 +- .../web/runtime/modules/transition.js | 2 +- 14 files changed, 166 insertions(+), 157 deletions(-) delete mode 100644 src/core/vdom/helpers.js create mode 100644 src/core/vdom/helpers/index.js create mode 100644 src/core/vdom/helpers/merge-hook.js create mode 100644 src/core/vdom/helpers/normalize-children.js create mode 100644 src/core/vdom/helpers/update-listeners.js diff --git a/src/core/components/keep-alive.js b/src/core/components/keep-alive.js index 0348bc8c717..8268c78d1d1 100644 --- a/src/core/components/keep-alive.js +++ b/src/core/components/keep-alive.js @@ -1,5 +1,5 @@ import { callHook } from 'core/instance/lifecycle' -import { getFirstComponentChild } from 'core/vdom/helpers' +import { getFirstComponentChild } from 'core/vdom/helpers/index' export default { name: 'keep-alive', diff --git a/src/core/instance/events.js b/src/core/instance/events.js index 5c664d44ea5..1058f7c64b5 100644 --- a/src/core/instance/events.js +++ b/src/core/instance/events.js @@ -1,7 +1,7 @@ /* @flow */ import { bind, toArray } from '../util/index' -import { updateListeners } from '../vdom/helpers' +import { updateListeners } from '../vdom/helpers/index' export function initEvents (vm: Component) { vm._events = Object.create(null) diff --git a/src/core/instance/render.js b/src/core/instance/render.js index 86fd6f165a2..48f990b5c8e 100644 --- a/src/core/instance/render.js +++ b/src/core/instance/render.js @@ -2,7 +2,7 @@ import config from '../config' import VNode, { emptyVNode, cloneVNode, cloneVNodes } from '../vdom/vnode' -import { normalizeChildren } from '../vdom/helpers' +import { normalizeChildren } from '../vdom/helpers/index' import { warn, formatComponentName, bind, isObject, toObject, nextTick, resolveAsset, _toString, toNumber, looseEqual, looseIndexOf diff --git a/src/core/vdom/create-component.js b/src/core/vdom/create-component.js index 44f7975b4bb..f09315fdf09 100644 --- a/src/core/vdom/create-component.js +++ b/src/core/vdom/create-component.js @@ -2,7 +2,7 @@ import Vue from '../instance/index' import VNode from './vnode' -import { normalizeChildren } from './helpers' +import { normalizeChildren } from './helpers/index' import { activeInstance, callHook } from '../instance/lifecycle' import { resolveSlots } from '../instance/render' import { createElement } from './create-element' diff --git a/src/core/vdom/create-element.js b/src/core/vdom/create-element.js index 268f88f7a91..f6d47a26ae6 100644 --- a/src/core/vdom/create-element.js +++ b/src/core/vdom/create-element.js @@ -3,7 +3,7 @@ import VNode, { emptyVNode } from './vnode' import config from '../config' import { createComponent } from './create-component' -import { normalizeChildren } from './helpers' +import { normalizeChildren } from './helpers/index' import { warn, resolveAsset } from '../util/index' // wrapper function for providing a more flexible interface diff --git a/src/core/vdom/helpers.js b/src/core/vdom/helpers.js deleted file mode 100644 index f1f014cea1d..00000000000 --- a/src/core/vdom/helpers.js +++ /dev/null @@ -1,148 +0,0 @@ -/* @flow */ - -import { isPrimitive, warn } from '../util/index' -import VNode from './vnode' - -export function normalizeChildren ( - children: any, - ns: string | void, - nestedIndex: number | void -): Array | void { - if (isPrimitive(children)) { - return [createTextVNode(children)] - } - if (Array.isArray(children)) { - const res = [] - for (let i = 0, l = children.length; i < l; i++) { - const c = children[i] - const last = res[res.length - 1] - // nested - if (Array.isArray(c)) { - res.push.apply(res, normalizeChildren(c, ns, i)) - } else if (isPrimitive(c)) { - if (last && last.text) { - last.text += String(c) - } else if (c !== '') { - // convert primitive to vnode - res.push(createTextVNode(c)) - } - } else if (c instanceof VNode) { - if (c.text && last && last.text) { - last.text += c.text - } else { - // inherit parent namespace - if (ns) { - applyNS(c, ns) - } - // default key for nested array children (likely generated by v-for) - if (c.tag && c.key == null && nestedIndex != null) { - c.key = `__vlist_${nestedIndex}_${i}__` - } - res.push(c) - } - } - } - return res - } -} - -function createTextVNode (val) { - return new VNode(undefined, undefined, undefined, String(val)) -} - -function applyNS (vnode, ns) { - if (vnode.tag && !vnode.ns) { - vnode.ns = ns - if (vnode.children) { - for (let i = 0, l = vnode.children.length; i < l; i++) { - applyNS(vnode.children[i], ns) - } - } - } -} - -export function getFirstComponentChild (children: ?Array) { - return children && children.filter(c => c && c.componentOptions)[0] -} - -export function mergeVNodeHook (def: Object, hookKey: string, hook: Function, key: string) { - key = key + hookKey - const injectedHash = def.__injected || (def.__injected = {}) - if (!injectedHash[key]) { - injectedHash[key] = true - const oldHook = def[hookKey] - if (oldHook) { - def[hookKey] = function () { - oldHook.apply(this, arguments) - hook.apply(this, arguments) - } - } else { - def[hookKey] = hook - } - } -} - -export function updateListeners ( - on: Object, - oldOn: Object, - add: Function, - remove: Function, - vm: Component -) { - let name, cur, old, fn, event, capture - for (name in on) { - cur = on[name] - old = oldOn[name] - if (!cur) { - process.env.NODE_ENV !== 'production' && warn( - `Invalid handler for event "${name}": got ` + String(cur), - vm - ) - } else if (!old) { - capture = name.charAt(0) === '!' - event = capture ? name.slice(1) : name - if (Array.isArray(cur)) { - add(event, (cur.invoker = arrInvoker(cur)), capture) - } else { - if (!cur.invoker) { - fn = cur - cur = on[name] = {} - cur.fn = fn - cur.invoker = fnInvoker(cur) - } - add(event, cur.invoker, capture) - } - } else if (cur !== old) { - if (Array.isArray(old)) { - old.length = cur.length - for (let i = 0; i < old.length; i++) old[i] = cur[i] - on[name] = old - } else { - old.fn = cur - on[name] = old - } - } - } - for (name in oldOn) { - if (!on[name]) { - event = name.charAt(0) === '!' ? name.slice(1) : name - remove(event, oldOn[name].invoker) - } - } -} - -function arrInvoker (arr: Array): Function { - return function (ev) { - const single = arguments.length === 1 - for (let i = 0; i < arr.length; i++) { - single ? arr[i](ev) : arr[i].apply(null, arguments) - } - } -} - -function fnInvoker (o: { fn: Function }): Function { - return function (ev) { - const single = arguments.length === 1 - single ? o.fn(ev) : o.fn.apply(null, arguments) - } -} diff --git a/src/core/vdom/helpers/index.js b/src/core/vdom/helpers/index.js new file mode 100644 index 00000000000..e5087409573 --- /dev/null +++ b/src/core/vdom/helpers/index.js @@ -0,0 +1,9 @@ +/* @flow */ + +export * from './merge-hook' +export * from './update-listeners' +export * from './normalize-children' + +export function getFirstComponentChild (children: ?Array): ?VNodeWithData { + return children && children.filter(c => c && c.componentOptions)[0] +} diff --git a/src/core/vdom/helpers/merge-hook.js b/src/core/vdom/helpers/merge-hook.js new file mode 100644 index 00000000000..b23a4d9841c --- /dev/null +++ b/src/core/vdom/helpers/merge-hook.js @@ -0,0 +1,18 @@ +/* @flow */ + +export function mergeVNodeHook (def: Object, hookKey: string, hook: Function, key: string) { + key = key + hookKey + const injectedHash = def.__injected || (def.__injected = {}) + if (!injectedHash[key]) { + injectedHash[key] = true + const oldHook = def[hookKey] + if (oldHook) { + def[hookKey] = function () { + oldHook.apply(this, arguments) + hook.apply(this, arguments) + } + } else { + def[hookKey] = hook + } + } +} diff --git a/src/core/vdom/helpers/normalize-children.js b/src/core/vdom/helpers/normalize-children.js new file mode 100644 index 00000000000..4c95399e420 --- /dev/null +++ b/src/core/vdom/helpers/normalize-children.js @@ -0,0 +1,62 @@ +/* @flow */ + +import { isPrimitive } from 'core/util/index' +import VNode from 'core/vdom/vnode' + +export function normalizeChildren ( + children: any, + ns: string | void, + nestedIndex: number | void +): Array | void { + if (isPrimitive(children)) { + return [createTextVNode(children)] + } + if (Array.isArray(children)) { + const res = [] + for (let i = 0, l = children.length; i < l; i++) { + const c = children[i] + const last = res[res.length - 1] + // nested + if (Array.isArray(c)) { + res.push.apply(res, normalizeChildren(c, ns, i)) + } else if (isPrimitive(c)) { + if (last && last.text) { + last.text += String(c) + } else if (c !== '') { + // convert primitive to vnode + res.push(createTextVNode(c)) + } + } else if (c instanceof VNode) { + if (c.text && last && last.text) { + last.text += c.text + } else { + // inherit parent namespace + if (ns) { + applyNS(c, ns) + } + // default key for nested array children (likely generated by v-for) + if (c.tag && c.key == null && nestedIndex != null) { + c.key = `__vlist_${nestedIndex}_${i}__` + } + res.push(c) + } + } + } + return res + } +} + +function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) +} + +function applyNS (vnode, ns) { + if (vnode.tag && !vnode.ns) { + vnode.ns = ns + if (vnode.children) { + for (let i = 0, l = vnode.children.length; i < l; i++) { + applyNS(vnode.children[i], ns) + } + } + } +} diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js new file mode 100644 index 00000000000..3feef8cbe39 --- /dev/null +++ b/src/core/vdom/helpers/update-listeners.js @@ -0,0 +1,68 @@ +/* @flow */ + +import { warn } from 'core/util/index' + +export function updateListeners ( + on: Object, + oldOn: Object, + add: Function, + remove: Function, + vm: Component +) { + let name, cur, old, fn, event, capture + for (name in on) { + cur = on[name] + old = oldOn[name] + if (!cur) { + process.env.NODE_ENV !== 'production' && warn( + `Invalid handler for event "${name}": got ` + String(cur), + vm + ) + } else if (!old) { + capture = name.charAt(0) === '!' + event = capture ? name.slice(1) : name + if (Array.isArray(cur)) { + add(event, (cur.invoker = arrInvoker(cur)), capture) + } else { + if (!cur.invoker) { + fn = cur + cur = on[name] = {} + cur.fn = fn + cur.invoker = fnInvoker(cur) + } + add(event, cur.invoker, capture) + } + } else if (cur !== old) { + if (Array.isArray(old)) { + old.length = cur.length + for (let i = 0; i < old.length; i++) old[i] = cur[i] + on[name] = old + } else { + old.fn = cur + on[name] = old + } + } + } + for (name in oldOn) { + if (!on[name]) { + event = name.charAt(0) === '!' ? name.slice(1) : name + remove(event, oldOn[name].invoker) + } + } +} + +function arrInvoker (arr: Array): Function { + return function (ev) { + const single = arguments.length === 1 + for (let i = 0; i < arr.length; i++) { + single ? arr[i](ev) : arr[i].apply(null, arguments) + } + } +} + +function fnInvoker (o: { fn: Function }): Function { + return function (ev) { + const single = arguments.length === 1 + single ? o.fn(ev) : o.fn.apply(null, arguments) + } +} diff --git a/src/core/vdom/modules/directives.js b/src/core/vdom/modules/directives.js index 513afe51ebc..f5f92f3d465 100644 --- a/src/core/vdom/modules/directives.js +++ b/src/core/vdom/modules/directives.js @@ -1,7 +1,7 @@ /* @flow */ import { resolveAsset } from 'core/util/options' -import { mergeVNodeHook } from 'core/vdom/helpers' +import { mergeVNodeHook } from 'core/vdom/helpers/index' import { emptyNode } from 'core/vdom/patch' export default { diff --git a/src/platforms/web/runtime/components/transition.js b/src/platforms/web/runtime/components/transition.js index 321ef4038d7..7018a9931c9 100644 --- a/src/platforms/web/runtime/components/transition.js +++ b/src/platforms/web/runtime/components/transition.js @@ -5,7 +5,7 @@ import { warn } from 'core/util/index' import { camelize, extend } from 'shared/util' -import { mergeVNodeHook, getFirstComponentChild } from 'core/vdom/helpers' +import { mergeVNodeHook, getFirstComponentChild } from 'core/vdom/helpers/index' export const transitionProps = { name: String, diff --git a/src/platforms/web/runtime/modules/events.js b/src/platforms/web/runtime/modules/events.js index 40c15530ec0..cf0c5441d99 100644 --- a/src/platforms/web/runtime/modules/events.js +++ b/src/platforms/web/runtime/modules/events.js @@ -1,7 +1,7 @@ // skip type checking this file because we need to attach private properties // to elements -import { updateListeners } from 'core/vdom/helpers' +import { updateListeners } from 'core/vdom/helpers/index' function updateDOMListeners (oldVnode, vnode) { if (!oldVnode.data.on && !vnode.data.on) { diff --git a/src/platforms/web/runtime/modules/transition.js b/src/platforms/web/runtime/modules/transition.js index cb5ceb8a458..79bc5fc280d 100644 --- a/src/platforms/web/runtime/modules/transition.js +++ b/src/platforms/web/runtime/modules/transition.js @@ -2,7 +2,7 @@ import { inBrowser, isIE9 } from 'core/util/index' import { cached, extend } from 'shared/util' -import { mergeVNodeHook } from 'core/vdom/helpers' +import { mergeVNodeHook } from 'core/vdom/helpers/index' import { activeInstance } from 'core/instance/lifecycle' import { nextFrame, From 5d0999ac375f477a9f2746ea746081d07ee60c64 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Oct 2016 15:59:14 -0400 Subject: [PATCH 06/30] ensure v-model runtime metadata for all types --- src/platforms/web/compiler/directives/model.js | 12 ++++-------- src/platforms/web/runtime/directives/model.js | 5 ++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/platforms/web/compiler/directives/model.js b/src/platforms/web/compiler/directives/model.js index a59752e6b10..5f8639cd6b5 100644 --- a/src/platforms/web/compiler/directives/model.js +++ b/src/platforms/web/compiler/directives/model.js @@ -25,14 +25,16 @@ export default function model ( } } if (tag === 'select') { - return genSelect(el, value) + genSelect(el, value) } else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value) } else if (tag === 'input' && type === 'radio') { genRadioModel(el, value) } else { - return genDefaultModel(el, value, modifiers) + genDefaultModel(el, value, modifiers) } + // ensure runtime directive metadata + return true } function genCheckboxModel (el: ASTElement, value: string) { @@ -128,10 +130,6 @@ function genDefaultModel ( } addProp(el, 'value', isNative ? `_s(${value})` : `(${value})`) addHandler(el, event, code, null, true) - if (needCompositionGuard) { - // need runtime directive code to help with composition events - return true - } } function genSelect (el: ASTElement, value: string) { @@ -143,8 +141,6 @@ function genSelect (el: ASTElement, value: string) { `.map(function(o){return "_value" in o ? o._value : o.value})` + (el.attrsMap.multiple == null ? '[0]' : '') addHandler(el, 'change', code, null, true) - // need runtime to help with possible dynamically generated options - return true } function checkOptionWarning (option: any): boolean { diff --git a/src/platforms/web/runtime/directives/model.js b/src/platforms/web/runtime/directives/model.js index 8c66721a2a4..d3e72f0d3fb 100644 --- a/src/platforms/web/runtime/directives/model.js +++ b/src/platforms/web/runtime/directives/model.js @@ -40,7 +40,10 @@ export default { if (isIE || isEdge) { setTimeout(cb, 0) } - } else if (vnode.tag === 'textarea' || el.type === 'text') { + } else if ( + (vnode.tag === 'textarea' || el.type === 'text') && + !binding.modifiers.lazy + ) { if (!isAndroid) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) From ea39d9f6b8220d24320fdef08070a01b94f241c6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Oct 2016 16:31:56 -0400 Subject: [PATCH 07/30] fix static node v-for nested check --- src/compiler/optimizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/optimizer.js b/src/compiler/optimizer.js index 42e2421527b..ba4288cb9ec 100644 --- a/src/compiler/optimizer.js +++ b/src/compiler/optimizer.js @@ -57,7 +57,7 @@ function markStaticRoots (node: ASTNode, isInFor: boolean) { } if (node.children) { for (let i = 0, l = node.children.length; i < l; i++) { - markStaticRoots(node.children[i], !!node.for) + markStaticRoots(node.children[i], isInFor || !!node.for) } } } From 6ab10c0559094b6870b5c6601ae526ee579b796b Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Oct 2016 17:12:12 -0400 Subject: [PATCH 08/30] fix v-for list auto-keying with nested