Skip to content

Commit 9215ff0

Browse files
KingMarioyyx990803
authored andcommitted
Modifier once for v-on (vuejs#4267)
* Modifier once for v-on * Reformat code * Modifier once for v-on: using removeEventListener instead, bug fix of handler arguments passing, bug fix of modifier ordering problem * Enhancement of event listener removal which allows rendering of capturing / once events for render function * Reformat code
1 parent b7fd053 commit 9215ff0

File tree

5 files changed

+165
-10
lines changed

5 files changed

+165
-10
lines changed

src/compiler/helpers.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export function addHandler (
4646
delete modifiers.capture
4747
name = '!' + name // mark the event as captured
4848
}
49+
if (modifiers && modifiers.once) {
50+
delete modifiers.once
51+
name = '~' + name // mark the event as once
52+
}
4953
let events
5054
if (modifiers && modifiers.native) {
5155
delete modifiers.native

src/core/vdom/helpers/update-listeners.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function updateListeners (
99
remove: Function,
1010
vm: Component
1111
) {
12-
let name, cur, old, fn, event, capture
12+
let name, cur, old, fn, event, capture, once
1313
for (name in on) {
1414
cur = on[name]
1515
old = oldOn[name]
@@ -19,18 +19,20 @@ export function updateListeners (
1919
vm
2020
)
2121
} else if (!old) {
22-
capture = name.charAt(0) === '!'
23-
event = capture ? name.slice(1) : name
22+
once = name.charAt(0) === '~' // Prefixed last, checked first
23+
event = once ? name.slice(1) : name
24+
capture = event.charAt(0) === '!'
25+
event = capture ? event.slice(1) : event
2426
if (Array.isArray(cur)) {
25-
add(event, (cur.invoker = arrInvoker(cur)), capture)
27+
add(event, (cur.invoker = arrInvoker(cur)), capture, once)
2628
} else {
2729
if (!cur.invoker) {
2830
fn = cur
2931
cur = on[name] = {}
3032
cur.fn = fn
3133
cur.invoker = fnInvoker(cur)
3234
}
33-
add(event, cur.invoker, capture)
35+
add(event, cur.invoker, capture, once)
3436
}
3537
} else if (cur !== old) {
3638
if (Array.isArray(old)) {
@@ -45,8 +47,11 @@ export function updateListeners (
4547
}
4648
for (name in oldOn) {
4749
if (!on[name]) {
48-
event = name.charAt(0) === '!' ? name.slice(1) : name
49-
remove(event, oldOn[name].invoker)
50+
once = name.charAt(0) === '~' // Prefixed last, checked first
51+
event = once ? name.slice(1) : name
52+
capture = event.charAt(0) === '!'
53+
event = capture ? event.slice(1) : event
54+
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.
5055
}
5156
}
5257
}

src/platforms/web/runtime/modules/events.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@ function updateDOMListeners (oldVnode, vnode) {
99
}
1010
const on = vnode.data.on || {}
1111
const oldOn = oldVnode.data.on || {}
12-
const add = vnode.elm._v_add || (vnode.elm._v_add = (event, handler, capture) => {
12+
const add = vnode.elm._v_add || (vnode.elm._v_add = (event, handler, capture, once) => {
13+
if (once) {
14+
const oldHandler = handler
15+
handler = function (ev) {
16+
remove(event, handler, capture)
17+
18+
arguments.length === 1 ? oldHandler(ev) : oldHandler.apply(null, arguments)
19+
}
20+
}
1321
vnode.elm.addEventListener(event, handler, capture)
1422
})
15-
const remove = vnode.elm._v_remove || (vnode.elm._v_remove = (event, handler) => {
16-
vnode.elm.removeEventListener(event, handler)
23+
const remove = vnode.elm._v_remove || (vnode.elm._v_remove = (event, handler, capture) => {
24+
vnode.elm.removeEventListener(event, handler, capture)
1725
})
1826
updateListeners(on, oldOn, add, remove, vnode.context)
1927
}

test/unit/features/directives/on.spec.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,41 @@ describe('Directive v-on', () => {
115115
expect(callOrder.toString()).toBe('1,2')
116116
})
117117

118+
it('should support once', () => {
119+
vm = new Vue({
120+
el,
121+
template: `
122+
<div @click.once="foo">
123+
</div>
124+
`,
125+
methods: { foo: spy }
126+
})
127+
triggerEvent(vm.$el, 'click')
128+
expect(spy.calls.count()).toBe(1)
129+
triggerEvent(vm.$el, 'click')
130+
expect(spy.calls.count()).toBe(1) // should no longer trigger
131+
})
132+
133+
it('should support capture and once', () => {
134+
const callOrder = []
135+
vm = new Vue({
136+
el,
137+
template: `
138+
<div @click.capture.once="foo">
139+
<div @click="bar"></div>
140+
</div>
141+
`,
142+
methods: {
143+
foo () { callOrder.push(1) },
144+
bar () { callOrder.push(2) }
145+
}
146+
})
147+
triggerEvent(vm.$el.firstChild, 'click')
148+
expect(callOrder.toString()).toBe('1,2')
149+
triggerEvent(vm.$el.firstChild, 'click')
150+
expect(callOrder.toString()).toBe('1,2,2')
151+
})
152+
118153
it('should support keyCode', () => {
119154
vm = new Vue({
120155
el,
@@ -206,6 +241,88 @@ describe('Directive v-on', () => {
206241
}).then(done)
207242
})
208243

244+
it('remove capturing listener', done => {
245+
const spy2 = jasmine.createSpy('remove listener')
246+
vm = new Vue({
247+
el,
248+
methods: { foo: spy, bar: spy2, stopped (ev) { ev.stopPropagation() } },
249+
data: {
250+
ok: true
251+
},
252+
render (h) {
253+
return this.ok
254+
? h('div', { on: { '!click': this.foo }}, [h('div', { on: { click: this.stopped }})])
255+
: h('div', { on: { mouseOver: this.bar }}, [h('div')])
256+
}
257+
})
258+
triggerEvent(vm.$el.firstChild, 'click')
259+
expect(spy.calls.count()).toBe(1)
260+
expect(spy2.calls.count()).toBe(0)
261+
vm.ok = false
262+
waitForUpdate(() => {
263+
triggerEvent(vm.$el.firstChild, 'click')
264+
expect(spy.calls.count()).toBe(1) // should no longer trigger
265+
triggerEvent(vm.$el, 'mouseOver')
266+
expect(spy2.calls.count()).toBe(1)
267+
}).then(done)
268+
})
269+
270+
it('remove once listener', done => {
271+
const spy2 = jasmine.createSpy('remove listener')
272+
vm = new Vue({
273+
el,
274+
methods: { foo: spy, bar: spy2 },
275+
data: {
276+
ok: true
277+
},
278+
render (h) {
279+
return this.ok
280+
? h('input', { on: { '~click': this.foo }})
281+
: h('input', { on: { input: this.bar }})
282+
}
283+
})
284+
triggerEvent(vm.$el, 'click')
285+
expect(spy.calls.count()).toBe(1)
286+
triggerEvent(vm.$el, 'click')
287+
expect(spy.calls.count()).toBe(1) // should no longer trigger
288+
expect(spy2.calls.count()).toBe(0)
289+
vm.ok = false
290+
waitForUpdate(() => {
291+
triggerEvent(vm.$el, 'click')
292+
expect(spy.calls.count()).toBe(1) // should no longer trigger
293+
triggerEvent(vm.$el, 'input')
294+
expect(spy2.calls.count()).toBe(1)
295+
}).then(done)
296+
})
297+
298+
it('remove capturing and once listener', done => {
299+
const spy2 = jasmine.createSpy('remove listener')
300+
vm = new Vue({
301+
el,
302+
methods: { foo: spy, bar: spy2, stopped (ev) { ev.stopPropagation() } },
303+
data: {
304+
ok: true
305+
},
306+
render (h) {
307+
return this.ok
308+
? h('div', { on: { '~!click': this.foo }}, [h('div', { on: { click: this.stopped }})])
309+
: h('div', { on: { mouseOver: this.bar }}, [h('div')])
310+
}
311+
})
312+
triggerEvent(vm.$el.firstChild, 'click')
313+
expect(spy.calls.count()).toBe(1)
314+
triggerEvent(vm.$el.firstChild, 'click')
315+
expect(spy.calls.count()).toBe(1) // should no longer trigger
316+
expect(spy2.calls.count()).toBe(0)
317+
vm.ok = false
318+
waitForUpdate(() => {
319+
triggerEvent(vm.$el.firstChild, 'click')
320+
expect(spy.calls.count()).toBe(1) // should no longer trigger
321+
triggerEvent(vm.$el, 'mouseOver')
322+
expect(spy2.calls.count()).toBe(1)
323+
}).then(done)
324+
})
325+
209326
it('remove listener on child component', done => {
210327
const spy2 = jasmine.createSpy('remove listener')
211328
vm = new Vue({

test/unit/modules/compiler/codegen.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,27 @@ describe('codegen', () => {
296296
)
297297
})
298298

299+
it('generate events with once modifier', () => {
300+
assertCodegen(
301+
'<input @input.once="onInput">',
302+
`with(this){return _h('input',{on:{"~input":function($event){onInput($event)}}})}`
303+
)
304+
})
305+
306+
it('generate events with capture and once modifier', () => {
307+
assertCodegen(
308+
'<input @input.capture.once="onInput">',
309+
`with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}`
310+
)
311+
})
312+
313+
it('generate events with once and capture modifier', () => {
314+
assertCodegen(
315+
'<input @input.once.capture="onInput">',
316+
`with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}`
317+
)
318+
})
319+
299320
it('generate events with inline statement', () => {
300321
assertCodegen(
301322
'<input @input="curent++">',

0 commit comments

Comments
 (0)