useEventListener
Memasang pendengar peristiwa yang ikut hidup dan mati bersama scope-nya.
Posisi: belum digerakkan
Klik: 0
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.
Pemasangan
npx 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.
import { getCurrentScope, onScopeDispose, toValue, watch } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { isBrowser } from '@/lib/is-browser'
import { bacaElemen } from '@/lib/target'
import type { TargetElemen } from '@/lib/target'
export interface UseEventListenerOptions {
/** Elemen yang dipasangi. Kosongkan untuk `window`. */
target?: TargetElemen
capture?: boolean
once?: boolean
passive?: boolean
/** Matikan sementara tanpa membongkar pemanggilannya. Boleh reaktif. */
enable?: MaybeRefOrGetter<boolean>
}
export function useEventListener(
eventName: string | string[],
handler: (event: any) => void,
options: UseEventListenerOptions = {},
) {
const { capture, once, passive } = options
const daftarNama = Array.isArray(eventName) ? eventName : [eventName]
let lepas: (() => void) | undefined
const bongkar = () => {
lepas?.()
lepas = undefined
}
const berhentiPantau = watch(
() => [
options.target === undefined
? (isBrowser ? window : undefined)
: bacaElemen(options.target),
toValue(options.enable ?? true),
] as const,
([elemen, aktif]) => {
// Selalu bongkar dulu. Target yang berganti meninggalkan pendengar di
// elemen lama kalau langkah ini dilewati, dan kebocoran seperti itu tidak
// pernah terlihat sampai ada yang mengganti-ganti targetnya.
bongkar()
if (!aktif || !elemen?.addEventListener)
return
const pendengar = (event: Event) => handler(event)
for (const nama of daftarNama)
elemen.addEventListener(nama, pendengar, { capture, once, passive })
lepas = () => {
for (const nama of daftarNama)
elemen.removeEventListener(nama, pendengar, { capture })
}
},
// `immediate` supaya target yang sudah ada sejak awal — `window`, elemen
// yang dioper langsung — tidak menunggu perubahan yang tidak akan datang.
// `flush: 'post'` supaya template ref sudah terisi saat gilirannya tiba.
{ immediate: true, flush: 'post' },
)
const berhenti = () => {
bongkar()
berhentiPantau()
}
if (getCurrentScope())
onScopeDispose(berhenti)
return berhenti
}API
export interface UseEventListenerOptions {
/** Elemen yang dipasangi. Kosongkan untuk `window`. */
target?: TargetElemen
capture?: boolean
once?: boolean
passive?: boolean
/** Matikan sementara tanpa membongkar pemanggilannya. Boleh reaktif. */
enable?: MaybeRefOrGetter<boolean>
}
export function useEventListener(
eventName: string | string[],
handler: (event: any) => void,
options: UseEventListenerOptions = {},
)