Skip to content

Commit

Permalink
feat(v-on): support v-on object syntax with no arguments
Browse files Browse the repository at this point in the history
Note this does not support modifiers and is meant to be used for handling
events proxying in higher-order-components.
  • Loading branch information
yyx990803 committed Jul 11, 2017
1 parent b0b6b7e commit 11614d6
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 23 deletions.
1 change: 1 addition & 0 deletions flow/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions flow/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ declare interface Component {
_t: (name: string, fallback: ?Array<VNode>, props: ?Object) => ?Array<VNode>;
// 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<number> | void) => boolean;
// resolve scoped slots
Expand Down
1 change: 1 addition & 0 deletions flow/vnode.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ declare type VNodeWithData = {
context: Component;
key: string | number | void;
parent?: VNodeWithData;
componentOptions?: VNodeComponentOptions;
componentInstance?: Component;
isRootInsert: boolean;
};
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/codegen/index.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions src/compiler/directives/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* @flow */

import on from './on'
import bind from './bind'
import { noop } from 'shared/util'

export default {
on,
bind,
cloak: noop
}
10 changes: 10 additions & 0 deletions src/compiler/directives/on.js
Original file line number Diff line number Diff line change
@@ -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})`
}
22 changes: 22 additions & 0 deletions src/core/instance/render-helpers/bind-object-listeners.js
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions src/core/instance/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -121,4 +122,5 @@ export function renderMixin (Vue: Class<Component>) {
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners
}
5 changes: 1 addition & 4 deletions src/core/vdom/create-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions src/platforms/web/runtime/modules/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions src/platforms/weex/runtime/modules/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
132 changes: 120 additions & 12 deletions test/unit/features/directives/on.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,26 +298,30 @@ describe('Directive v-on', () => {
})

it('should bind to a child component', () => {
Vue.component('bar', {
template: '<span>Hello</span>'
})
vm = new Vue({
el,
template: '<bar @custom="foo"></bar>',
methods: { foo: spy }
methods: { foo: spy },
components: {
bar: {
template: '<span>Hello</span>'
}
}
})
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: '<span>Hello</span>'
})
vm = new Vue({
el,
template: '<bar @click.native="foo"></bar>',
methods: { foo: spy }
methods: { foo: spy },
components: {
bar: {
template: '<span>Hello</span>'
}
}
})
vm.$children[0].$emit('click')
expect(spy).not.toHaveBeenCalled()
Expand All @@ -326,13 +330,15 @@ describe('Directive v-on', () => {
})

it('.once modifier should work with child components', () => {
Vue.component('bar', {
template: '<span>Hello</span>'
})
vm = new Vue({
el,
template: '<bar @custom.once="foo"></bar>',
methods: { foo: spy }
methods: { foo: spy },
components: {
bar: {
template: '<span>Hello</span>'
}
}
})
vm.$children[0].$emit('custom')
expect(spy.calls.count()).toBe(1)
Expand Down Expand Up @@ -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: `<button v-on="listeners">foo</button>`,
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: `<button v-on="listeners" @click="click2">foo</button>`,
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: `
<foo-button
id="foo"
@click="click"
@mousedown="mousedown"
@mouseup.native="mouseup">
hello
</foo-button>
`,
methods: {
click,
mouseup,
mousedown
},
components: {
fooButton: {
template: `
<button
v-bind="$vnode.data.attrs"
v-on="$vnode.data.on">
<slot/>
</button>
`
}
}
})

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)
})
})

0 comments on commit 11614d6

Please sign in to comment.