})
})
+ 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]
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
}
})
* the identifiers declared inside the function body.
*/
identifiers?: string[]
+ isHandlerKey?: boolean
}
export interface InterpolationNode extends Node {
* the identifiers declared inside the function body.
*/
identifiers?: string[]
+ isHandlerKey?: boolean
}
export interface IfNode extends Node {
}
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]) {
}
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) {
// 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),
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'
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) {
}
}
+ if (modifiers.includes('prop')) {
+ injectPrefix(arg, '.')
+ }
+
+ if (modifiers.includes('attr')) {
+ injectPrefix(arg, '^')
+ }
+
if (
!exp ||
(exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
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(`)`)
+ }
+}
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
}
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)
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)
}
}