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

feat(useEventListener): accept multiple events or listeners #2180

Merged
merged 7 commits into from Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
147 changes: 147 additions & 0 deletions packages/core/useEventListener/index.test.ts
@@ -0,0 +1,147 @@
import type { Fn } from '@vueuse/shared'
import type { SpyInstance } from 'vitest'
import { useEventListener } from '.'

describe('useEventListener', () => {
const options = { capture: true }
let stop: Fn
let target: HTMLDivElement
let removeSpy: SpyInstance
let addSpy: SpyInstance

beforeEach(() => {
target = document.createElement('div')
removeSpy = vitest.spyOn(target, 'removeEventListener')
addSpy = vitest.spyOn(target, 'addEventListener')
})

it('should be defined', () => {
expect(useEventListener).toBeDefined()
})

describe('given both none array', () => {
const listener = vitest.fn()
const event = 'click'

beforeEach(() => {
listener.mockReset()
stop = useEventListener(target, event, listener, options)
})

it('should add listener', () => {
expect(addSpy).toBeCalledTimes(1)
})

it('should trigger listener', () => {
expect(listener).not.toBeCalled()
target.dispatchEvent(new MouseEvent(event))
expect(listener).toBeCalledTimes(1)
})

it('should remove listener', () => {
expect(removeSpy).not.toBeCalled()

stop()

expect(removeSpy).toBeCalledTimes(1)
expect(removeSpy).toBeCalledWith(event, listener, options)
})
})

describe('given array of events but single listener', () => {
const listener = vitest.fn()
const events = ['click', 'scroll', 'blur', 'resize']

beforeEach(() => {
listener.mockReset()
stop = useEventListener(target, events, listener, options)
})

it('should add listener for all events', () => {
events.forEach(event => expect(addSpy).toBeCalledWith(event, listener, options))
})

it('should trigger listener with all events', () => {
expect(listener).not.toBeCalled()
events.forEach((event, index) => {
target.dispatchEvent(new Event(event))
expect(listener).toBeCalledTimes(index + 1)
})
})

it('should remove listener with all events', () => {
expect(removeSpy).not.toBeCalled()

stop()

expect(removeSpy).toBeCalledTimes(events.length)
events.forEach(event => expect(removeSpy).toBeCalledWith(event, listener, options))
})
})

describe('given single event but array of listeners', () => {
const listeners = [vitest.fn(), vitest.fn(), vitest.fn()]
const event = 'click'

beforeEach(() => {
listeners.forEach(listener => listener.mockReset())
stop = useEventListener(target, event, listeners, options)
})

it('should add all listeners', () => {
listeners.forEach(listener => expect(addSpy).toBeCalledWith(event, listener, options))
})

it('should call all listeners with single click event', () => {
listeners.forEach(listener => expect(listener).not.toBeCalled())

target.dispatchEvent(new Event(event))

listeners.forEach(listener => expect(listener).toBeCalledTimes(1))
})

it('should remove listeners', () => {
expect(removeSpy).not.toBeCalled()

stop()

expect(removeSpy).toBeCalledTimes(listeners.length)
listeners.forEach(listener => expect(removeSpy).toBeCalledWith(event, listener, options))
})
})

describe('given both array of events and listeners', () => {
const listeners = [vitest.fn(), vitest.fn(), vitest.fn()]
const events = ['click', 'scroll', 'blur', 'resize', 'custom-event']

beforeEach(() => {
listeners.forEach(listener => listener.mockReset())
stop = useEventListener(target, events, listeners, options)
})

it('should add all listeners for all events', () => {
listeners.forEach((listener) => {
events.forEach((event) => {
expect(addSpy).toBeCalledWith(event, listener, options)
})
})
})

it('should call all listeners with all events', () => {
events.forEach((event, index) => {
target.dispatchEvent(new Event(event))
listeners.forEach(listener => expect(listener).toBeCalledTimes(index + 1))
})
})

it('should remove all listeners with all events', () => {
stop()

listeners.forEach((listener) => {
events.forEach((event) => {
expect(removeSpy).toBeCalledWith(event, listener, options)
})
})
})
})
})
59 changes: 36 additions & 23 deletions packages/core/useEventListener/index.ts
@@ -1,4 +1,4 @@
import type { Fn, MaybeComputedRef } from '@vueuse/shared'
import type { Arrayable, Fn, MaybeComputedRef } from '@vueuse/shared'
import { isString, noop, tryOnScopeDispose } from '@vueuse/shared'
import { watch } from 'vue-demi'
import type { MaybeElementRef } from '../unrefElement'
Expand Down Expand Up @@ -28,8 +28,8 @@ export interface GeneralEventListener<E = Event> {
* @param options
*/
export function useEventListener<E extends keyof WindowEventMap>(
event: E,
listener: (this: Window, ev: WindowEventMap[E]) => any,
event: Arrayable<E>,
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
options?: boolean | AddEventListenerOptions
): Fn

Expand All @@ -46,8 +46,8 @@ export function useEventListener<E extends keyof WindowEventMap>(
*/
export function useEventListener<E extends keyof WindowEventMap>(
target: Window,
event: E,
listener: (this: Window, ev: WindowEventMap[E]) => any,
event: Arrayable<E>,
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
options?: boolean | AddEventListenerOptions
): Fn

Expand All @@ -64,8 +64,8 @@ export function useEventListener<E extends keyof WindowEventMap>(
*/
export function useEventListener<E extends keyof DocumentEventMap>(
target: Document,
event: E,
listener: (this: Document, ev: DocumentEventMap[E]) => any,
event: Arrayable<E>,
listener: Arrayable<(this: Document, ev: DocumentEventMap[E]) => any>,
options?: boolean | AddEventListenerOptions
): Fn

Expand All @@ -82,8 +82,8 @@ export function useEventListener<E extends keyof DocumentEventMap>(
*/
export function useEventListener<Names extends string, EventType = Event>(
target: InferEventTarget<Names>,
event: Names,
listener: GeneralEventListener<EventType>,
event: Arrayable<Names>,
listener: Arrayable<GeneralEventListener<EventType>>,
options?: boolean | AddEventListenerOptions
): Fn

Expand All @@ -100,29 +100,43 @@ export function useEventListener<Names extends string, EventType = Event>(
*/
export function useEventListener<EventType = Event>(
target: MaybeComputedRef<EventTarget | null | undefined>,
event: string,
listener: GeneralEventListener<EventType>,
event: Arrayable<string>,
listener: Arrayable<GeneralEventListener<EventType>>,
options?: boolean | AddEventListenerOptions
): Fn

export function useEventListener(...args: any[]) {
let target: MaybeComputedRef<EventTarget> | undefined
let event: string
let listener: any
let events: Arrayable<string>
let listeners: Arrayable<Function>
let options: any

if (isString(args[0])) {
[event, listener, options] = args
if (isString(args[0]) || Array.isArray(args[0])) {
[events, listeners, options] = args
target = defaultWindow
}
else {
[target, event, listener, options] = args
[target, events, listeners, options] = args
}

if (!target)
return noop

let cleanup = noop
if (!Array.isArray(events))
events = [events]
if (!Array.isArray(listeners))
listeners = [listeners]

const cleanups: Function[] = []
const cleanup = () => {
cleanups.forEach(fn => fn())
cleanups.length = 0
}

const register = (el: any, event: string, listener: any) => {
el.addEventListener(event, listener, options)
return () => el.removeEventListener(event, listener, options)
}

const stopWatch = watch(
() => unrefElement(target as unknown as MaybeElementRef),
Expand All @@ -131,12 +145,11 @@ export function useEventListener(...args: any[]) {
if (!el)
return

el.addEventListener(event, listener, options)

cleanup = () => {
el.removeEventListener(event, listener, options)
cleanup = noop
}
cleanups.push(
...(events as string[]).flatMap((event) => {
return (listeners as Function[]).map(listener => register(el, event, listener))
}),
)
},
{ immediate: true, flush: 'post' },
)
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/utils/types.ts
Expand Up @@ -60,6 +60,8 @@ export type DeepMaybeRef<T> = T extends Ref<infer V>
? { [K in keyof T]: DeepMaybeRef<T[K]> }
: MaybeRef<T>

export type Arrayable<T> = T[] | T

/**
* Infers the element type of an array
*/
Expand Down