feat(compiler-core): support v-bind shorthand for key and value with the same name (#9451)

This commit is contained in:
zhiyuanzmj 2023-11-02 17:48:11 +08:00 committed by GitHub
parent 48b47a1ab6
commit 26399aa6fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 6 deletions

View File

@ -72,6 +72,60 @@ describe('compiler: transform v-bind', () => {
}) })
}) })
test('no expression', () => {
const node = parseWithVBind(`<div v-bind:id />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `id`,
isStatic: true,
loc: {
start: {
line: 1,
column: 13,
offset: 12
},
end: {
line: 1,
column: 15,
offset: 14
}
}
},
value: {
content: `id`,
isStatic: false,
loc: {
start: {
line: 1,
column: 1,
offset: 0
},
end: {
line: 1,
column: 1,
offset: 0
}
}
}
})
})
test('no expression (shorthand)', () => {
const node = parseWithVBind(`<div :id />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `id`,
isStatic: true
},
value: {
content: `id`,
isStatic: false
}
})
})
test('dynamic arg', () => { test('dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[id]="id"/>`) const node = parseWithVBind(`<div v-bind:[id]="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression const props = (node.codegenNode as VNodeCall).props as CallExpression
@ -98,9 +152,9 @@ describe('compiler: transform v-bind', () => {
}) })
}) })
test('should error if no expression', () => { test('should error if empty expression', () => {
const onError = vi.fn() const onError = vi.fn()
const node = parseWithVBind(`<div v-bind:arg />`, { onError }) const node = parseWithVBind(`<div v-bind:arg="" />`, { onError })
const props = (node.codegenNode as VNodeCall).props as ObjectExpression const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(onError.mock.calls[0][0]).toMatchObject({ expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_V_BIND_NO_EXPRESSION, code: ErrorCodes.X_V_BIND_NO_EXPRESSION,
@ -111,7 +165,7 @@ describe('compiler: transform v-bind', () => {
}, },
end: { end: {
line: 1, line: 1,
column: 16 column: 19
} }
} }
}) })
@ -142,6 +196,21 @@ describe('compiler: transform v-bind', () => {
}) })
}) })
test('.camel modifier w/ no expression', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.camel />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `fooBar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
test('.camel modifier w/ dynamic arg', () => { test('.camel modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`) const node = parseWithVBind(`<div v-bind:[foo].camel="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression const props = (node.codegenNode as VNodeCall).props as CallExpression
@ -219,6 +288,21 @@ describe('compiler: transform v-bind', () => {
}) })
}) })
test('.prop modifier w/ no expression', () => {
const node = parseWithVBind(`<div v-bind:fooBar.prop />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `.fooBar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
test('.prop modifier w/ dynamic arg', () => { test('.prop modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`) const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression const props = (node.codegenNode as VNodeCall).props as CallExpression
@ -296,6 +380,21 @@ describe('compiler: transform v-bind', () => {
}) })
}) })
test('.prop modifier (shortband) w/ no expression', () => {
const node = parseWithVBind(`<div .fooBar />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `.fooBar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
test('.attr modifier', () => { test('.attr modifier', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`) const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression const props = (node.codegenNode as VNodeCall).props as ObjectExpression
@ -310,4 +409,19 @@ describe('compiler: transform v-bind', () => {
} }
}) })
}) })
test('.attr modifier w/ no expression', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.attr />`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `^foo-bar`,
isStatic: true
},
value: {
content: `fooBar`,
isStatic: false
}
})
})
}) })

View File

@ -3,17 +3,19 @@ import {
createObjectProperty, createObjectProperty,
createSimpleExpression, createSimpleExpression,
ExpressionNode, ExpressionNode,
locStub,
NodeTypes NodeTypes
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { camelize } from '@vue/shared' import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeHelpers' import { CAMELIZE } from '../runtimeHelpers'
import { processExpression } from './transformExpression'
// v-bind without arg is handled directly in ./transformElements.ts due to it affecting // v-bind without arg is handled directly in ./transformElements.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-bind // codegen for the entire props object. This transform here is only for v-bind
// *with* args. // *with* args.
export const transformBind: DirectiveTransform = (dir, _node, context) => { export const transformBind: DirectiveTransform = (dir, _node, context) => {
const { exp, modifiers, loc } = dir const { modifiers, loc } = dir
const arg = dir.arg! const arg = dir.arg!
if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) { if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) {
@ -46,6 +48,18 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
} }
} }
// :arg is replaced by :arg="arg"
let { exp } = dir
if (!exp && arg.type === NodeTypes.SIMPLE_EXPRESSION) {
const propName = camelize(arg.loc.source)
const simpleExpression = createSimpleExpression(propName, false, {
...locStub,
source: propName
})
exp = dir.exp = processExpression(simpleExpression, context)
}
if ( if (
!exp || !exp ||
(exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim()) (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())

View File

@ -79,7 +79,6 @@ exports[`source map 1`] = `
exports[`template errors 1`] = ` exports[`template errors 1`] = `
[ [
[SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)], [SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)],
[SyntaxError: v-bind is missing expression.],
[SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements.], [SyntaxError: v-model can only be used on <input>, <textarea> and <select> elements.],
] ]
`; `;

View File

@ -124,7 +124,7 @@ test('source map', () => {
test('template errors', () => { test('template errors', () => {
const result = compile({ const result = compile({
filename: 'example.vue', filename: 'example.vue',
source: `<div :foo source: `<div
:bar="a[" v-model="baz"/>` :bar="a[" v-model="baz"/>`
}) })
expect(result.errors).toMatchSnapshot() expect(result.errors).toMatchSnapshot()