Browse Source

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.
dev
Evan You 7 years ago
parent
commit
11614d63b7
  1. 1
      flow/compiler.js
  2. 2
      flow/component.js
  3. 1
      flow/vnode.js
  4. 6
      src/compiler/codegen/index.js
  5. 2
      src/compiler/directives/index.js
  6. 10
      src/compiler/directives/on.js
  7. 22
      src/core/instance/render-helpers/bind-object-listeners.js
  8. 2
      src/core/instance/render.js
  9. 5
      src/core/vdom/create-component.js
  10. 9
      src/platforms/web/runtime/modules/events.js
  11. 9
      src/platforms/weex/runtime/modules/events.js
  12. 132
      test/unit/features/directives/on.spec.js

1
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;

2
flow/component.js

@ -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

1
flow/vnode.js

@ -27,6 +27,7 @@ declare type VNodeWithData = {
context: Component;
key: string | number | void;
parent?: VNodeWithData;
componentOptions?: VNodeComponentOptions;
componentInstance?: Component;
isRootInsert: boolean;
};

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

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

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

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

2
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<Component>) {
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners
}

5
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

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

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

132
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: '<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()
@ -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)
@ -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)
})
})

Loading…
Cancel
Save