Skip to content

Commit

Permalink
feat: support v-bind .prop & .attr modifiers
Browse files Browse the repository at this point in the history
Also allows render function usage like the following:

```js
h({
  '.prop': 1, // force set as property
  '^attr': 'foo' // force set as attribute
})
```
  • Loading branch information
yyx990803 committed Jul 16, 2021
1 parent 00f0b3c commit 1c7d737
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 60 deletions.
48 changes: 48 additions & 0 deletions packages/compiler-core/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,54 @@ describe('compiler: parse', () => {
})
})

test('v-bind .prop shorthand', () => {
const ast = baseParse('<div .a=b />')
const directive = (ast.children[0] as ElementNode).props[0]

expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: true,
constType: ConstantTypes.CAN_STRINGIFY,

loc: {
source: 'a',
start: {
column: 7,
line: 1,
offset: 6
},
end: {
column: 8,
line: 1,
offset: 7
}
}
},
modifiers: ['prop'],
exp: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'b',
isStatic: false,
constType: ConstantTypes.NOT_CONSTANT,

loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 9, line: 1, column: 10 },
source: 'b'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 9, line: 1, column: 10 },
source: '.a=b'
}
})
})

test('v-bind shorthand with modifier', () => {
const ast = baseParse('<div :a.sync=b />')
const directive = (ast.children[0] as ElementNode).props[0]
Expand Down
140 changes: 129 additions & 11 deletions packages/compiler-core/__tests__/transforms/vBind.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,22 +172,140 @@ describe('compiler: transform v-bind', () => {
const node = parseWithVBind(`<div v-bind:[foo(bar)].camel="id"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as VNodeCall).props as CallExpression
expect(props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: NORMALIZE_PROPS,
arguments: [
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
children: [
`_${helperNameMap[CAMELIZE]}(`,
`(`,
{ content: `_ctx.foo` },
`(`,
{ content: `_ctx.bar` },
`)`,
`) || ""`,
`)`
]
},
value: {
content: `_ctx.id`,
isStatic: false
}
}
]
}
]
})
})

test('.prop modifier', () => {
const node = parseWithVBind(`<div v-bind:fooBar.prop="id"/>`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
children: [
`_${helperNameMap[CAMELIZE]}(`,
`(`,
{ content: `_ctx.foo` },
`(`,
{ content: `_ctx.bar` },
`)`,
`) || ""`,
`)`
]
content: `.fooBar`,
isStatic: true
},
value: {
content: `_ctx.id`,
content: `id`,
isStatic: false
}
})
})

test('.prop modifier w/ dynamic arg', () => {
const node = parseWithVBind(`<div v-bind:[fooBar].prop="id"/>`)
const props = (node.codegenNode as VNodeCall).props as CallExpression
expect(props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: NORMALIZE_PROPS,
arguments: [
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
content: '`.${fooBar || ""}`',
isStatic: false
},
value: {
content: `id`,
isStatic: false
}
}
]
}
]
})
})

test('.prop modifier w/ dynamic arg + prefixIdentifiers', () => {
const node = parseWithVBind(`<div v-bind:[foo(bar)].prop="id"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as VNodeCall).props as CallExpression
expect(props).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: NORMALIZE_PROPS,
arguments: [
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: {
children: [
`'.' + (`,
`(`,
{ content: `_ctx.foo` },
`(`,
{ content: `_ctx.bar` },
`)`,
`) || ""`,
`)`
]
},
value: {
content: `_ctx.id`,
isStatic: false
}
}
]
}
]
})
})

test('.prop modifier (shorthand)', () => {
const node = parseWithVBind(`<div .fooBar="id"/>`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `.fooBar`,
isStatic: true
},
value: {
content: `id`,
isStatic: false
}
})
})

test('.attr modifier', () => {
const node = parseWithVBind(`<div v-bind:foo-bar.attr="id"/>`)
const props = (node.codegenNode as VNodeCall).props as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: {
content: `^foo-bar`,
isStatic: true
},
value: {
content: `id`,
isStatic: false
}
})
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export interface SimpleExpressionNode extends Node {
* the identifiers declared inside the function body.
*/
identifiers?: string[]
isHandlerKey?: boolean
}

export interface InterpolationNode extends Node {
Expand All @@ -243,6 +244,7 @@ export interface CompoundExpressionNode extends Node {
* the identifiers declared inside the function body.
*/
identifiers?: string[]
isHandlerKey?: boolean
}

export interface IfNode extends Node {
Expand Down
12 changes: 9 additions & 3 deletions packages/compiler-core/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,14 +772,19 @@ function parseAttribute(
}
const loc = getSelection(context, start)

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

let isPropShorthand = startsWith(name, '.')
let dirName =
match[1] ||
(startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot')
(isPropShorthand || startsWith(name, ':')
? 'bind'
: startsWith(name, '@')
? 'on'
: 'slot')
let arg: ExpressionNode | undefined

if (match[2]) {
Expand Down Expand Up @@ -835,6 +840,7 @@ function parseAttribute(
}

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

// 2.x compat v-bind:foo.sync -> v-model:foo
if (__COMPAT__ && dirName === 'bind' && arg) {
Expand Down
19 changes: 12 additions & 7 deletions packages/compiler-core/src/transforms/transformElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,21 +700,26 @@ export function buildProps(
// but still need to deal with dynamic key binding
let classKeyIndex = -1
let styleKeyIndex = -1
let dynamicKeyIndex = -1
let hasDynamicKey = false

for (let i = 0; i < propsExpression.properties.length; i++) {
const p = propsExpression.properties[i]
if (p.key.type !== NodeTypes.SIMPLE_EXPRESSION) continue
if (!isStaticExp(p.key)) dynamicKeyIndex = i
if (isStaticExp(p.key) && p.key.content === 'class') classKeyIndex = i
if (isStaticExp(p.key) && p.key.content === 'style') styleKeyIndex = i
const key = propsExpression.properties[i].key
if (isStaticExp(key)) {
if (key.content === 'class') {
classKeyIndex = i
} else if (key.content === 'style') {
styleKeyIndex = i
}
} else if (!key.isHandlerKey) {
hasDynamicKey = true
}
}

const classProp = propsExpression.properties[classKeyIndex]
const styleProp = propsExpression.properties[styleKeyIndex]

// no dynamic key
if (dynamicKeyIndex === -1) {
if (!hasDynamicKey) {
if (classProp && !isStaticExp(classProp.value)) {
classProp.value = createCallExpression(
context.helper(NORMALIZE_CLASS),
Expand Down
29 changes: 27 additions & 2 deletions packages/compiler-core/src/transforms/vBind.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { DirectiveTransform } from '../transform'
import { createObjectProperty, createSimpleExpression, NodeTypes } from '../ast'
import {
createObjectProperty,
createSimpleExpression,
ExpressionNode,
NodeTypes
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { camelize } from '@vue/shared'
import { CAMELIZE } from '../runtimeHelpers'
Expand All @@ -18,7 +23,6 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
arg.content = `${arg.content} || ""`
}

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

if (modifiers.includes('prop')) {
injectPrefix(arg, '.')
}

if (modifiers.includes('attr')) {
injectPrefix(arg, '^')
}

if (
!exp ||
(exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
Expand All @@ -47,3 +59,16 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
props: [createObjectProperty(arg!, exp)]
}
}

const injectPrefix = (arg: ExpressionNode, prefix: string) => {
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) {
arg.content = prefix + arg.content
} else {
arg.content = `\`${prefix}\${${arg.content}}\``
}
} else {
arg.children.unshift(`'${prefix}' + (`)
arg.children.push(`)`)
}
}
2 changes: 2 additions & 0 deletions packages/compiler-core/src/transforms/vOn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,7 @@ export const transformOn: DirectiveTransform = (
ret.props[0].value = context.cache(ret.props[0].value)
}

// mark the key as handler for props normalization check
ret.props.forEach(p => (p.key.isHandlerKey = true))
return ret
}
14 changes: 14 additions & 0 deletions packages/runtime-dom/__tests__/patchProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,20 @@ describe('runtime-dom: props patching', () => {
patchProp(el, 'type', 'text', null)
})

test('force patch as prop', () => {
const el = document.createElement('div') as any
patchProp(el, '.x', null, 1)
expect(el.x).toBe(1)
})

test('force patch as attribute', () => {
const el = document.createElement('div') as any
el.x = 1
patchProp(el, '^x', null, 2)
expect(el.x).toBe(1)
expect(el.getAttribute('x')).toBe('2')
})

test('input with size', () => {
const el = document.createElement('input')
patchProp(el, 'size', null, 100)
Expand Down
Loading

0 comments on commit 1c7d737

Please sign in to comment.