Browse Source

feat: dynamic directive arguments for v-on, v-bind and custom directives (#9373)

dev
Evan You 6 years ago
parent
commit
dbc0582587
  1. 12
      flow/compiler.js
  2. 1
      flow/vnode.js
  3. 27
      src/compiler/codegen/events.js
  4. 32
      src/compiler/codegen/index.js
  5. 46
      src/compiler/helpers.js
  6. 51
      src/compiler/parser/index.js
  7. 35
      src/core/instance/render-helpers/bind-dynamic-keys.js
  8. 3
      src/core/instance/render-helpers/index.js
  9. 1
      src/core/vdom/modules/directives.js
  10. 124
      test/unit/features/directives/bind.spec.js
  11. 117
      test/unit/features/directives/on.spec.js
  12. 22
      test/unit/features/options/directives.spec.js
  13. 20
      test/unit/modules/compiler/codegen.spec.js
  14. 9
      test/unit/modules/compiler/parser.spec.js
  15. 1
      types/vnode.d.ts

12
flow/compiler.js

@ -61,12 +61,20 @@ declare type ModuleOptions = {
declare type ASTModifiers = { [key: string]: boolean };
declare type ASTIfCondition = { exp: ?string; block: ASTElement };
declare type ASTIfConditions = Array<ASTIfCondition>;
declare type ASTAttr = { name: string; value: any; start?: number; end?: number };
declare type ASTAttr = {
name: string;
value: any;
dynamic?: boolean;
start?: number;
end?: number
};
declare type ASTElementHandler = {
value: string;
params?: Array<any>;
modifiers: ?ASTModifiers;
dynamic?: boolean;
start?: number;
end?: number;
};
@ -80,6 +88,7 @@ declare type ASTDirective = {
rawName: string;
value: string;
arg: ?string;
isDynamicArg: boolean;
modifiers: ?ASTModifiers;
start?: number;
end?: number;
@ -109,6 +118,7 @@ declare type ASTElement = {
text?: string;
attrs?: Array<ASTAttr>;
dynamicAttrs?: Array<ASTAttr>;
props?: Array<ASTAttr>;
plain?: boolean;
pre?: true;

1
flow/vnode.js

@ -71,6 +71,7 @@ declare type VNodeDirective = {
value?: any;
oldValue?: any;
arg?: string;
oldArg?: string;
modifiers?: ASTModifiers;
def?: Object;
};

27
src/compiler/codegen/events.js

@ -56,11 +56,23 @@ export function genHandlers (
events: ASTElementHandlers,
isNative: boolean
): string {
let res = isNative ? 'nativeOn:{' : 'on:{'
const prefix = isNative ? 'nativeOn:' : 'on:'
let staticHandlers = ``
let dynamicHandlers = ``
for (const name in events) {
res += `"${name}":${genHandler(name, events[name])},`
const handlerCode = genHandler(events[name])
if (events[name] && events[name].dynamic) {
dynamicHandlers += `${name},${handlerCode},`
} else {
staticHandlers += `"${name}":${handlerCode},`
}
}
staticHandlers = `{${staticHandlers.slice(0, -1)}}`
if (dynamicHandlers) {
return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
} else {
return prefix + staticHandlers
}
return res.slice(0, -1) + '}'
}
// Generate handler code with binding params on Weex
@ -81,16 +93,13 @@ function genWeexHandler (params: Array<any>, handlerCode: string) {
'}'
}
function genHandler (
name: string,
handler: ASTElementHandler | Array<ASTElementHandler>
): string {
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
if (!handler) {
return 'function(){}'
}
if (Array.isArray(handler)) {
return `[${handler.map(handler => genHandler(name, handler)).join(',')}]`
return `[${handler.map(handler => genHandler(handler)).join(',')}]`
}
const isMethodPath = simplePathRE.test(handler.value)
@ -154,7 +163,7 @@ function genHandler (
}
function genKeyFilter (keys: Array<string>): string {
return `if(!('button' in $event)&&${keys.map(genFilterCode).join('&&')})return null;`
return `if(('keyCode' in $event)&&${keys.map(genFilterCode).join('&&')})return null;`
}
function genFilterCode (key: string): string {

32
src/compiler/codegen/index.js

@ -248,11 +248,11 @@ export function genData (el: ASTElement, state: CodegenState): string {
}
// attributes
if (el.attrs) {
data += `attrs:{${genProps(el.attrs)}},`
data += `attrs:${genProps(el.attrs)},`
}
// DOM props
if (el.props) {
data += `domProps:{${genProps(el.props)}},`
data += `domProps:${genProps(el.props)},`
}
// event handlers
if (el.events) {
@ -288,6 +288,12 @@ export function genData (el: ASTElement, state: CodegenState): string {
}
}
data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
@ -319,7 +325,7 @@ function genDirectives (el: ASTElement, state: CodegenState): string | void {
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:"${dir.arg}"` : ''
dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
@ -510,17 +516,25 @@ function genComponent (
}
function genProps (props: Array<ASTAttr>): string {
let res = ''
let staticProps = ``
let dynamicProps = ``
for (let i = 0; i < props.length; i++) {
const prop = props[i]
/* istanbul ignore if */
if (__WEEX__) {
res += `"${prop.name}":${generateValue(prop.value)},`
const value = __WEEX__
? generateValue(prop.value)
: transformSpecialNewlines(prop.value)
if (prop.dynamic) {
dynamicProps += `${prop.name},${value},`
} else {
res += `"${prop.name}":${transformSpecialNewlines(prop.value)},`
staticProps += `"${prop.name}":${value},`
}
}
staticProps = `{${staticProps.slice(0, -1)}}`
if (dynamicProps) {
return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
} else {
return staticProps
}
return res.slice(0, -1)
}
/* istanbul ignore next */

46
src/compiler/helpers.js

@ -20,13 +20,16 @@ export function pluckModuleFunction<F: Function> (
: []
}
export function addProp (el: ASTElement, name: string, value: string, range?: Range) {
(el.props || (el.props = [])).push(rangeSetItem({ name, value }, range))
export function addProp (el: ASTElement, name: string, value: string, range?: Range, dynamic?: boolean) {
(el.props || (el.props = [])).push(rangeSetItem({ name, value, dynamic }, range))
el.plain = false
}
export function addAttr (el: ASTElement, name: string, value: any, range?: Range) {
(el.attrs || (el.attrs = [])).push(rangeSetItem({ name, value }, range))
export function addAttr (el: ASTElement, name: string, value: any, range?: Range, dynamic?: boolean) {
const attrs = dynamic
? (el.dynamicAttrs || (el.dynamicAttrs = []))
: (el.attrs || (el.attrs = []))
attrs.push(rangeSetItem({ name, value, dynamic }, range))
el.plain = false
}
@ -42,13 +45,27 @@ export function addDirective (
rawName: string,
value: string,
arg: ?string,
isDynamicArg: boolean,
modifiers: ?ASTModifiers,
range?: Range
) {
(el.directives || (el.directives = [])).push(rangeSetItem({ name, rawName, value, arg, modifiers }, range))
(el.directives || (el.directives = [])).push(rangeSetItem({
name,
rawName,
value,
arg,
isDynamicArg,
modifiers
}, range))
el.plain = false
}
function prependModifierMarker (symbol: string, name: string, dynamic?: boolean): string {
return dynamic
? `_p(${name},"${symbol}")`
: symbol + name // mark the event as captured
}
export function addHandler (
el: ASTElement,
name: string,
@ -56,7 +73,8 @@ export function addHandler (
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
@ -75,11 +93,17 @@ export function addHandler (
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
if (name === 'click') {
if (modifiers.right) {
if (dynamic) {
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
name = 'contextmenu'
delete modifiers.right
}
} else if (modifiers.middle) {
if (dynamic) {
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
name = 'mouseup'
}
}
@ -87,16 +111,16 @@ export function addHandler (
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = '!' + name // mark the event as captured
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) {
delete modifiers.once
name = '~' + name // mark the event as once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
name = '&' + name // mark the event as passive
name = prependModifierMarker('&', name, dynamic)
}
let events
@ -107,7 +131,7 @@ export function addHandler (
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim() }, range)
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}

51
src/compiler/parser/index.js

@ -26,7 +26,7 @@ export const dirRE = /^v-|^@|^:|^\./
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const dynamicKeyRE = /^\[.*\]$/
const dynamicArgRE = /^\[.*\]$/
const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/
@ -666,7 +666,7 @@ function getSlotName (binding) {
)
}
}
return dynamicKeyRE.test(name)
return dynamicArgRE.test(name)
// dynamic [name]
? { name: name.slice(1, -1), dynamic: true }
// static name
@ -700,7 +700,7 @@ function processComponent (el) {
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp, syncGen
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
@ -719,7 +719,10 @@ function processAttrs (el) {
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '')
value = parseFilters(value)
isProp = false
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
if (
process.env.NODE_ENV !== 'production' &&
value.trim().length === 0
@ -729,16 +732,16 @@ function processAttrs (el) {
)
}
if (modifiers) {
if (modifiers.prop) {
isProp = true
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
if (modifiers.camel) {
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`)
if (!isDynamic) {
addHandler(
el,
`update:${camelize(name)}`,
@ -759,27 +762,49 @@ function processAttrs (el) {
list[i]
)
}
} else {
// handler w/ dynamic event name
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true // dynamic
)
}
}
}
if (isProp || (
if ((modifiers && modifiers.prop) || (
!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
)) {
addProp(el, name, value, list[i])
addProp(el, name, value, list[i], isDynamic)
} else {
addAttr(el, name, value, list[i])
addAttr(el, name, value, list[i], isDynamic)
}
} else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '')
addHandler(el, name, value, modifiers, false, warn, list[i])
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
} else { // normal directives
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
const arg = argMatch && argMatch[1]
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(el, name, rawName, value, arg, modifiers, list[i])
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value)
}

35
src/core/instance/render-helpers/bind-dynamic-keys.js

@ -0,0 +1,35 @@
/* @flow */
// helper to process dynamic keys for dynamic arguments in v-bind and v-on.
// For example, the following template:
//
// <div id="foo" :[key]="value">
//
// compiles to the following:
//
// _c('div', { attrs: bindDynamicKeys({ "id": "app" }, [key, value]) })
import { warn } from 'core/util/debug'
export function bindDynamicKeys (baseObj: Object, values: Array<any>): Object {
for (let i = 0; i < values.length; i += 2) {
const key = values[i]
if (typeof key === 'string' && key) {
baseObj[values[i]] = values[i + 1]
} else if (process.env.NODE_ENV !== 'production' && key !== '' && key !== null) {
// null is a speical value for explicitly removing a binding
warn(
`Invalid value for dynamic directive argument (expected string or null): ${key}`,
this
)
}
}
return baseObj
}
// helper to dynamically append modifier runtime markers to event names.
// ensure only append when value is already string, otherwise it will be cast
// to string and cause the type check to miss.
export function prependModifier (value: any, symbol: string): any {
return typeof value === 'string' ? symbol + value : value
}

3
src/core/instance/render-helpers/index.js

@ -10,6 +10,7 @@ import { bindObjectProps } from './bind-object-props'
import { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-slots'
import { bindDynamicKeys, prependModifier } from './bind-dynamic-keys'
export function installRenderHelpers (target: any) {
target._o = markOnce
@ -27,4 +28,6 @@ export function installRenderHelpers (target: any) {
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}

1
src/core/vdom/modules/directives.js

@ -40,6 +40,7 @@ function _update (oldVnode, vnode) {
} else {
// existing directive, update
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir)

124
test/unit/features/directives/bind.spec.js

@ -473,4 +473,128 @@ describe('Directive v-bind', () => {
expect(vm.$el.innerHTML).toBe('<div>comp</div>')
})
})
describe('dynamic arguments', () => {
it('basic', done => {
const vm = new Vue({
template: `<div v-bind:[key]="value"></div>`,
data: {
key: 'id',
value: 'hello'
}
}).$mount()
expect(vm.$el.id).toBe('hello')
vm.key = 'class'
waitForUpdate(() => {
expect(vm.$el.id).toBe('')
expect(vm.$el.className).toBe('hello')
// explicit null value
vm.key = null
}).then(() => {
expect(vm.$el.className).toBe('')
expect(vm.$el.id).toBe('')
vm.key = undefined
}).then(() => {
expect(`Invalid value for dynamic directive argument`).toHaveBeenWarned()
}).then(done)
})
it('shorthand', done => {
const vm = new Vue({
template: `<div :[key]="value"></div>`,
data: {
key: 'id',
value: 'hello'
}
}).$mount()
expect(vm.$el.id).toBe('hello')
vm.key = 'class'
waitForUpdate(() => {
expect(vm.$el.className).toBe('hello')
}).then(done)
})
it('with .prop modifier', done => {
const vm = new Vue({
template: `<div :[key].prop="value"></div>`,
data: {
key: 'id',
value: 'hello'
}
}).$mount()
expect(vm.$el.id).toBe('hello')
vm.key = 'textContent'
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('hello')
}).then(done)
})
it('.prop shorthand', done => {
const vm = new Vue({
template: `<div .[key]="value"></div>`,
data: {
key: 'id',
value: 'hello'
}
}).$mount()
expect(vm.$el.id).toBe('hello')
vm.key = 'textContent'
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('hello')
}).then(done)
})
it('handle class and style', () => {
const vm = new Vue({
template: `<div :[key]="value" :[key2]="value2"></div>`,
data: {
key: 'class',
value: ['hello', 'world'],
key2: 'style',
value2: {
color: 'red'
}
}
}).$mount()
expect(vm.$el.className).toBe('hello world')
expect(vm.$el.style.color).toBe('red')
})
it('handle shouldUseProp', done => {
const vm = new Vue({
template: `<input :[key]="value">`,
data: {
key: 'value',
value: 'foo'
}
}).$mount()
expect(vm.$el.value).toBe('foo')
vm.value = 'bar'
waitForUpdate(() => {
expect(vm.$el.value).toBe('bar')
}).then(done)
})
it('with .sync modifier', done => {
const vm = new Vue({
template: `<foo ref="child" :[key].sync="value"/>`,
data: {
key: 'foo',
value: 'bar'
},
components: {
foo: {
props: ['foo'],
template: `<div>{{ foo }}</div>`
}
}
}).$mount()
expect(vm.$el.textContent).toBe('bar')
vm.$refs.child.$emit('update:foo', 'baz')
waitForUpdate(() => {
expect(vm.value).toBe('baz')
expect(vm.$el.textContent).toBe('baz')
}).then(done)
})
})
})

117
test/unit/features/directives/on.spec.js

@ -947,4 +947,121 @@ describe('Directive v-on', () => {
}).$mount()
expect(value).toBe(1)
})
describe('dynamic arguments', () => {
it('basic', done => {
const spy = jasmine.createSpy()
const vm = new Vue({
template: `<div v-on:[key]="spy"></div>`,
data: {
key: 'click'
},
methods: {
spy
}
}).$mount()
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(1)
vm.key = 'mouseup'
waitForUpdate(() => {
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(1)
triggerEvent(vm.$el, 'mouseup')
expect(spy.calls.count()).toBe(2)
// explicit null value
vm.key = null
}).then(() => {
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(2)
triggerEvent(vm.$el, 'mouseup')
expect(spy.calls.count()).toBe(2)
}).then(done)
})
it('shorthand', done => {
const spy = jasmine.createSpy()
const vm = new Vue({
template: `<div @[key]="spy"></div>`,
data: {
key: 'click'
},
methods: {
spy
}
}).$mount()
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(1)
vm.key = 'mouseup'
waitForUpdate(() => {
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(1)
triggerEvent(vm.$el, 'mouseup')
expect(spy.calls.count()).toBe(2)
}).then(done)
})
it('with .middle modifier', () => {
const spy = jasmine.createSpy()
const vm = new Vue({
template: `<div @[key].middle="spy"></div>`,
data: {
key: 'click'
},
methods: {
spy
}
}).$mount()
triggerEvent(vm.$el, 'mouseup', e => { e.button = 0 })
expect(spy).not.toHaveBeenCalled()
triggerEvent(vm.$el, 'mouseup', e => { e.button = 1 })
expect(spy).toHaveBeenCalled()
})
it('with .right modifier', () => {
const spy = jasmine.createSpy()
const vm = new Vue({
template: `<div @[key].right="spy"></div>`,
data: {
key: 'click'
},
methods: {
spy
}
}).$mount()
triggerEvent(vm.$el, 'contextmenu')
expect(spy).toHaveBeenCalled()
})
it('with .capture modifier', () => {
const callOrder = []
const vm = new Vue({
template: `
<div @[key].capture="foo">
<div @[key]="bar"></div>
</div>
`,
data: {
key: 'click'
},
methods: {
foo () { callOrder.push(1) },
bar () { callOrder.push(2) }
}
}).$mount()
triggerEvent(vm.$el.firstChild, 'click')
expect(callOrder.toString()).toBe('1,2')
})
it('with .once modifier', () => {
const vm = new Vue({
template: `<div @[key].once="foo"></div>`,
data: { key: 'click' },
methods: { foo: spy }
}).$mount()
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(1)
triggerEvent(vm.$el, 'click')
expect(spy.calls.count()).toBe(1) // should no longer trigger
})
})
})

22
test/unit/features/options/directives.spec.js

@ -265,4 +265,26 @@ describe('Options directives', () => {
expect(dir.unbind.calls.argsFor(0)[0]).toBe(oldEl)
}).then(done)
})
it('dynamic arguments', done => {
const vm = new Vue({
template: `<div v-my:[key]="1"/>`,
data: {
key: 'foo'
},
directives: {
my: {
bind(el, binding) {
expect(binding.arg).toBe('foo')
},
update(el, binding) {
expect(binding.arg).toBe('bar')
expect(binding.oldArg).toBe('foo')
done()
}
}
}
}).$mount()
vm.key = 'bar'
})
})

20
test/unit/modules/compiler/codegen.spec.js

@ -356,37 +356,37 @@ describe('codegen', () => {
it('generate events with keycode', () => {
assertCodegen(
'<input @input.enter="onInput">',
`with(this){return _c('input',{on:{"input":function($event){if(!('button' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;return onInput($event)}}})}`
`with(this){return _c('input',{on:{"input":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;return onInput($event)}}})}`
)
// multiple keycodes (delete)
assertCodegen(
'<input @input.delete="onInput">',
`with(this){return _c('input',{on:{"input":function($event){if(!('button' in $event)&&_k($event.keyCode,"delete",[8,46],$event.key,["Backspace","Delete","Del"]))return null;return onInput($event)}}})}`
`with(this){return _c('input',{on:{"input":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"delete",[8,46],$event.key,["Backspace","Delete","Del"]))return null;return onInput($event)}}})}`
)
// multiple keycodes (esc)
assertCodegen(
'<input @input.esc="onInput">',
`with(this){return _c('input',{on:{"input":function($event){if(!('button' in $event)&&_k($event.keyCode,"esc",27,$event.key,["Esc","Escape"]))return null;return onInput($event)}}})}`
`with(this){return _c('input',{on:{"input":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"esc",27,$event.key,["Esc","Escape"]))return null;return onInput($event)}}})}`
)
// multiple keycodes (space)
assertCodegen(
'<input @input.space="onInput">',
`with(this){return _c('input',{on:{"input":function($event){if(!('button' in $event)&&_k($event.keyCode,"space",32,$event.key,[" ","Spacebar"]))return null;return onInput($event)}}})}`
`with(this){return _c('input',{on:{"input":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"space",32,$event.key,[" ","Spacebar"]))return null;return onInput($event)}}})}`
)
// multiple keycodes (chained)
assertCodegen(
'<input @keydown.enter.delete="onInput">',
`with(this){return _c('input',{on:{"keydown":function($event){if(!('button' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter")&&_k($event.keyCode,"delete",[8,46],$event.key,["Backspace","Delete","Del"]))return null;return onInput($event)}}})}`
`with(this){return _c('input',{on:{"keydown":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter")&&_k($event.keyCode,"delete",[8,46],$event.key,["Backspace","Delete","Del"]))return null;return onInput($event)}}})}`
)
// number keycode
assertCodegen(
'<input @input.13="onInput">',
`with(this){return _c('input',{on:{"input":function($event){if(!('button' in $event)&&$event.keyCode!==13)return null;return onInput($event)}}})}`
`with(this){return _c('input',{on:{"input":function($event){if(('keyCode' in $event)&&$event.keyCode!==13)return null;return onInput($event)}}})}`
)
// custom keycode
assertCodegen(
'<input @input.custom="onInput">',
`with(this){return _c('input',{on:{"input":function($event){if(!('button' in $event)&&_k($event.keyCode,"custom",undefined,$event.key,undefined))return null;return onInput($event)}}})}`
`with(this){return _c('input',{on:{"input":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"custom",undefined,$event.key,undefined))return null;return onInput($event)}}})}`
)
})
@ -409,12 +409,12 @@ describe('codegen', () => {
it('generate events with generic modifiers and keycode correct order', () => {
assertCodegen(
'<input @keydown.enter.prevent="onInput">',
`with(this){return _c('input',{on:{"keydown":function($event){if(!('button' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;$event.preventDefault();return onInput($event)}}})}`
`with(this){return _c('input',{on:{"keydown":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;$event.preventDefault();return onInput($event)}}})}`
)
assertCodegen(
'<input @keydown.enter.stop="onInput">',
`with(this){return _c('input',{on:{"keydown":function($event){if(!('button' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;$event.stopPropagation();return onInput($event)}}})}`
`with(this){return _c('input',{on:{"keydown":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;$event.stopPropagation();return onInput($event)}}})}`
)
})
@ -521,7 +521,7 @@ describe('codegen', () => {
// with modifiers
assertCodegen(
`<input @keyup.enter="e=>current++">`,
`with(this){return _c('input',{on:{"keyup":function($event){if(!('button' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;return (e=>current++)($event)}}})}`
`with(this){return _c('input',{on:{"keyup":function($event){if(('keyCode' in $event)&&_k($event.keyCode,"enter",13,$event.key,"Enter"))return null;return (e=>current++)($event)}}})}`
)
})

9
test/unit/modules/compiler/parser.spec.js

@ -537,12 +537,17 @@ describe('parser', () => {
it('v-bind.prop shorthand syntax', () => {
const ast = parse('<div .id="foo"></div>', baseOptions)
expect(ast.props).toEqual([{ name: 'id', value: 'foo'}])
expect(ast.props).toEqual([{ name: 'id', value: 'foo', dynamic: false }])
})
it('v-bind.prop shorthand syntax w/ modifiers', () => {
const ast = parse('<div .id.mod="foo"></div>', baseOptions)
expect(ast.props).toEqual([{ name: 'id', value: 'foo'}])
expect(ast.props).toEqual([{ name: 'id', value: 'foo', dynamic: false }])
})
it('v-bind dynamic argument', () => {
const ast = parse('<div .[id]="foo"></div>', baseOptions)
expect(ast.props).toEqual([{ name: 'id', value: 'foo', dynamic: true }])
})
// #6887

1
types/vnode.d.ts

@ -67,5 +67,6 @@ export interface VNodeDirective {
oldValue?: any;
expression?: any;
arg?: string;
oldArg?: string;
modifiers?: { [key: string]: boolean };
}

Loading…
Cancel
Save