Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,129 @@ return function render(_ctx, _cache) {
}"
`;

exports[`compiler: v-if > codegen > user-defined keys > avoid duplicate keys 1`] = `
"const _Vue = Vue

return function render(_ctx, _cache) {
with (_ctx) {
const { mergeProps: _mergeProps, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue

return ok
? (_openBlock(), _createElementBlock("div", _mergeProps({ key: "custom_key" }, obj), null, 16 /* FULL_PROPS */))
: _createCommentVNode("v-if", true)
}
}"
`;

exports[`compiler: v-if > codegen > user-defined keys > correct key matching 1`] = `
"const _Vue = Vue
const { createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = Symbol()

return function render(_ctx, _cache) {
with (_ctx) {
const { normalizeProps: _normalizeProps, guardReactiveProps: _guardReactiveProps, openBlock: _openBlock, createElementBlock: _createElementBlock, mergeProps: _mergeProps, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue

return (_openBlock(), _createElementBlock(_Fragment, null, [
ok
? (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: 0 }, separate)), null, 16 /* FULL_PROPS */))
: _createCommentVNode("v-if", true),
(_openBlock(), _createElementBlock("div", _mergeProps({ key: 123 }, other1), null, 16 /* FULL_PROPS */)),
ok1
? (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_1 }, obj1)), null, 16 /* FULL_PROPS */))
: (_openBlock(), _createElementBlock("div", _mergeProps({ key: 0 }, obj2), null, 16 /* FULL_PROPS */))
], 64 /* STABLE_FRAGMENT */))
}
}"
`;

exports[`compiler: v-if > codegen > user-defined keys > key on v-else 1`] = `
"const _Vue = Vue
const { createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = Symbol()
const _hoisted_2 = Symbol()

return function render(_ctx, _cache) {
with (_ctx) {
const { normalizeProps: _normalizeProps, guardReactiveProps: _guardReactiveProps, openBlock: _openBlock, createElementBlock: _createElementBlock, mergeProps: _mergeProps, createCommentVNode: _createCommentVNode } = _Vue

return ok1
? (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_1 }, obj1)), null, 16 /* FULL_PROPS */))
: ok2
? (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_2 }, obj2)), null, 16 /* FULL_PROPS */))
: (_openBlock(), _createElementBlock("div", _mergeProps({ key: 0 }, obj3), null, 16 /* FULL_PROPS */))
}
}"
`;

exports[`compiler: v-if > codegen > user-defined keys > key on v-else with misc irrelevant nodes between 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = Symbol()
const _hoisted_2 = Symbol()

return function render(_ctx, _cache) {
with (_ctx) {
const { normalizeProps: _normalizeProps, guardReactiveProps: _guardReactiveProps, openBlock: _openBlock, createElementBlock: _createElementBlock, mergeProps: _mergeProps, createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, Fragment: _Fragment } = _Vue

return ok1
? (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_1 }, obj1)), null, 16 /* FULL_PROPS */))
: ok2
? (_openBlock(), _createElementBlock(_Fragment, { key: _hoisted_2 }, [
_createCommentVNode("comment1"),
_createElementVNode("div", _normalizeProps(_guardReactiveProps(obj2)), null, 16 /* FULL_PROPS */)
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
: (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
_createCommentVNode("comment2"),
(_openBlock(), _createElementBlock("div", _mergeProps({ key: 0 }, obj3), null, 16 /* FULL_PROPS */))
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
}
}"
`;

exports[`compiler: v-if > codegen > user-defined keys > key on v-else-if 1`] = `
"const _Vue = Vue
const { createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = Symbol()
const _hoisted_2 = Symbol()

return function render(_ctx, _cache) {
with (_ctx) {
const { normalizeProps: _normalizeProps, guardReactiveProps: _guardReactiveProps, openBlock: _openBlock, createElementBlock: _createElementBlock, mergeProps: _mergeProps, createCommentVNode: _createCommentVNode } = _Vue

return ok1
? (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_1 }, obj1)), null, 16 /* FULL_PROPS */))
: ok2
? (_openBlock(), _createElementBlock("div", _mergeProps({ key: 0 }, obj2), null, 16 /* FULL_PROPS */))
: (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_2 }, obj3)), null, 16 /* FULL_PROPS */))
}
}"
`;

exports[`compiler: v-if > codegen > user-defined keys > key on v-if 1`] = `
"const _Vue = Vue
const { createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = Symbol()
const _hoisted_2 = Symbol()

return function render(_ctx, _cache) {
with (_ctx) {
const { mergeProps: _mergeProps, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, normalizeProps: _normalizeProps, guardReactiveProps: _guardReactiveProps } = _Vue

return ok1
? (_openBlock(), _createElementBlock("div", _mergeProps({ key: 1 }, obj1), null, 16 /* FULL_PROPS */))
: ok2
? (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_1 }, obj2)), null, 16 /* FULL_PROPS */))
: (_openBlock(), _createElementBlock("div", _normalizeProps(_mergeProps({ key: _hoisted_2 }, obj3)), null, 16 /* FULL_PROPS */))
}
}"
`;

exports[`compiler: v-if > codegen > v-if + v-else 1`] = `
"const _Vue = Vue

Expand Down
137 changes: 120 additions & 17 deletions packages/compiler-core/__tests__/transforms/vIf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type CompilerOptions,
TO_HANDLERS,
generate,
transformBind,
transformVBindShorthand,
} from '../../src'
import {
Expand All @@ -37,6 +38,7 @@ function parseWithIfTransform(
options: CompilerOptions = {},
returnIndex: number = 0,
childrenLen: number = 1,
expectAllChildrenIfNodes: boolean = true,
) {
const ast = parse(template, options)
transform(ast, {
Expand All @@ -50,8 +52,10 @@ function parseWithIfTransform(
})
if (!options.onError) {
expect(ast.children.length).toBe(childrenLen)
for (let i = 0; i < childrenLen; i++) {
expect(ast.children[i].type).toBe(NodeTypes.IF)
if (expectAllChildrenIfNodes) {
for (let i = 0; i < childrenLen; i++) {
expect(ast.children[i].type).toBe(NodeTypes.IF)
}
}
}
return {
Expand Down Expand Up @@ -670,21 +674,120 @@ describe('compiler: v-if', () => {
expect(branch1.props).toMatchObject(createObjectMatcher({ key: `[0]` }))
})

// #6631
test('avoid duplicate keys', () => {
const {
node: { codegenNode },
} = parseWithIfTransform(`<div v-if="ok" key="custom_key" v-bind="obj"/>`)
const branch1 = codegenNode.consequent as VNodeCall
expect(branch1.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
createObjectMatcher({
key: 'custom_key',
}),
{ content: `obj` },
],
describe('user-defined keys', () => {
// #6631
test('avoid duplicate keys', () => {
const {
root,
node: { codegenNode },
} = parseWithIfTransform(
`<div v-if="ok" key="custom_key" v-bind="obj"/>`,
)
const branch1 = codegenNode.consequent as VNodeCall
expect(branch1.props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: MERGE_PROPS,
arguments: [
createObjectMatcher({
key: 'custom_key',
}),
{ content: `obj` },
],
})
expect(generate(root).code).toMatchSnapshot()
})

test('key on v-if', () => {
const { root } = parseWithIfTransform(
`<div v-if="ok1" :key="1" v-bind="obj1"/><div v-else-if="ok2" v-bind="obj2"/><div v-else v-bind="obj3"/>`,
{ directiveTransforms: { bind: transformBind } },
)
expect(root.hoists).toMatchObject([
{ content: 'Symbol()' },
{ content: 'Symbol()' },
])

const code = generate(root).code
expect(code).toContain('_mergeProps({ key: 1 }, obj1)')
expect(code).toContain('_mergeProps({ key: _hoisted_1 }, obj2)')
expect(code).toContain('_mergeProps({ key: _hoisted_2 }, obj3)')
expect(code).toMatchSnapshot()
})

test('key on v-else-if', () => {
const { root } = parseWithIfTransform(
`<div v-if="ok1" v-bind="obj1"/><div v-else-if="ok2" :key="0" v-bind="obj2"/><div v-else v-bind="obj3"/>`,
{ directiveTransforms: { bind: transformBind } },
)
expect(root.hoists).toMatchObject([
{ content: 'Symbol()' },
{ content: 'Symbol()' },
])

const code = generate(root).code
expect(code).toContain('_mergeProps({ key: _hoisted_1 }, obj1)')
expect(code).toContain('_mergeProps({ key: 0 }, obj2)')
expect(code).toContain('_mergeProps({ key: _hoisted_2 }, obj3)')
expect(code).toMatchSnapshot()
})

test('key on v-else', () => {
const { root } = parseWithIfTransform(
`<div v-if="ok1" v-bind="obj1"/><div v-else-if="ok2" v-bind="obj2"/><div v-else :key="0" v-bind="obj3"/>`,
{ directiveTransforms: { bind: transformBind } },
)
expect(root.hoists).toMatchObject([
{ content: 'Symbol()' },
{ content: 'Symbol()' },
])

const code = generate(root).code
expect(code).toContain('_mergeProps({ key: _hoisted_1 }, obj1)')
expect(code).toContain('_mergeProps({ key: _hoisted_2 }, obj2)')
expect(code).toContain('_mergeProps({ key: 0 }, obj3)')
expect(code).toMatchSnapshot()
})

test('key on v-else with misc irrelevant nodes between', () => {
const { root } = parseWithIfTransform(
`<div v-if="ok1" v-bind="obj1"/> <!--comment1--> <div v-else-if="ok2" v-bind="obj2"/> <!--comment2--> <div v-else :key="0" v-bind="obj3"/>`,
{ directiveTransforms: { bind: transformBind } },
0,
1,
false,
)
expect(root.hoists).toMatchObject([
{ content: 'Symbol()' },
{ content: 'Symbol()' },
])

const code = generate(root).code
expect(code).toContain('_mergeProps({ key: _hoisted_1 }, obj1)')
expect(code).toContain(
'_createElementBlock(_Fragment, { key: _hoisted_2 }',
)
expect(code).toContain('_normalizeProps(_guardReactiveProps(obj2))')
expect(code).toContain('_mergeProps({ key: 0 }, obj3)')
expect(code).toMatchSnapshot()
})

test('correct key matching', () => {
const { root } = parseWithIfTransform(
`<div v-if="ok" v-bind="separate"/><div :key="123" v-bind="other1" /><div v-if="ok1" v-bind="obj1"/><div v-else :key="0" v-bind="obj2"/>`,
{ directiveTransforms: { bind: transformBind } },
0,
3,
false,
)
expect(root.hoists).toMatchObject([{ content: 'Symbol()' }])

const code = generate(root).code
// not part of if-block with a key anywhere so expect
expect(code).toContain('_mergeProps({ key: 0 }, separate)')
expect(code).toContain('_mergeProps({ key: 123 }, other1)')
expect(code).toContain('_mergeProps({ key: _hoisted_1 }, obj1)')
expect(code).toContain('_mergeProps({ key: 0 }, obj2)')
expect(code).toMatchSnapshot()
})
})

Expand Down
1 change: 1 addition & 0 deletions packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export interface IfNode extends Node {
type: NodeTypes.IF
branches: IfBranchNode[]
codegenNode?: IfConditionalExpression | CacheExpression // <div v-if v-once>
anyBranchesHaveUserDefinedKey?: boolean
}

export interface IfBranchNode extends Node {
Expand Down
Loading
Loading