Skip to content

Commit 08df965

Browse files
CyberAPyyx990803
authored andcommitted
feat(runtime-dom): support event options (vuejs#149)
1 parent 954f3f7 commit 08df965

File tree

2 files changed

+147
-8
lines changed

2 files changed

+147
-8
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { patchEvent } from '../src/modules/events'
2+
3+
describe(`events`, () => {
4+
it('should assign event handler', () => {
5+
const el = document.createElement('div')
6+
const event = new Event('click')
7+
const fn = jest.fn()
8+
patchEvent(el, 'click', null, fn, null)
9+
el.dispatchEvent(event)
10+
el.dispatchEvent(event)
11+
el.dispatchEvent(event)
12+
expect(fn).toHaveBeenCalledTimes(3)
13+
})
14+
15+
it('should update event handler', () => {
16+
const el = document.createElement('div')
17+
const event = new Event('click')
18+
const prevFn = jest.fn()
19+
const nextFn = jest.fn()
20+
patchEvent(el, 'click', null, prevFn, null)
21+
el.dispatchEvent(event)
22+
patchEvent(el, 'click', prevFn, nextFn, null)
23+
el.dispatchEvent(event)
24+
el.dispatchEvent(event)
25+
expect(prevFn).toHaveBeenCalledTimes(1)
26+
expect(nextFn).toHaveBeenCalledTimes(2)
27+
})
28+
29+
it('should support multiple event handlers', () => {
30+
const el = document.createElement('div')
31+
const event = new Event('click')
32+
const fn1 = jest.fn()
33+
const fn2 = jest.fn()
34+
patchEvent(el, 'click', null, [fn1, fn2], null)
35+
el.dispatchEvent(event)
36+
expect(fn1).toHaveBeenCalledTimes(1)
37+
expect(fn2).toHaveBeenCalledTimes(1)
38+
})
39+
40+
it('should unassign event handler', () => {
41+
const el = document.createElement('div')
42+
const event = new Event('click')
43+
const fn = jest.fn()
44+
patchEvent(el, 'click', null, fn, null)
45+
patchEvent(el, 'click', fn, null, null)
46+
el.dispatchEvent(event)
47+
expect(fn).not.toHaveBeenCalled()
48+
})
49+
50+
it('should support event options', () => {
51+
const el = document.createElement('div')
52+
const event = new Event('click')
53+
const fn = jest.fn()
54+
const nextValue = {
55+
handler: fn,
56+
options: {
57+
once: true
58+
}
59+
}
60+
patchEvent(el, 'click', null, nextValue, null)
61+
el.dispatchEvent(event)
62+
el.dispatchEvent(event)
63+
expect(fn).toHaveBeenCalledTimes(1)
64+
})
65+
66+
it('should support varying event options', () => {
67+
const el = document.createElement('div')
68+
const event = new Event('click')
69+
const prevFn = jest.fn()
70+
const nextFn = jest.fn()
71+
const nextValue = {
72+
handler: nextFn,
73+
options: {
74+
once: true
75+
}
76+
}
77+
patchEvent(el, 'click', null, prevFn, null)
78+
patchEvent(el, 'click', prevFn, nextValue, null)
79+
el.dispatchEvent(event)
80+
el.dispatchEvent(event)
81+
expect(prevFn).not.toHaveBeenCalled()
82+
expect(nextFn).toHaveBeenCalledTimes(1)
83+
})
84+
85+
it('should unassign event handler with options', () => {
86+
const el = document.createElement('div')
87+
const event = new Event('click')
88+
const fn = jest.fn()
89+
const nextValue = {
90+
handler: fn,
91+
options: {
92+
once: true
93+
}
94+
}
95+
patchEvent(el, 'click', null, nextValue, null)
96+
patchEvent(el, 'click', nextValue, null, null)
97+
el.dispatchEvent(event)
98+
el.dispatchEvent(event)
99+
expect(fn).not.toHaveBeenCalled()
100+
})
101+
})

packages/runtime-dom/src/modules/events.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isArray } from '@vue/shared'
1+
import { isArray, EMPTY_OBJ } from '@vue/shared'
22
import {
33
ComponentInternalInstance,
44
callWithAsyncErrorHandling
@@ -14,6 +14,13 @@ type EventValue = (Function | Function[]) & {
1414
invoker?: Invoker | null
1515
}
1616

17+
type EventValueWithOptions = {
18+
handler: EventValue
19+
options: AddEventListenerOptions
20+
persistent?: boolean
21+
invoker?: Invoker | null
22+
}
23+
1724
// Async edge case fix requires storing an event listener's attach timestamp.
1825
let _getNow: () => number = Date.now
1926

@@ -43,22 +50,53 @@ const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
4350
export function patchEvent(
4451
el: Element,
4552
name: string,
46-
prevValue: EventValue | null,
47-
nextValue: EventValue | null,
53+
prevValue: EventValueWithOptions | EventValue | null,
54+
nextValue: EventValueWithOptions | EventValue | null,
4855
instance: ComponentInternalInstance | null = null
4956
) {
57+
const prevOptions = prevValue && 'options' in prevValue && prevValue.options
58+
const nextOptions = nextValue && 'options' in nextValue && nextValue.options
5059
const invoker = prevValue && prevValue.invoker
51-
if (nextValue) {
60+
const value =
61+
nextValue && 'handler' in nextValue ? nextValue.handler : nextValue
62+
const persistent =
63+
nextValue && 'persistent' in nextValue && nextValue.persistent
64+
65+
if (!persistent && (prevOptions || nextOptions)) {
66+
const prev = prevOptions || EMPTY_OBJ
67+
const next = nextOptions || EMPTY_OBJ
68+
if (
69+
prev.capture !== next.capture ||
70+
prev.passive !== next.passive ||
71+
prev.once !== next.once
72+
) {
73+
if (invoker) {
74+
el.removeEventListener(name, invoker as any, prevOptions as any)
75+
}
76+
if (nextValue && value) {
77+
const invoker = createInvoker(value, instance)
78+
nextValue.invoker = invoker
79+
el.addEventListener(name, invoker, nextOptions as any)
80+
}
81+
return
82+
}
83+
}
84+
85+
if (nextValue && value) {
5286
if (invoker) {
5387
;(prevValue as EventValue).invoker = null
54-
invoker.value = nextValue
88+
invoker.value = value
5589
nextValue.invoker = invoker
5690
invoker.lastUpdated = getNow()
5791
} else {
58-
el.addEventListener(name, createInvoker(nextValue, instance))
92+
el.addEventListener(
93+
name,
94+
createInvoker(value, instance),
95+
nextOptions as any
96+
)
5997
}
6098
} else if (invoker) {
61-
el.removeEventListener(name, invoker)
99+
el.removeEventListener(name, invoker, prevOptions as any)
62100
}
63101
}
64102

@@ -73,7 +111,7 @@ function createInvoker(
73111
// the solution is simple: we save the timestamp when a handler is attached,
74112
// and the handler would only fire if the event passed to it was fired
75113
// AFTER it was attached.
76-
if (e.timeStamp >= invoker.lastUpdated) {
114+
if (e.timeStamp >= invoker.lastUpdated - 1) {
77115
const args = [e]
78116
const value = invoker.value
79117
if (isArray(value)) {

0 commit comments

Comments
 (0)