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(
'',