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 }) ```
This commit is contained in:
parent
00f0b3c465
commit
1c7d737cc8
@ -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]
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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 {
|
||||
@ -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 {
|
||||
|
@ -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]) {
|
||||
@ -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) {
|
||||
|
@ -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),
|
||||
|
@ -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'
|
||||
@ -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) {
|
||||
@ -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())
|
||||
@ -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(`)`)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -24,43 +24,42 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
|
||||
parentSuspense,
|
||||
unmountChildren
|
||||
) => {
|
||||
switch (key) {
|
||||
// special
|
||||
case 'class':
|
||||
patchClass(el, nextValue, isSVG)
|
||||
break
|
||||
case 'style':
|
||||
patchStyle(el, prevValue, nextValue)
|
||||
break
|
||||
default:
|
||||
if (isOn(key)) {
|
||||
// ignore v-model listeners
|
||||
if (!isModelListener(key)) {
|
||||
patchEvent(el, key, prevValue, nextValue, parentComponent)
|
||||
}
|
||||
} else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
|
||||
patchDOMProp(
|
||||
el,
|
||||
key,
|
||||
nextValue,
|
||||
prevChildren,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
unmountChildren
|
||||
)
|
||||
} else {
|
||||
// special case for <input v-model type="checkbox"> with
|
||||
// :true-value & :false-value
|
||||
// store value as dom properties since non-string values will be
|
||||
// stringified.
|
||||
if (key === 'true-value') {
|
||||
;(el as any)._trueValue = nextValue
|
||||
} else if (key === 'false-value') {
|
||||
;(el as any)._falseValue = nextValue
|
||||
}
|
||||
patchAttr(el, key, nextValue, isSVG, parentComponent)
|
||||
}
|
||||
break
|
||||
if (key === 'class') {
|
||||
patchClass(el, nextValue, isSVG)
|
||||
} else if (key === 'style') {
|
||||
patchStyle(el, prevValue, nextValue)
|
||||
} else if (isOn(key)) {
|
||||
// ignore v-model listeners
|
||||
if (!isModelListener(key)) {
|
||||
patchEvent(el, key, prevValue, nextValue, parentComponent)
|
||||
}
|
||||
} else if (
|
||||
key[0] === '.'
|
||||
? ((key = key.slice(1)), true)
|
||||
: key[0] === '^'
|
||||
? ((key = key.slice(1)), false)
|
||||
: shouldSetAsProp(el, key, nextValue, isSVG)
|
||||
) {
|
||||
patchDOMProp(
|
||||
el,
|
||||
key,
|
||||
nextValue,
|
||||
prevChildren,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
unmountChildren
|
||||
)
|
||||
} else {
|
||||
// special case for <input v-model type="checkbox"> with
|
||||
// :true-value & :false-value
|
||||
// store value as dom properties since non-string values will be
|
||||
// stringified.
|
||||
if (key === 'true-value') {
|
||||
;(el as any)._trueValue = nextValue
|
||||
} else if (key === 'false-value') {
|
||||
;(el as any)._falseValue = nextValue
|
||||
}
|
||||
patchAttr(el, key, nextValue, isSVG, parentComponent)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user