Sfoglia il codice sorgente

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

Evan You 7 anni fa
parent
commit
dbc0582587

+ 11 - 1
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 - 0
flow/vnode.js

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

+ 18 - 9
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 {

+ 23 - 9
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},`
     }
   }
-  return res.slice(0, -1)
+  staticProps = `{${staticProps.slice(0, -1)}}`
+  if (dynamicProps) {
+    return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
+  } else {
+    return staticProps
+  }
 }
 
 /* istanbul ignore next */

+ 37 - 13
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 (modifiers.right) {
+    if (dynamic) {
+      name = `(${name})==='click'?'contextmenu':(${name})`
+    } else if (name === 'click') {
       name = 'contextmenu'
       delete modifiers.right
-    } else if (modifiers.middle) {
+    }
+  } 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
   }

+ 49 - 24
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,57 +732,79 @@ 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`)
-            addHandler(
-              el,
-              `update:${camelize(name)}`,
-              syncGen,
-              null,
-              false,
-              warn,
-              list[i]
-            )
-            if (hyphenate(name) !== camelize(name)) {
+            if (!isDynamic) {
               addHandler(
                 el,
-                `update:${hyphenate(name)}`,
+                `update:${camelize(name)}`,
                 syncGen,
                 null,
                 false,
                 warn,
                 list[i]
               )
+              if (hyphenate(name) !== camelize(name)) {
+                addHandler(
+                  el,
+                  `update:${hyphenate(name)}`,
+                  syncGen,
+                  null,
+                  false,
+                  warn,
+                  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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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'
+  })
 })

+ 10 - 10
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)}}})}`
     )
   })
 

+ 7 - 2
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 - 0
types/vnode.d.ts

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