Skip to content

Commit 11614d6

Browse files
committed
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.
1 parent b0b6b7e commit 11614d6

File tree

12 files changed

+178
-23
lines changed

12 files changed

+178
-23
lines changed

Diff for: flow/compiler.js

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ declare type ASTElement = {
133133
once?: true;
134134
onceProcessed?: boolean;
135135
wrapData?: (code: string) => string;
136+
wrapListeners?: (code: string) => string;
136137

137138
// 2.4 ssr optimization
138139
ssrOptimizability?: number;

Diff for: flow/component.js

+2
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ declare interface Component {
121121
_t: (name: string, fallback: ?Array<VNode>, props: ?Object) => ?Array<VNode>;
122122
// apply v-bind object
123123
_b: (data: any, tag: string, value: any, asProp: boolean, isSync?: boolean) => VNodeData;
124+
// apply v-on object
125+
_g: (data: any, value: any) => VNodeData;
124126
// check custom keyCode
125127
_k: (eventKeyCode: number, key: string, builtInAlias: number | Array<number> | void) => boolean;
126128
// resolve scoped slots

Diff for: flow/vnode.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ declare type VNodeWithData = {
2727
context: Component;
2828
key: string | number | void;
2929
parent?: VNodeWithData;
30+
componentOptions?: VNodeComponentOptions;
3031
componentInstance?: Component;
3132
isRootInsert: boolean;
3233
};

Diff for: src/compiler/codegen/index.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* @flow */
22

33
import { genHandlers } from './events'
4-
import { baseWarn, pluckModuleFunction } from '../helpers'
54
import baseDirectives from '../directives/index'
65
import { camelize, no, extend } from 'shared/util'
6+
import { baseWarn, pluckModuleFunction } from '../helpers'
77

88
type TransformFunction = (el: ASTElement, code: string) => string;
99
type DataGenFunction = (el: ASTElement) => string;
@@ -268,6 +268,10 @@ export function genData (el: ASTElement, state: CodegenState): string {
268268
if (el.wrapData) {
269269
data = el.wrapData(data)
270270
}
271+
// v-on data wrap
272+
if (el.wrapListeners) {
273+
data = el.wrapListeners(data)
274+
}
271275
return data
272276
}
273277

Diff for: src/compiler/directives/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/* @flow */
22

3+
import on from './on'
34
import bind from './bind'
45
import { noop } from 'shared/util'
56

67
export default {
8+
on,
79
bind,
810
cloak: noop
911
}

Diff for: src/compiler/directives/on.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* @flow */
2+
3+
import { warn } from 'core/util/index'
4+
5+
export default function on (el: ASTElement, dir: ASTDirective) {
6+
if (process.env.NODE_ENV !== 'production' && dir.modifiers) {
7+
warn(`v-on without argument does not support modifiers.`)
8+
}
9+
el.wrapListeners = (code: string) => `_g(${code},${dir.value})`
10+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* @flow */
2+
3+
import { warn, extend, isPlainObject } from 'core/util/index'
4+
5+
export function bindObjectListeners (data: any, value: any): VNodeData {
6+
if (value) {
7+
if (!isPlainObject(value)) {
8+
process.env.NODE_ENV !== 'production' && warn(
9+
'v-on without argument expects an Object value',
10+
this
11+
)
12+
} else {
13+
const on = data.on = data.on ? extend({}, data.on) : {}
14+
for (const key in value) {
15+
const existing = on[key]
16+
const ours = value[key]
17+
on[key] = existing ? [ours].concat(existing) : ours
18+
}
19+
}
20+
}
21+
return data
22+
}

Diff for: src/core/instance/render.js

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { resolveFilter } from './render-helpers/resolve-filter'
2424
import { checkKeyCodes } from './render-helpers/check-keycodes'
2525
import { bindObjectProps } from './render-helpers/bind-object-props'
2626
import { renderStatic, markOnce } from './render-helpers/render-static'
27+
import { bindObjectListeners } from './render-helpers/bind-object-listeners'
2728
import { resolveSlots, resolveScopedSlots } from './render-helpers/resolve-slots'
2829

2930
export function initRender (vm: Component) {
@@ -121,4 +122,5 @@ export function renderMixin (Vue: Class<Component>) {
121122
Vue.prototype._v = createTextVNode
122123
Vue.prototype._e = createEmptyVNode
123124
Vue.prototype._u = resolveScopedSlots
125+
Vue.prototype._g = bindObjectListeners
124126
}

Diff for: src/core/vdom/create-component.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,8 @@ export function createComponent (
161161
return createFunctionalComponent(Ctor, propsData, data, context, children)
162162
}
163163

164-
// extract listeners, since these needs to be treated as
165-
// child component listeners instead of DOM listeners
164+
// keep listeners
166165
const listeners = data.on
167-
// replace with listeners with .native modifier
168-
data.on = data.nativeOn
169166

170167
if (isTrue(Ctor.options.abstract)) {
171168
// abstract components do not keep anything

Diff for: src/platforms/web/runtime/modules/events.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,14 @@ function remove (
6666
}
6767

6868
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
69-
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
69+
const isComponentRoot = isDef(vnode.componentOptions)
70+
let oldOn = isComponentRoot ? oldVnode.data.nativeOn : oldVnode.data.on
71+
let on = isComponentRoot ? vnode.data.nativeOn : vnode.data.on
72+
if (isUndef(oldOn) && isUndef(on)) {
7073
return
7174
}
72-
const on = vnode.data.on || {}
73-
const oldOn = oldVnode.data.on || {}
75+
on = on || {}
76+
oldOn = oldOn || {}
7477
target = vnode.elm
7578
normalizeEvents(on)
7679
updateListeners(on, oldOn, add, remove, vnode.context)

Diff for: src/platforms/weex/runtime/modules/events.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ function remove (
3939
}
4040

4141
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
42-
if (!oldVnode.data.on && !vnode.data.on) {
42+
const isComponentRoot = !!vnode.componentOptions
43+
let oldOn = isComponentRoot ? oldVnode.data.nativeOn : oldVnode.data.on
44+
let on = isComponentRoot ? vnode.data.nativeOn : vnode.data.on
45+
if (!oldOn && !on) {
4346
return
4447
}
45-
const on = vnode.data.on || {}
46-
const oldOn = oldVnode.data.on || {}
48+
on = on || {}
49+
oldOn = oldOn || {}
4750
target = vnode.elm
4851
updateListeners(on, oldOn, add, remove, vnode.context)
4952
}

Diff for: test/unit/features/directives/on.spec.js

+120-12
Original file line numberDiff line numberDiff line change
@@ -298,26 +298,30 @@ describe('Directive v-on', () => {
298298
})
299299

300300
it('should bind to a child component', () => {
301-
Vue.component('bar', {
302-
template: '<span>Hello</span>'
303-
})
304301
vm = new Vue({
305302
el,
306303
template: '<bar @custom="foo"></bar>',
307-
methods: { foo: spy }
304+
methods: { foo: spy },
305+
components: {
306+
bar: {
307+
template: '<span>Hello</span>'
308+
}
309+
}
308310
})
309311
vm.$children[0].$emit('custom', 'foo', 'bar')
310312
expect(spy).toHaveBeenCalledWith('foo', 'bar')
311313
})
312314

313315
it('should be able to bind native events for a child component', () => {
314-
Vue.component('bar', {
315-
template: '<span>Hello</span>'
316-
})
317316
vm = new Vue({
318317
el,
319318
template: '<bar @click.native="foo"></bar>',
320-
methods: { foo: spy }
319+
methods: { foo: spy },
320+
components: {
321+
bar: {
322+
template: '<span>Hello</span>'
323+
}
324+
}
321325
})
322326
vm.$children[0].$emit('click')
323327
expect(spy).not.toHaveBeenCalled()
@@ -326,13 +330,15 @@ describe('Directive v-on', () => {
326330
})
327331

328332
it('.once modifier should work with child components', () => {
329-
Vue.component('bar', {
330-
template: '<span>Hello</span>'
331-
})
332333
vm = new Vue({
333334
el,
334335
template: '<bar @custom.once="foo"></bar>',
335-
methods: { foo: spy }
336+
methods: { foo: spy },
337+
components: {
338+
bar: {
339+
template: '<span>Hello</span>'
340+
}
341+
}
336342
})
337343
vm.$children[0].$emit('custom')
338344
expect(spy.calls.count()).toBe(1)
@@ -593,4 +599,106 @@ describe('Directive v-on', () => {
593599

594600
expect(`Use "contextmenu" instead`).toHaveBeenWarned()
595601
})
602+
603+
it('object syntax (no argument)', () => {
604+
const click = jasmine.createSpy('click')
605+
const mouseup = jasmine.createSpy('mouseup')
606+
vm = new Vue({
607+
el,
608+
template: `<button v-on="listeners">foo</button>`,
609+
created () {
610+
this.listeners = {
611+
click,
612+
mouseup
613+
}
614+
}
615+
})
616+
617+
triggerEvent(vm.$el, 'click')
618+
expect(click.calls.count()).toBe(1)
619+
expect(mouseup.calls.count()).toBe(0)
620+
621+
triggerEvent(vm.$el, 'mouseup')
622+
expect(click.calls.count()).toBe(1)
623+
expect(mouseup.calls.count()).toBe(1)
624+
})
625+
626+
it('object syntax (no argument, mixed with normal listeners)', () => {
627+
const click1 = jasmine.createSpy('click1')
628+
const click2 = jasmine.createSpy('click2')
629+
const mouseup = jasmine.createSpy('mouseup')
630+
vm = new Vue({
631+
el,
632+
template: `<button v-on="listeners" @click="click2">foo</button>`,
633+
created () {
634+
this.listeners = {
635+
click: click1,
636+
mouseup
637+
}
638+
},
639+
methods: {
640+
click2
641+
}
642+
})
643+
644+
triggerEvent(vm.$el, 'click')
645+
expect(click1.calls.count()).toBe(1)
646+
expect(click2.calls.count()).toBe(1)
647+
expect(mouseup.calls.count()).toBe(0)
648+
649+
triggerEvent(vm.$el, 'mouseup')
650+
expect(click1.calls.count()).toBe(1)
651+
expect(click2.calls.count()).toBe(1)
652+
expect(mouseup.calls.count()).toBe(1)
653+
})
654+
655+
it('object syntax (usage in HOC, mixed with native listners)', () => {
656+
const click = jasmine.createSpy('click')
657+
const mouseup = jasmine.createSpy('mouseup')
658+
const mousedown = jasmine.createSpy('mousedown')
659+
660+
var vm = new Vue({
661+
el,
662+
template: `
663+
<foo-button
664+
id="foo"
665+
@click="click"
666+
@mousedown="mousedown"
667+
@mouseup.native="mouseup">
668+
hello
669+
</foo-button>
670+
`,
671+
methods: {
672+
click,
673+
mouseup,
674+
mousedown
675+
},
676+
components: {
677+
fooButton: {
678+
template: `
679+
<button
680+
v-bind="$vnode.data.attrs"
681+
v-on="$vnode.data.on">
682+
<slot/>
683+
</button>
684+
`
685+
}
686+
}
687+
})
688+
689+
triggerEvent(vm.$el, 'click')
690+
expect(click.calls.count()).toBe(1)
691+
expect(mouseup.calls.count()).toBe(0)
692+
expect(mousedown.calls.count()).toBe(0)
693+
694+
triggerEvent(vm.$el, 'mouseup')
695+
expect(click.calls.count()).toBe(1)
696+
expect(mouseup.calls.count()).toBe(1)
697+
expect(mousedown.calls.count()).toBe(0)
698+
699+
triggerEvent(vm.$el, 'mousedown')
700+
expect(click.calls.count()).toBe(1)
701+
expect(mouseup.calls.count()).toBe(1)
702+
expect(mousedown.calls.count()).toBe(1)
703+
})
596704
})

0 commit comments

Comments
 (0)