Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modifier once for v-on #4267

Merged
merged 5 commits into from
Nov 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/compiler/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 12 additions & 7 deletions src/core/vdom/helpers/update-listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -19,18 +19,20 @@ 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
cur = on[name] = {}
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)) {
Expand All @@ -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.
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions src/platforms/web/runtime/modules/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
117 changes: 117 additions & 0 deletions test/unit/features/directives/on.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,41 @@ describe('Directive v-on', () => {
expect(callOrder.toString()).toBe('1,2')
})

it('should support once', () => {
vm = new Vue({
el,
template: `
<div @click.once="foo">
</div>
`,
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: `
<div @click.capture.once="foo">
<div @click="bar"></div>
</div>
`,
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,
Expand Down Expand Up @@ -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({
Expand Down
21 changes: 21 additions & 0 deletions test/unit/modules/compiler/codegen.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,27 @@ describe('codegen', () => {
)
})

it('generate events with once modifier', () => {
assertCodegen(
'<input @input.once="onInput">',
`with(this){return _h('input',{on:{"~input":function($event){onInput($event)}}})}`
)
})

it('generate events with capture and once modifier', () => {
assertCodegen(
'<input @input.capture.once="onInput">',
`with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}`
)
})

it('generate events with once and capture modifier', () => {
assertCodegen(
'<input @input.once.capture="onInput">',
`with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}`
)
})

it('generate events with inline statement', () => {
assertCodegen(
'<input @input="curent++">',
Expand Down