Skip to content

Commit b0d01e9

Browse files
committed
dx(compiler-core): warn on <template v-for> key misplacement
Note: the behavior is different from Vue 2. `<template v-for>` are compiled into an array of Fragment vnodes so the key should be placed the `<template>` for v-for to use it for diffing.
1 parent de0c8a7 commit b0d01e9

File tree

3 files changed

+50
-2
lines changed

3 files changed

+50
-2
lines changed

Diff for: packages/compiler-core/__tests__/transforms/vFor.spec.ts

+28
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,34 @@ describe('compiler: v-for', () => {
263263
})
264264
)
265265
})
266+
267+
test('<template v-for> key placement', () => {
268+
const onError = jest.fn()
269+
parseWithForTransform(
270+
`
271+
<template v-for="item in items">
272+
<div :key="item.id"/>
273+
</template>`,
274+
{ onError }
275+
)
276+
277+
expect(onError).toHaveBeenCalledTimes(1)
278+
expect(onError).toHaveBeenCalledWith(
279+
expect.objectContaining({
280+
code: ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT
281+
})
282+
)
283+
284+
// should not warn on nested v-for keys
285+
parseWithForTransform(
286+
`
287+
<template v-for="item in items">
288+
<div v-for="c in item.children" :key="c.id"/>
289+
</template>`,
290+
{ onError }
291+
)
292+
expect(onError).toHaveBeenCalledTimes(1)
293+
})
266294
})
267295

268296
describe('source location', () => {

Diff for: packages/compiler-core/src/errors.ts

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const enum ErrorCodes {
6767
X_V_ELSE_NO_ADJACENT_IF,
6868
X_V_FOR_NO_EXPRESSION,
6969
X_V_FOR_MALFORMED_EXPRESSION,
70+
X_V_FOR_TEMPLATE_KEY_PLACEMENT,
7071
X_V_BIND_NO_EXPRESSION,
7172
X_V_ON_NO_EXPRESSION,
7273
X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
@@ -140,6 +141,7 @@ export const errorMessages: { [code: number]: string } = {
140141
[ErrorCodes.X_V_ELSE_NO_ADJACENT_IF]: `v-else/v-else-if has no adjacent v-if.`,
141142
[ErrorCodes.X_V_FOR_NO_EXPRESSION]: `v-for is missing expression.`,
142143
[ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression.`,
144+
[ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT]: `<template v-for> key should be placed on the <template> tag.`,
143145
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
144146
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
145147
[ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,

Diff for: packages/compiler-core/src/transforms/vFor.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
SimpleExpressionNode,
1111
createCallExpression,
1212
createFunctionExpression,
13-
ElementTypes,
1413
createObjectExpression,
1514
createObjectProperty,
1615
ForCodegenNode,
@@ -81,6 +80,25 @@ export const transformFor = createStructuralDirectiveTransform(
8180
let childBlock: BlockCodegenNode
8281
const isTemplate = isTemplateNode(node)
8382
const { children } = forNode
83+
84+
// check <template v-for> key placement
85+
if ((__DEV__ || !__BROWSER__) && isTemplate) {
86+
node.children.some(c => {
87+
if (c.type === NodeTypes.ELEMENT) {
88+
const key = findProp(c, 'key')
89+
if (key) {
90+
context.onError(
91+
createCompilerError(
92+
ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT,
93+
key.loc
94+
)
95+
)
96+
return true
97+
}
98+
}
99+
})
100+
}
101+
84102
const needFragmentWrapper =
85103
children.length !== 1 || children[0].type !== NodeTypes.ELEMENT
86104
const slotOutlet = isSlotOutlet(node)
@@ -183,7 +201,7 @@ export function processFor(
183201
keyAlias: key,
184202
objectIndexAlias: index,
185203
parseResult,
186-
children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
204+
children: isTemplateNode(node) ? node.children : [node]
187205
}
188206

189207
context.replaceNode(forNode)

0 commit comments

Comments
 (0)