From 11614d63b7862b68b11cc45c0891437c62a832d7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 11 Jul 2017 16:36:04 +0800 Subject: [PATCH] feat(v-on): support v-on object syntax with no arguments Note this does not support modifiers and is meant to be used for handling events proxying in higher-order-components. --- flow/compiler.js | 1 + flow/component.js | 2 + flow/vnode.js | 1 + src/compiler/codegen/index.js | 6 +- src/compiler/directives/index.js | 2 + src/compiler/directives/on.js | 10 ++ .../render-helpers/bind-object-listeners.js | 22 +++ src/core/instance/render.js | 2 + src/core/vdom/create-component.js | 5 +- src/platforms/web/runtime/modules/events.js | 9 +- src/platforms/weex/runtime/modules/events.js | 9 +- test/unit/features/directives/on.spec.js | 132 ++++++++++++++++-- 12 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 src/compiler/directives/on.js create mode 100644 src/core/instance/render-helpers/bind-object-listeners.js diff --git a/flow/compiler.js b/flow/compiler.js index 85ce3090..f364351b 100644 --- a/flow/compiler.js +++ b/flow/compiler.js @@ -133,6 +133,7 @@ declare type ASTElement = { once?: true; onceProcessed?: boolean; wrapData?: (code: string) => string; + wrapListeners?: (code: string) => string; // 2.4 ssr optimization ssrOptimizability?: number; diff --git a/flow/component.js b/flow/component.js index bf2dcb25..3f8fd990 100644 --- a/flow/component.js +++ b/flow/component.js @@ -121,6 +121,8 @@ declare interface Component { _t: (name: string, fallback: ?Array, props: ?Object) => ?Array; // apply v-bind object _b: (data: any, tag: string, value: any, asProp: boolean, isSync?: boolean) => VNodeData; + // apply v-on object + _g: (data: any, value: any) => VNodeData; // check custom keyCode _k: (eventKeyCode: number, key: string, builtInAlias: number | Array | void) => boolean; // resolve scoped slots diff --git a/flow/vnode.js b/flow/vnode.js index 8a3622b4..46fe3dea 100644 --- a/flow/vnode.js +++ b/flow/vnode.js @@ -27,6 +27,7 @@ declare type VNodeWithData = { context: Component; key: string | number | void; parent?: VNodeWithData; + componentOptions?: VNodeComponentOptions; componentInstance?: Component; isRootInsert: boolean; }; diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js index 8d11779d..e8adf146 100644 --- a/src/compiler/codegen/index.js +++ b/src/compiler/codegen/index.js @@ -1,9 +1,9 @@ /* @flow */ import { genHandlers } from './events' -import { baseWarn, pluckModuleFunction } from '../helpers' import baseDirectives from '../directives/index' import { camelize, no, extend } from 'shared/util' +import { baseWarn, pluckModuleFunction } from '../helpers' type TransformFunction = (el: ASTElement, code: string) => string; type DataGenFunction = (el: ASTElement) => string; @@ -268,6 +268,10 @@ export function genData (el: ASTElement, state: CodegenState): string { if (el.wrapData) { data = el.wrapData(data) } + // v-on data wrap + if (el.wrapListeners) { + data = el.wrapListeners(data) + } return data } diff --git a/src/compiler/directives/index.js b/src/compiler/directives/index.js index df295f3d..8f4342c0 100644 --- a/src/compiler/directives/index.js +++ b/src/compiler/directives/index.js @@ -1,9 +1,11 @@ /* @flow */ +import on from './on' import bind from './bind' import { noop } from 'shared/util' export default { + on, bind, cloak: noop } diff --git a/src/compiler/directives/on.js b/src/compiler/directives/on.js new file mode 100644 index 00000000..893a678e --- /dev/null +++ b/src/compiler/directives/on.js @@ -0,0 +1,10 @@ +/* @flow */ + +import { warn } from 'core/util/index' + +export default function on (el: ASTElement, dir: ASTDirective) { + if (process.env.NODE_ENV !== 'production' && dir.modifiers) { + warn(`v-on without argument does not support modifiers.`) + } + el.wrapListeners = (code: string) => `_g(${code},${dir.value})` +} diff --git a/src/core/instance/render-helpers/bind-object-listeners.js b/src/core/instance/render-helpers/bind-object-listeners.js new file mode 100644 index 00000000..7d1c6c20 --- /dev/null +++ b/src/core/instance/render-helpers/bind-object-listeners.js @@ -0,0 +1,22 @@ +/* @flow */ + +import { warn, extend, isPlainObject } from 'core/util/index' + +export function bindObjectListeners (data: any, value: any): VNodeData { + if (value) { + if (!isPlainObject(value)) { + process.env.NODE_ENV !== 'production' && warn( + 'v-on without argument expects an Object value', + this + ) + } else { + const on = data.on = data.on ? extend({}, data.on) : {} + for (const key in value) { + const existing = on[key] + const ours = value[key] + on[key] = existing ? [ours].concat(existing) : ours + } + } + } + return data +} diff --git a/src/core/instance/render.js b/src/core/instance/render.js index a29bc7ca..36f6e39e 100644 --- a/src/core/instance/render.js +++ b/src/core/instance/render.js @@ -24,6 +24,7 @@ import { resolveFilter } from './render-helpers/resolve-filter' import { checkKeyCodes } from './render-helpers/check-keycodes' import { bindObjectProps } from './render-helpers/bind-object-props' import { renderStatic, markOnce } from './render-helpers/render-static' +import { bindObjectListeners } from './render-helpers/bind-object-listeners' import { resolveSlots, resolveScopedSlots } from './render-helpers/resolve-slots' export function initRender (vm: Component) { @@ -121,4 +122,5 @@ export function renderMixin (Vue: Class) { Vue.prototype._v = createTextVNode Vue.prototype._e = createEmptyVNode Vue.prototype._u = resolveScopedSlots + Vue.prototype._g = bindObjectListeners } diff --git a/src/core/vdom/create-component.js b/src/core/vdom/create-component.js index 0553706c..162e3725 100644 --- a/src/core/vdom/create-component.js +++ b/src/core/vdom/create-component.js @@ -161,11 +161,8 @@ export function createComponent ( return createFunctionalComponent(Ctor, propsData, data, context, children) } - // extract listeners, since these needs to be treated as - // child component listeners instead of DOM listeners + // keep listeners const listeners = data.on - // replace with listeners with .native modifier - data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything diff --git a/src/platforms/web/runtime/modules/events.js b/src/platforms/web/runtime/modules/events.js index 104ef12c..8a1c1aea 100644 --- a/src/platforms/web/runtime/modules/events.js +++ b/src/platforms/web/runtime/modules/events.js @@ -66,11 +66,14 @@ function remove ( } function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { - if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { + const isComponentRoot = isDef(vnode.componentOptions) + let oldOn = isComponentRoot ? oldVnode.data.nativeOn : oldVnode.data.on + let on = isComponentRoot ? vnode.data.nativeOn : vnode.data.on + if (isUndef(oldOn) && isUndef(on)) { return } - const on = vnode.data.on || {} - const oldOn = oldVnode.data.on || {} + on = on || {} + oldOn = oldOn || {} target = vnode.elm normalizeEvents(on) updateListeners(on, oldOn, add, remove, vnode.context) diff --git a/src/platforms/weex/runtime/modules/events.js b/src/platforms/weex/runtime/modules/events.js index 824f08e6..3b49d174 100755 --- a/src/platforms/weex/runtime/modules/events.js +++ b/src/platforms/weex/runtime/modules/events.js @@ -39,11 +39,14 @@ function remove ( } function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { - if (!oldVnode.data.on && !vnode.data.on) { + const isComponentRoot = !!vnode.componentOptions + let oldOn = isComponentRoot ? oldVnode.data.nativeOn : oldVnode.data.on + let on = isComponentRoot ? vnode.data.nativeOn : vnode.data.on + if (!oldOn && !on) { return } - const on = vnode.data.on || {} - const oldOn = oldVnode.data.on || {} + on = on || {} + oldOn = oldOn || {} target = vnode.elm updateListeners(on, oldOn, add, remove, vnode.context) } diff --git a/test/unit/features/directives/on.spec.js b/test/unit/features/directives/on.spec.js index e77d18d7..7529aa31 100644 --- a/test/unit/features/directives/on.spec.js +++ b/test/unit/features/directives/on.spec.js @@ -298,26 +298,30 @@ describe('Directive v-on', () => { }) it('should bind to a child component', () => { - Vue.component('bar', { - template: 'Hello' - }) vm = new Vue({ el, template: '', - methods: { foo: spy } + methods: { foo: spy }, + components: { + bar: { + template: 'Hello' + } + } }) vm.$children[0].$emit('custom', 'foo', 'bar') expect(spy).toHaveBeenCalledWith('foo', 'bar') }) it('should be able to bind native events for a child component', () => { - Vue.component('bar', { - template: 'Hello' - }) vm = new Vue({ el, template: '', - methods: { foo: spy } + methods: { foo: spy }, + components: { + bar: { + template: 'Hello' + } + } }) vm.$children[0].$emit('click') expect(spy).not.toHaveBeenCalled() @@ -326,13 +330,15 @@ describe('Directive v-on', () => { }) it('.once modifier should work with child components', () => { - Vue.component('bar', { - template: 'Hello' - }) vm = new Vue({ el, template: '', - methods: { foo: spy } + methods: { foo: spy }, + components: { + bar: { + template: 'Hello' + } + } }) vm.$children[0].$emit('custom') expect(spy.calls.count()).toBe(1) @@ -593,4 +599,106 @@ describe('Directive v-on', () => { expect(`Use "contextmenu" instead`).toHaveBeenWarned() }) + + it('object syntax (no argument)', () => { + const click = jasmine.createSpy('click') + const mouseup = jasmine.createSpy('mouseup') + vm = new Vue({ + el, + template: ``, + created () { + this.listeners = { + click, + mouseup + } + } + }) + + triggerEvent(vm.$el, 'click') + expect(click.calls.count()).toBe(1) + expect(mouseup.calls.count()).toBe(0) + + triggerEvent(vm.$el, 'mouseup') + expect(click.calls.count()).toBe(1) + expect(mouseup.calls.count()).toBe(1) + }) + + it('object syntax (no argument, mixed with normal listeners)', () => { + const click1 = jasmine.createSpy('click1') + const click2 = jasmine.createSpy('click2') + const mouseup = jasmine.createSpy('mouseup') + vm = new Vue({ + el, + template: ``, + created () { + this.listeners = { + click: click1, + mouseup + } + }, + methods: { + click2 + } + }) + + triggerEvent(vm.$el, 'click') + expect(click1.calls.count()).toBe(1) + expect(click2.calls.count()).toBe(1) + expect(mouseup.calls.count()).toBe(0) + + triggerEvent(vm.$el, 'mouseup') + expect(click1.calls.count()).toBe(1) + expect(click2.calls.count()).toBe(1) + expect(mouseup.calls.count()).toBe(1) + }) + + it('object syntax (usage in HOC, mixed with native listners)', () => { + const click = jasmine.createSpy('click') + const mouseup = jasmine.createSpy('mouseup') + const mousedown = jasmine.createSpy('mousedown') + + var vm = new Vue({ + el, + template: ` + + hello + + `, + methods: { + click, + mouseup, + mousedown + }, + components: { + fooButton: { + template: ` + + ` + } + } + }) + + triggerEvent(vm.$el, 'click') + expect(click.calls.count()).toBe(1) + expect(mouseup.calls.count()).toBe(0) + expect(mousedown.calls.count()).toBe(0) + + triggerEvent(vm.$el, 'mouseup') + expect(click.calls.count()).toBe(1) + expect(mouseup.calls.count()).toBe(1) + expect(mousedown.calls.count()).toBe(0) + + triggerEvent(vm.$el, 'mousedown') + expect(click.calls.count()).toBe(1) + expect(mouseup.calls.count()).toBe(1) + expect(mousedown.calls.count()).toBe(1) + }) })