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

1
flow/vnode.js

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

27
src/compiler/codegen/events.js

@ -56,11 +56,23 @@ export function genHandlers (
events: ASTElementHandlers, events: ASTElementHandlers,
isNative: boolean isNative: boolean
): string { ): string {
let res = isNative ? 'nativeOn:{' : 'on:{' const prefix = isNative ? 'nativeOn:' : 'on:'
let staticHandlers = ``
let dynamicHandlers = ``
for (const name in events) { 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 // Generate handler code with binding params on Weex
@ -81,16 +93,13 @@ function genWeexHandler (params: Array<any>, handlerCode: string) {
'}' '}'
} }
function genHandler ( function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
name: string,
handler: ASTElementHandler | Array<ASTElementHandler>
): string {
if (!handler) { if (!handler) {
return 'function(){}' return 'function(){}'
} }
if (Array.isArray(handler)) { 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) const isMethodPath = simplePathRE.test(handler.value)
@ -154,7 +163,7 @@ function genHandler (
} }
function genKeyFilter (keys: Array<string>): string { 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 { function genFilterCode (key: string): string {

32
src/compiler/codegen/index.js

@ -248,11 +248,11 @@ export function genData (el: ASTElement, state: CodegenState): string {
} }
// attributes // attributes
if (el.attrs) { if (el.attrs) {
data += `attrs:{${genProps(el.attrs)}},` data += `attrs:${genProps(el.attrs)},`
} }
// DOM props // DOM props
if (el.props) { if (el.props) {
data += `domProps:{${genProps(el.props)}},` data += `domProps:${genProps(el.props)},`
} }
// event handlers // event handlers
if (el.events) { if (el.events) {
@ -288,6 +288,12 @@ export function genData (el: ASTElement, state: CodegenState): string {
} }
} }
data = data.replace(/,$/, '') + '}' 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 // v-bind data wrap
if (el.wrapData) { if (el.wrapData) {
data = el.wrapData(data) data = el.wrapData(data)
@ -319,7 +325,7 @@ function genDirectives (el: ASTElement, state: CodegenState): string | void {
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${ res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : '' 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)}` : '' dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},` }},`
@ -510,17 +516,25 @@ function genComponent (
} }
function genProps (props: Array<ASTAttr>): string { function genProps (props: Array<ASTAttr>): string {
let res = '' let staticProps = ``
let dynamicProps = ``
for (let i = 0; i < props.length; i++) { for (let i = 0; i < props.length; i++) {
const prop = props[i] const prop = props[i]
/* istanbul ignore if */ const value = __WEEX__
if (__WEEX__) { ? generateValue(prop.value)
res += `"${prop.name}":${generateValue(prop.value)},` : transformSpecialNewlines(prop.value)
if (prop.dynamic) {
dynamicProps += `${prop.name},${value},`
} else { } 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 */ /* 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) { export function addProp (el: ASTElement, name: string, value: string, range?: Range, dynamic?: boolean) {
(el.props || (el.props = [])).push(rangeSetItem({ name, value }, range)) (el.props || (el.props = [])).push(rangeSetItem({ name, value, dynamic }, range))
el.plain = false el.plain = false
} }
export function addAttr (el: ASTElement, name: string, value: any, range?: Range) { export function addAttr (el: ASTElement, name: string, value: any, range?: Range, dynamic?: boolean) {
(el.attrs || (el.attrs = [])).push(rangeSetItem({ name, value }, range)) const attrs = dynamic
? (el.dynamicAttrs || (el.dynamicAttrs = []))
: (el.attrs || (el.attrs = []))
attrs.push(rangeSetItem({ name, value, dynamic }, range))
el.plain = false el.plain = false
} }
@ -42,13 +45,27 @@ export function addDirective (
rawName: string, rawName: string,
value: string, value: string,
arg: ?string, arg: ?string,
isDynamicArg: boolean,
modifiers: ?ASTModifiers, modifiers: ?ASTModifiers,
range?: Range 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 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 ( export function addHandler (
el: ASTElement, el: ASTElement,
name: string, name: string,
@ -56,7 +73,8 @@ export function addHandler (
modifiers: ?ASTModifiers, modifiers: ?ASTModifiers,
important?: boolean, important?: boolean,
warn?: ?Function, warn?: ?Function,
range?: Range range?: Range,
dynamic?: boolean
) { ) {
modifiers = modifiers || emptyObject modifiers = modifiers || emptyObject
// warn prevent and passive modifier // warn prevent and passive modifier
@ -75,11 +93,17 @@ export function addHandler (
// normalize click.right and click.middle since they don't actually fire // normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are // this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks. // the only target envs that have right/middle clicks.
if (name === 'click') {
if (modifiers.right) { if (modifiers.right) {
if (dynamic) {
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
name = 'contextmenu' name = 'contextmenu'
delete modifiers.right delete modifiers.right
}
} else if (modifiers.middle) { } else if (modifiers.middle) {
if (dynamic) {
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
name = 'mouseup' name = 'mouseup'
} }
} }
@ -87,16 +111,16 @@ export function addHandler (
// check capture modifier // check capture modifier
if (modifiers.capture) { if (modifiers.capture) {
delete modifiers.capture delete modifiers.capture
name = '!' + name // mark the event as captured name = prependModifierMarker('!', name, dynamic)
} }
if (modifiers.once) { if (modifiers.once) {
delete modifiers.once delete modifiers.once
name = '~' + name // mark the event as once name = prependModifierMarker('~', name, dynamic)
} }
/* istanbul ignore if */ /* istanbul ignore if */
if (modifiers.passive) { if (modifiers.passive) {
delete modifiers.passive delete modifiers.passive
name = '&' + name // mark the event as passive name = prependModifierMarker('&', name, dynamic)
} }
let events let events
@ -107,7 +131,7 @@ export function addHandler (
events = el.events || (el.events = {}) 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) { if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers 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 forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/ export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g const stripParensRE = /^\(|\)$/g
const dynamicKeyRE = /^\[.*\]$/ const dynamicArgRE = /^\[.*\]$/
const argRE = /:(.*)$/ const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/ export const bindRE = /^:|^\.|^v-bind:/
@ -666,7 +666,7 @@ function getSlotName (binding) {
) )
} }
} }
return dynamicKeyRE.test(name) return dynamicArgRE.test(name)
// dynamic [name] // dynamic [name]
? { name: name.slice(1, -1), dynamic: true } ? { name: name.slice(1, -1), dynamic: true }
// static name // static name
@ -700,7 +700,7 @@ function processComponent (el) {
function processAttrs (el) { function processAttrs (el) {
const list = el.attrsList 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++) { for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name name = rawName = list[i].name
value = list[i].value value = list[i].value
@ -719,7 +719,10 @@ function processAttrs (el) {
if (bindRE.test(name)) { // v-bind if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '') name = name.replace(bindRE, '')
value = parseFilters(value) value = parseFilters(value)
isProp = false isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
if ( if (
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'production' &&
value.trim().length === 0 value.trim().length === 0
@ -729,16 +732,16 @@ function processAttrs (el) {
) )
} }
if (modifiers) { if (modifiers) {
if (modifiers.prop) { if (modifiers.prop && !isDynamic) {
isProp = true
name = camelize(name) name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML' if (name === 'innerHtml') name = 'innerHTML'
} }
if (modifiers.camel) { if (modifiers.camel && !isDynamic) {
name = camelize(name) name = camelize(name)
} }
if (modifiers.sync) { if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`) syncGen = genAssignmentCode(value, `$event`)
if (!isDynamic) {
addHandler( addHandler(
el, el,
`update:${camelize(name)}`, `update:${camelize(name)}`,
@ -759,27 +762,49 @@ function processAttrs (el) {
list[i] 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) !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
)) { )) {
addProp(el, name, value, list[i]) addProp(el, name, value, list[i], isDynamic)
} else { } else {
addAttr(el, name, value, list[i]) addAttr(el, name, value, list[i], isDynamic)
} }
} else if (onRE.test(name)) { // v-on } else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '') 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 } else { // normal directives
name = name.replace(dirRE, '') name = name.replace(dirRE, '')
// parse arg // parse arg
const argMatch = name.match(argRE) const argMatch = name.match(argRE)
const arg = argMatch && argMatch[1] let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) { if (arg) {
name = name.slice(0, -(arg.length + 1)) 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') { if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value) 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 { renderStatic, markOnce } from './render-static'
import { bindObjectListeners } from './bind-object-listeners' import { bindObjectListeners } from './bind-object-listeners'
import { resolveScopedSlots } from './resolve-slots' import { resolveScopedSlots } from './resolve-slots'
import { bindDynamicKeys, prependModifier } from './bind-dynamic-keys'
export function installRenderHelpers (target: any) { export function installRenderHelpers (target: any) {
target._o = markOnce target._o = markOnce
@ -27,4 +28,6 @@ export function installRenderHelpers (target: any) {
target._e = createEmptyVNode target._e = createEmptyVNode
target._u = resolveScopedSlots target._u = resolveScopedSlots
target._g = bindObjectListeners 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 { } else {
// existing directive, update // existing directive, update
dir.oldValue = oldDir.value dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode) callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) { if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir) 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>') 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() }).$mount()
expect(value).toBe(1) 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) expect(dir.unbind.calls.argsFor(0)[0]).toBe(oldEl)
}).then(done) }).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', () => { it('generate events with keycode', () => {
assertCodegen( assertCodegen(
'<input @input.enter="onInput">', '<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) // multiple keycodes (delete)
assertCodegen( assertCodegen(
'<input @input.delete="onInput">', '<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) // multiple keycodes (esc)
assertCodegen( assertCodegen(
'<input @input.esc="onInput">', '<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) // multiple keycodes (space)
assertCodegen( assertCodegen(
'<input @input.space="onInput">', '<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) // multiple keycodes (chained)
assertCodegen( assertCodegen(
'<input @keydown.enter.delete="onInput">', '<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 // number keycode
assertCodegen( assertCodegen(
'<input @input.13="onInput">', '<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 // custom keycode
assertCodegen( assertCodegen(
'<input @input.custom="onInput">', '<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', () => { it('generate events with generic modifiers and keycode correct order', () => {
assertCodegen( assertCodegen(
'<input @keydown.enter.prevent="onInput">', '<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( assertCodegen(
'<input @keydown.enter.stop="onInput">', '<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 // with modifiers
assertCodegen( assertCodegen(
`<input @keyup.enter="e=>current++">`, `<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', () => { it('v-bind.prop shorthand syntax', () => {
const ast = parse('<div .id="foo"></div>', baseOptions) 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', () => { it('v-bind.prop shorthand syntax w/ modifiers', () => {
const ast = parse('<div .id.mod="foo"></div>', baseOptions) 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 // #6887

1
types/vnode.d.ts

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

Loading…
Cancel
Save