Vize

event-listener

Attach an event listener to a reactive target and clean it up with the current reactive scope.

Package @vizejs/composable/event-listener
Own the source vize lib pull composable:event-listener
Runtime exports useEventListener
Gzip budget 2048 B

Minimal setup

Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).

vp install @vizejs/composable

Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.

import { useEventListener } from "@vizejs/composable/event-listener";

Try it

Observe input from a selected workshop draft

Follow native input events from one editable draft, retarget the listener, and pause or resume observation without locking either field.

Open the interactive example

What to observe: Edit First workshop draft to update the event count and last observed text. Editing the other field does not update the monitor. Choose Second workshop draft to move the listener. Pause, edit either draft, switch the observed draft, then Resume: only new input from the current target is observed.

useEventListener receives a computed HTMLInputElement target and a typed input handler. Retargeting removes the old listener. stop() removes the watcher and listener; start() restores observation of the current target, and repeated active starts are harmless. Each v-model remains independently writable. The monitor reads currentTarget.value from the real event and sends no request or save operation. Inside a component, attachment waits for mounted state, keeping isListening false during SSR and the initial hydration render. The owning scope removes the watcher and listener on disposal.

Compare the initial and interacted states
Observe input from a selected workshop draft: initial state
Initial state
Observe input from a selected workshop draft: state after the verified interactions
After the verified interactions

Copy the example

Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.

<script setup lang="ts">
import { computed, ref, useId, useTemplateRef } from "vue";
import { useEventListener } from "@vizejs/composable/event-listener";

const draftsId = useId();
const firstInput = useTemplateRef<HTMLInputElement>("firstInput");
const secondInput = useTemplateRef<HTMLInputElement>("secondInput");
const firstDraft = ref("Compiler workshop");
const secondDraft = ref("Editor workshop");
const selectedTarget = ref("first");
const target = computed(() =>
  selectedTarget.value === "first" ? firstInput.value : secondInput.value,
);
const targetName = computed(() => (selectedTarget.value === "first" ? "first" : "second"));
const observedEvents = ref(0);
const observedText = ref("No input observed yet.");
const { isListening, start, stop } = useEventListener(target, "input", (event) => {
  if (!(event.currentTarget instanceof HTMLInputElement)) return;
  observedEvents.value += 1;
  observedText.value = event.currentTarget.value;
});
</script>

<template>
  <div class="composable-example">
    <label :for="`${draftsId}-first`">First workshop draft</label>
    <input :id="`${draftsId}-first`" ref="firstInput" v-model="firstDraft" />
    <label :for="`${draftsId}-second`">Second workshop draft</label>
    <input :id="`${draftsId}-second`" ref="secondInput" v-model="secondDraft" />
    <label :for="`${draftsId}-target`">Observed draft</label>
    <select :id="`${draftsId}-target`" v-model="selectedTarget">
      <option value="first">First workshop draft</option>
      <option value="second">Second workshop draft</option>
    </select>
    <output aria-live="polite"
      >Listening to {{ targetName }} draft: {{ isListening ? "yes" : "no" }} · Observed input
      events: {{ observedEvents }}</output
    >
    <p>Last observed text: {{ observedText }}</p>
    <div class="example-actions">
      <button type="button" :disabled="!isListening" @click="() => stop()">
        Pause input observation
      </button>
      <button type="button" :disabled="isListening" @click="() => start()">
        Resume input observation
      </button>
    </div>
    <p>
      Both drafts remain editable. Only input events from the selected draft update the observation;
      pausing removes that listener and resuming attaches it to the current draft. This local
      monitor does not save or send draft text.
    </p>
  </div>
</template>

Runtime contract

Utility Category Stability SSR Hydration Cleanup Targets Host globals Uses
useEventListener events experimental safe stable caller, reactive-scope web, server, worker, native, desktop, terminal — tryOnScopeDispose

API

useEventListener

Attach an event listener to a reactive target and clean it up with the current reactive scope. Missing targets are valid during server rendering: a null/undefined target keeps the listener detached until a concrete target appears, so no browser globals are required. The listener is re-attached whenever the reactive target changes and is removed when the owning reactive scope stops, when EventListenerControls.stop is called, or when the abort signal fires. Outside an active scope, teardown ownership stays with the caller, who must call stop explicitly. Errors thrown by a custom target's add/remove methods propagate to the active watcher run.

function useEventListener<Key extends keyof WindowEventMap>( target: MaybeRefOrGetter<Window | null | undefined>, event: Key, listener: (event: WindowEventMap[Key]) => void, options?: UseEventListenerOptions, ): EventListenerControls

useEventListener

function useEventListener<Key extends keyof DocumentEventMap>( target: MaybeRefOrGetter<Document | null | undefined>, event: Key, listener: (event: DocumentEventMap[Key]) => void, options?: UseEventListenerOptions, ): EventListenerControls

useEventListener

function useEventListener<Key extends keyof HTMLElementEventMap>( target: MaybeRefOrGetter<HTMLElement | null | undefined>, event: Key, listener: (event: HTMLElementEventMap[Key]) => void, options?: UseEventListenerOptions, ): EventListenerControls

useEventListener

function useEventListener( target: MaybeRefOrGetter<EventTarget | null | undefined>, event: string, listener: EventListener, options?: UseEventListenerOptions, ): EventListenerControls

useEventListener

function useEventListener( target: MaybeRefOrGetter<EventTarget | null | undefined>, event: string, listener: EventListener, options: UseEventListenerOptions = {}, ): EventListenerControls

Types

UseEventListenerOptions

Options for useEventListener.

Member Type Description
capture? boolean Invoke the listener during the capture phase.
once? boolean Stop listening after the first event.
passive? boolean Declare that the listener does not cancel the event's default action.
signal? AbortSignal Stop listening when this signal is aborted. An already-aborted signal prevents listening from ever starting.
immediate? boolean Start listening during composable creation.
flush? "pre" | "post" | "sync" Reactive target update timing.

EventListenerControls

Reactive controls returned by useEventListener.

Member Type Description
isListening Readonly<Ref<boolean>> Whether a concrete target currently owns the listener.
start () => boolean Begin listening.
stop () => void Stop listening. Repeated calls are safe.