diff --git a/src/compiler/helpers.js b/src/compiler/helpers.js index 9a91e8bb6f8..0ae213cf879 100644 --- a/src/compiler/helpers.js +++ b/src/compiler/helpers.js @@ -44,6 +44,10 @@ export function addHandler ( delete modifiers.capture name = '!' + name // mark the event as captured } + if (modifiers && modifiers.once) { + delete modifiers.once + name = '~' + name // mark the event as once + } let events if (modifiers && modifiers.native) { delete modifiers.native diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js index 3feef8cbe39..3523666bcf5 100644 --- a/src/core/vdom/helpers/update-listeners.js +++ b/src/core/vdom/helpers/update-listeners.js @@ -9,7 +9,7 @@ export function updateListeners ( remove: Function, vm: Component ) { - let name, cur, old, fn, event, capture + let name, cur, old, fn, event, capture, once for (name in on) { cur = on[name] old = oldOn[name] @@ -19,10 +19,12 @@ export function updateListeners ( vm ) } else if (!old) { - capture = name.charAt(0) === '!' - event = capture ? name.slice(1) : name + once = name.charAt(0) === '~' // Prefixed last, checked first + event = once ? name.slice(1) : name + capture = event.charAt(0) === '!' + event = capture ? event.slice(1) : event if (Array.isArray(cur)) { - add(event, (cur.invoker = arrInvoker(cur)), capture) + add(event, (cur.invoker = arrInvoker(cur)), capture, once) } else { if (!cur.invoker) { fn = cur @@ -30,7 +32,7 @@ export function updateListeners ( cur.fn = fn cur.invoker = fnInvoker(cur) } - add(event, cur.invoker, capture) + add(event, cur.invoker, capture, once) } } else if (cur !== old) { if (Array.isArray(old)) { @@ -45,8 +47,11 @@ export function updateListeners ( } for (name in oldOn) { if (!on[name]) { - event = name.charAt(0) === '!' ? name.slice(1) : name - remove(event, oldOn[name].invoker) + once = name.charAt(0) === '~' // Prefixed last, checked first + event = once ? name.slice(1) : name + capture = event.charAt(0) === '!' + event = capture ? event.slice(1) : event + remove(event, oldOn[name].invoker, capture) // Removal of a capturing listener does not affect a non-capturing version of the same listener, and vice versa. } } } diff --git a/src/platforms/web/runtime/modules/events.js b/src/platforms/web/runtime/modules/events.js index cf0c5441d99..063d6c5f2f4 100644 --- a/src/platforms/web/runtime/modules/events.js +++ b/src/platforms/web/runtime/modules/events.js @@ -9,11 +9,19 @@ function updateDOMListeners (oldVnode, vnode) { } const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} - const add = vnode.elm._v_add || (vnode.elm._v_add = (event, handler, capture) => { + const add = vnode.elm._v_add || (vnode.elm._v_add = (event, handler, capture, once) => { + if (once) { + const oldHandler = handler + handler = function (ev) { + remove(event, handler, capture) + + arguments.length === 1 ? oldHandler(ev) : oldHandler.apply(null, arguments) + } + } vnode.elm.addEventListener(event, handler, capture) }) - const remove = vnode.elm._v_remove || (vnode.elm._v_remove = (event, handler) => { - vnode.elm.removeEventListener(event, handler) + const remove = vnode.elm._v_remove || (vnode.elm._v_remove = (event, handler, capture) => { + vnode.elm.removeEventListener(event, handler, capture) }) 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 301c8ec0412..70b76a90132 100644 --- a/test/unit/features/directives/on.spec.js +++ b/test/unit/features/directives/on.spec.js @@ -102,6 +102,41 @@ describe('Directive v-on', () => { expect(callOrder.toString()).toBe('1,2') }) + it('should support once', () => { + vm = new Vue({ + el, + template: ` +
+
+ `, + methods: { foo: spy } + }) + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + }) + + it('should support capture and once', () => { + const callOrder = [] + vm = new Vue({ + el, + template: ` +
+
+
+ `, + methods: { + foo () { callOrder.push(1) }, + bar () { callOrder.push(2) } + } + }) + triggerEvent(vm.$el.firstChild, 'click') + expect(callOrder.toString()).toBe('1,2') + triggerEvent(vm.$el.firstChild, 'click') + expect(callOrder.toString()).toBe('1,2,2') + }) + it('should support keyCode', () => { vm = new Vue({ el, @@ -193,6 +228,88 @@ describe('Directive v-on', () => { }).then(done) }) + it('remove capturing listener', done => { + const spy2 = jasmine.createSpy('remove listener') + vm = new Vue({ + el, + methods: { foo: spy, bar: spy2, stopped (ev) { ev.stopPropagation() } }, + data: { + ok: true + }, + render (h) { + return this.ok + ? h('div', { on: { '!click': this.foo }}, [h('div', { on: { click: this.stopped }})]) + : h('div', { on: { mouseOver: this.bar }}, [h('div')]) + } + }) + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) + expect(spy2.calls.count()).toBe(0) + vm.ok = false + waitForUpdate(() => { + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + triggerEvent(vm.$el, 'mouseOver') + expect(spy2.calls.count()).toBe(1) + }).then(done) + }) + + it('remove once listener', done => { + const spy2 = jasmine.createSpy('remove listener') + vm = new Vue({ + el, + methods: { foo: spy, bar: spy2 }, + data: { + ok: true + }, + render (h) { + return this.ok + ? h('input', { on: { '~click': this.foo }}) + : h('input', { on: { input: this.bar }}) + } + }) + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + expect(spy2.calls.count()).toBe(0) + vm.ok = false + waitForUpdate(() => { + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + triggerEvent(vm.$el, 'input') + expect(spy2.calls.count()).toBe(1) + }).then(done) + }) + + it('remove capturing and once listener', done => { + const spy2 = jasmine.createSpy('remove listener') + vm = new Vue({ + el, + methods: { foo: spy, bar: spy2, stopped (ev) { ev.stopPropagation() } }, + data: { + ok: true + }, + render (h) { + return this.ok + ? h('div', { on: { '~!click': this.foo }}, [h('div', { on: { click: this.stopped }})]) + : h('div', { on: { mouseOver: this.bar }}, [h('div')]) + } + }) + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + expect(spy2.calls.count()).toBe(0) + vm.ok = false + waitForUpdate(() => { + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + triggerEvent(vm.$el, 'mouseOver') + expect(spy2.calls.count()).toBe(1) + }).then(done) + }) + it('remove listener on child component', done => { const spy2 = jasmine.createSpy('remove listener') vm = new Vue({ diff --git a/test/unit/modules/compiler/codegen.spec.js b/test/unit/modules/compiler/codegen.spec.js index f4759119237..ee971cadff6 100644 --- a/test/unit/modules/compiler/codegen.spec.js +++ b/test/unit/modules/compiler/codegen.spec.js @@ -256,6 +256,27 @@ describe('codegen', () => { ) }) + it('generate events with once modifier', () => { + assertCodegen( + '', + `with(this){return _h('input',{on:{"~input":function($event){onInput($event)}}})}` + ) + }) + + it('generate events with capture and once modifier', () => { + assertCodegen( + '', + `with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}` + ) + }) + + it('generate events with once and capture modifier', () => { + assertCodegen( + '', + `with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}` + ) + }) + it('generate events with inline statement', () => { assertCodegen( '',