Skip to content

Commit 1c7d737

Browse files
committed
feat: support v-bind .prop & .attr modifiers
Also allows render function usage like the following: ```js h({ '.prop': 1, // force set as property '^attr': 'foo' // force set as attribute }) ```
1 parent 00f0b3c commit 1c7d737

File tree

9 files changed

+279
-60
lines changed

9 files changed

+279
-60
lines changed

packages/compiler-core/__tests__/parse.spec.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,54 @@ describe('compiler: parse', () => {
12761276
})
12771277
})
12781278

1279+
test('v-bind .prop shorthand', () => {
1280+
const ast = baseParse('<div .a=b />')
1281+
const directive = (ast.children[0] as ElementNode).props[0]
1282+
1283+
expect(directive).toStrictEqual({
1284+
type: NodeTypes.DIRECTIVE,
1285+
name: 'bind',
1286+
arg: {
1287+
type: NodeTypes.SIMPLE_EXPRESSION,
1288+
content: 'a',
1289+
isStatic: true,
1290+
constType: ConstantTypes.CAN_STRINGIFY,
1291+
1292+
loc: {
1293+
source: 'a',
1294+
start: {
1295+
column: 7,
1296+
line: 1,
1297+
offset: 6
1298+
},
1299+
end: {
1300+
column: 8,
1301+
line: 1,
1302+
offset: 7
1303+
}
1304+
}
1305+
},
1306+
modifiers: ['prop'],
1307+
exp: {
1308+
type: NodeTypes.SIMPLE_EXPRESSION,
1309+
content: 'b',
1310+
isStatic: false,
1311+
constType: ConstantTypes.NOT_CONSTANT,
1312+
1313+
loc: {
1314+
start: { offset: 8, line: 1, column: 9 },
1315+
end: { offset: 9, line: 1, column: 10 },
1316+
source: 'b'
1317+
}
1318+
},
1319+
loc: {
1320+
start: { offset: 5, line: 1, column: 6 },
1321+
end: { offset: 9, line: 1, column: 10 },
1322+
source: '.a=b'
1323+
}
1324+
})
1325+
})
1326+
12791327
test('v-bind shorthand with modifier', () => {
12801328
const ast = baseParse('<div :a.sync=b />')
12811329
const directive = (ast.children[0] as ElementNode).props[0]

packages/compiler-core/__tests__/transforms/vBind.spec.ts

+129-11
Original file line numberDiff line numberDiff line change
@@ -172,22 +172,140 @@ describe('compiler: transform v-bind', () => {
172172
const node = parseWithVBind(`<div v-bind:[foo(bar)].camel="id"/>`, {
173173
prefixIdentifiers: true
174174
})
175+
const props = (node.codegenNode as VNodeCall).props as CallExpression
176+
expect(props).toMatchObject({
177+
type: NodeTypes.JS_CALL_EXPRESSION,
178+
callee: NORMALIZE_PROPS,
179+
arguments: [
180+
{
181+
type: NodeTypes.JS_OBJECT_EXPRESSION,
182+
properties: [
183+
{
184+
key: {
185+
children: [
186+
`_${helperNameMap[CAMELIZE]}(`,
187+
`(`,
188+
{ content: `_ctx.foo` },
189+
`(`,
190+
{ content: `_ctx.bar` },
191+
`)`,
192+
`) || ""`,
193+
`)`
194+
]
195+
},
196+
value: {
197+
content: `_ctx.id`,
198+
isStatic: false
199+
}
200+
}
201+
]
202+
}
203+
]
204+
})
205+
})
206+
207+
test('.prop modifier', () => {
208+
const node = parseWithVBind(`<div v-bind:fooBar.prop="id"/>`)
175209
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
176210
expect(props.properties[0]).toMatchObject({
177211
key: {
178-
children: [
179-
`_${helperNameMap[CAMELIZE]}(`,
180-
`(`,
181-
{ content: `_ctx.foo` },
182-
`(`,
183-
{ content: `_ctx.bar` },
184-
`)`,
185-
`) || ""`,
186-
`)`
187-
]
212+
content: `.fooBar`,
213+
isStatic: true
188214
},
189215
value: {
190-
content: `_ctx.id`,
216+
content: `id`,
217+
isStatic: false
218+
}
219+
})
220+
})
221+
222+
test('.prop modifier w/ dynamic arg', () => {
223+
const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
224+
const props = (node.codegenNode as VNodeCall).props as CallExpression
225+
expect(props).toMatchObject({
226+
type: NodeTypes.JS_CALL_EXPRESSION,
227+
callee: NORMALIZE_PROPS,
228+
arguments: [
229+
{
230+
type: NodeTypes.JS_OBJECT_EXPRESSION,
231+
properties: [
232+
{
233+
key: {
234+
content: '`.${fooBar || ""}`',
235+
isStatic: false
236+
},
237+
value: {
238+
content: `id`,
239+
isStatic: false
240+
}
241+
}
242+
]
243+
}
244+
]
245+
})
246+
})
247+
248+
test('.prop modifier w/ dynamic arg + prefixIdentifiers', () => {
249+
const node = parseWithVBind(`<div v-bind:[foo(bar)].prop="id"/>`, {
250+
prefixIdentifiers: true
251+
})
252+
const props = (node.codegenNode as VNodeCall).props as CallExpression
253+
expect(props).toMatchObject({
254+
type: NodeTypes.JS_CALL_EXPRESSION,
255+
callee: NORMALIZE_PROPS,
256+
arguments: [
257+
{
258+
type: NodeTypes.JS_OBJECT_EXPRESSION,
259+
properties: [
260+
{
261+
key: {
262+
children: [
263+
`'.' + (`,
264+
`(`,
265+
{ content: `_ctx.foo` },
266+
`(`,
267+
{ content: `_ctx.bar` },
268+
`)`,
269+
`) || ""`,
270+
`)`
271+
]
272+
},
273+
value: {
274+
content: `_ctx.id`,
275+
isStatic: false
276+
}
277+
}
278+
]
279+
}
280+
]
281+
})
282+
})
283+
284+
test('.prop modifier (shorthand)', () => {
285+
const node = parseWithVBind(`<div .fooBar="id"/>`)
286+
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
287+
expect(props.properties[0]).toMatchObject({
288+
key: {
289+
content: `.fooBar`,
290+
isStatic: true
291+
},
292+
value: {
293+
content: `id`,
294+
isStatic: false
295+
}
296+
})
297+
})
298+
299+
test('.attr modifier', () => {
300+
const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
301+
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
302+
expect(props.properties[0]).toMatchObject({
303+
key: {
304+
content: `^foo-bar`,
305+
isStatic: true
306+
},
307+
value: {
308+
content: `id`,
191309
isStatic: false
192310
}
193311
})

packages/compiler-core/src/ast.ts

+2
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export interface SimpleExpressionNode extends Node {
221221
* the identifiers declared inside the function body.
222222
*/
223223
identifiers?: string[]
224+
isHandlerKey?: boolean
224225
}
225226

226227
export interface InterpolationNode extends Node {
@@ -243,6 +244,7 @@ export interface CompoundExpressionNode extends Node {
243244
* the identifiers declared inside the function body.
244245
*/
245246
identifiers?: string[]
247+
isHandlerKey?: boolean
246248
}
247249

248250
export interface IfNode extends Node {

packages/compiler-core/src/parse.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -772,14 +772,19 @@ function parseAttribute(
772772
}
773773
const loc = getSelection(context, start)
774774

775-
if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
776-
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
775+
if (!context.inVPre && /^(v-|:|\.|@|#)/.test(name)) {
776+
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
777777
name
778778
)!
779779

780+
let isPropShorthand = startsWith(name, '.')
780781
let dirName =
781782
match[1] ||
782-
(startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot')
783+
(isPropShorthand || startsWith(name, ':')
784+
? 'bind'
785+
: startsWith(name, '@')
786+
? 'on'
787+
: 'slot')
783788
let arg: ExpressionNode | undefined
784789

785790
if (match[2]) {
@@ -835,6 +840,7 @@ function parseAttribute(
835840
}
836841

837842
const modifiers = match[3] ? match[3].substr(1).split('.') : []
843+
if (isPropShorthand) modifiers.push('prop')
838844

839845
// 2.x compat v-bind:foo.sync -> v-model:foo
840846
if (__COMPAT__ && dirName === 'bind' && arg) {

packages/compiler-core/src/transforms/transformElement.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -700,21 +700,26 @@ export function buildProps(
700700
// but still need to deal with dynamic key binding
701701
let classKeyIndex = -1
702702
let styleKeyIndex = -1
703-
let dynamicKeyIndex = -1
703+
let hasDynamicKey = false
704704

705705
for (let i = 0; i < propsExpression.properties.length; i++) {
706-
const p = propsExpression.properties[i]
707-
if (p.key.type !== NodeTypes.SIMPLE_EXPRESSION) continue
708-
if (!isStaticExp(p.key)) dynamicKeyIndex = i
709-
if (isStaticExp(p.key) && p.key.content === 'class') classKeyIndex = i
710-
if (isStaticExp(p.key) && p.key.content === 'style') styleKeyIndex = i
706+
const key = propsExpression.properties[i].key
707+
if (isStaticExp(key)) {
708+
if (key.content === 'class') {
709+
classKeyIndex = i
710+
} else if (key.content === 'style') {
711+
styleKeyIndex = i
712+
}
713+
} else if (!key.isHandlerKey) {
714+
hasDynamicKey = true
715+
}
711716
}
712717

713718
const classProp = propsExpression.properties[classKeyIndex]
714719
const styleProp = propsExpression.properties[styleKeyIndex]
715720

716721
// no dynamic key
717-
if (dynamicKeyIndex === -1) {
722+
if (!hasDynamicKey) {
718723
if (classProp && !isStaticExp(classProp.value)) {
719724
classProp.value = createCallExpression(
720725
context.helper(NORMALIZE_CLASS),

packages/compiler-core/src/transforms/vBind.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { DirectiveTransform } from '../transform'
2-
import { createObjectProperty, createSimpleExpression, NodeTypes } from '../ast'
2+
import {
3+
createObjectProperty,
4+
createSimpleExpression,
5+
ExpressionNode,
6+
NodeTypes
7+
} from '../ast'
38
import { createCompilerError, ErrorCodes } from '../errors'
49
import { camelize } from '@vue/shared'
510
import { CAMELIZE } from '../runtimeHelpers'
@@ -18,7 +23,6 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
1823
arg.content = `${arg.content} || ""`
1924
}
2025

21-
// .prop is no longer necessary due to new patch behavior
2226
// .sync is replaced by v-model:arg
2327
if (modifiers.includes('camel')) {
2428
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
@@ -33,6 +37,14 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
3337
}
3438
}
3539

40+
if (modifiers.includes('prop')) {
41+
injectPrefix(arg, '.')
42+
}
43+
44+
if (modifiers.includes('attr')) {
45+
injectPrefix(arg, '^')
46+
}
47+
3648
if (
3749
!exp ||
3850
(exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
@@ -47,3 +59,16 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
4759
props: [createObjectProperty(arg!, exp)]
4860
}
4961
}
62+
63+
const injectPrefix = (arg: ExpressionNode, prefix: string) => {
64+
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
65+
if (arg.isStatic) {
66+
arg.content = prefix + arg.content
67+
} else {
68+
arg.content = `\`${prefix}\${${arg.content}}\``
69+
}
70+
} else {
71+
arg.children.unshift(`'${prefix}' + (`)
72+
arg.children.push(`)`)
73+
}
74+
}

packages/compiler-core/src/transforms/vOn.ts

+2
Original file line numberDiff line numberDiff line change
@@ -163,5 +163,7 @@ export const transformOn: DirectiveTransform = (
163163
ret.props[0].value = context.cache(ret.props[0].value)
164164
}
165165

166+
// mark the key as handler for props normalization check
167+
ret.props.forEach(p => (p.key.isHandlerKey = true))
166168
return ret
167169
}

packages/runtime-dom/__tests__/patchProps.spec.ts

+14
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ describe('runtime-dom: props patching', () => {
171171
patchProp(el, 'type', 'text', null)
172172
})
173173

174+
test('force patch as prop', () => {
175+
const el = document.createElement('div') as any
176+
patchProp(el, '.x', null, 1)
177+
expect(el.x).toBe(1)
178+
})
179+
180+
test('force patch as attribute', () => {
181+
const el = document.createElement('div') as any
182+
el.x = 1
183+
patchProp(el, '^x', null, 2)
184+
expect(el.x).toBe(1)
185+
expect(el.getAttribute('x')).toBe('2')
186+
})
187+
174188
test('input with size', () => {
175189
const el = document.createElement('input')
176190
patchProp(el, 'size', null, 100)

0 commit comments

Comments
 (0)