Memasang pendengar peristiwa yang ikut hidup dan mati bersama scope-nya.
Ini fondasi. Sembilan composable lain dibangun di atasnya, jadi kesalahan di sini menyebar ke semuanya — dan kesalahan yang paling mahal bukan salah pasang, melainkan lupa melepas.
Tiga hal yang membedakannya dari addEventListener biasa:
- Targetnya boleh reaktif.
useTemplateRef()masihnullsaatsetup()berjalan dan baru terisi setelah komponennya terpasang. Karena itu pemasangannya dijalankan lewatwatch, bukan sekali dionMounted— target yang berganti di tengah jalan ikut terlayani, pendengar lamanya dilepas lebih dulu. - Dilepas lewat scope, bukan lewat komponen.
onScopeDisposebekerja di dalam komponen maupun di dalameffectScope()lepas;onUnmountedhanya yang pertama. Composable ini dipakai composable lain, yang belum tentu dipanggil dari komponen. - Aman di server. Tanpa
window,watch-nya tetap jalan dan tidak menemukan apa pun untuk dipasangi. Tidak ada yang meledak.
Handler-nya dipanggil langsung, tanpa penampung apa pun. setup() berjalan sekali, jadi fungsi yang diterima di sini memang sudah yang terbaru selamanya — tidak ada nilai basi yang perlu disegarkan.
Installation
pnpm dlx shadcn-vue@latest add @kliping/use-event-listener Perlu @kliping terdaftar di components.json project Anda. Lihat cara menyiapkannya.
Ikut terpasang sendiri: is-browser, target.
Salin dulu is-browser, target — masing-masing memuat sumbernya di halamannya sendiri — lalu tempel berkas ini.
ts
import { getCurrentScope, onScopeDispose, toValue, watch } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { isBrowser } from '@/lib/is-browser'
import { getTargetElement } from '@/lib/target'
import type { BasicTarget } from '@/lib/target'
export interface UseEventListenerOptions {
/** Element to attach to. Leave empty for `window`. */
target?: BasicTarget
capture?: boolean
once?: boolean
passive?: boolean
/** Temporarily disable without removing the call. May be reactive. */
enable?: MaybeRefOrGetter<boolean>
}
export function useEventListener(
eventName: string | string[],
handler: (event: any) => void,
options: UseEventListenerOptions = {},
) {
const { capture, once, passive } = options
const eventNames = Array.isArray(eventName) ? eventName : [eventName]
let detach: (() => void) | undefined
const teardown = () => {
detach?.()
detach = undefined
}
const stopWatch = watch(
() =>
[
options.target === undefined
? isBrowser
? window
: undefined
: getTargetElement(options.target),
toValue(options.enable ?? true),
] as const,
([element, enabled]) => {
teardown()
if (!enabled || !element?.addEventListener) return
const listener = (event: Event) => handler(event)
for (const name of eventNames)
element.addEventListener(name, listener, { capture, once, passive })
detach = () => {
for (const name of eventNames) element.removeEventListener(name, listener, { capture })
}
},
{ immediate: true, flush: 'post' },
)
const stop = () => {
teardown()
stopWatch()
}
if (getCurrentScope()) onScopeDispose(stop)
return stop
}API
ts
export interface UseEventListenerOptions {
/** Element to attach to. Leave empty for `window`. */
target?: BasicTarget
capture?: boolean
once?: boolean
passive?: boolean
/** Temporarily disable without removing the call. May be reactive. */
enable?: MaybeRefOrGetter<boolean>
}
export function useEventListener(
eventName: string | string[],
handler: (event: any) => void,
options: UseEventListenerOptions = {},
)