On

Attaches a DOM event listener to the island host or any descendant element. Listeners are set up at mount time and cleaned up automatically on unmount.

Interactive Tutorial

Basic usage

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
, {
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
} from "ilha";
const
const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>
Counter
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>
state
("count", 0)
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.on<"button@click">(selectorOrCombined: "button@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, "click", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> (+1 overload)
on
("button@click", ({
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
}) =>
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count
(
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
() => number (+1 overload)
count
() + 1))
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>
render
(
({
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
}) =>
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`
<div> <p>Count: ${
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}</p>
<button>Increment</button> </div> `, );

Selector syntax

The first argument combines a CSS selector and an event name using @ as a separator:

"cssSelector@eventName"

Omit the selector to target the island host element itself:

.on("@click", handler)              // host click
.on("button@click", handler)        // any <button> inside the island
.on("input.search@input", handler)  // input with class "search"
.on("#submit@click", handler)       // element with id="submit"

Event modifiers

Append modifiers after the event name with : as a separator:

ModifierEquivalentDescription
once{ once: true }Listener fires only once, then removes itself
capture{ capture: true }Listens in the capture phase
passive{ passive: true }Hints the browser this handler won't call preventDefault
.on("button@click:once", handler)
.on("@scroll:passive", handler)
.on("button@click:once:capture", handler)

Multiple modifiers can be combined in any order.

Handler context

The handler receives a HandlerContext with everything needed to respond to the event:

{
  state: IslandState; // reactive state signals
  derived: IslandDerived; // current derived values
  input: TInput; // resolved input props
  host: Element; // island root element
  target: Element; // element that fired the event
  event: Event; // the native DOM event
}

Both target and event are typed when the event name is a known HTML event:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
from "ilha";
const
const Island: Island<Record<string, unknown>, MergeState<Record<string, never>, "value", string>>
Island
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<string, "value">(key: "value", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "value", string>, Record<string, never>>
state
("value", "")
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "value", string>, Record<string, never>>.on<"input@input">(selectorOrCombined: "input@input", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "value", string>, "input", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "value", string>, Record<string, never>> (+1 overload)
on
("input@input", ({
state: IslandState<MergeState<Record<string, never>, "value", string>>
state
,
event: InputEvent
event
}) => {
state: IslandState<MergeState<Record<string, never>, "value", string>>
state
.
value: MarkedSignalAccessor
(value: string) => void (+1 overload)
value
((
event: InputEvent
event
.
Event.target: EventTarget | null

The read-only target property of the Event interface is a reference to the object onto which the event was dispatched. It is different from Event.currentTarget when the event handler is called during the bubbling or capturing phase of the event.

MDN Reference

target
as HTMLInputElement).
HTMLInputElement.value: string

The value property of the HTMLInputElement interface represents the current value of the element as a string.

MDN Reference

value
);
}) .
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "value", string>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "value", string>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "value", string>>
render
(({
state: IslandState<MergeState<Record<string, never>, "value", string>>
state
}) => `<input value="${
state: IslandState<MergeState<Record<string, never>, "value", string>>
state
.
value: MarkedSignalAccessor
() => string (+1 overload)
value
()}" />`);

Async handlers

Handlers can be async. Errors are not caught automatically, so handle them explicitly:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
, {
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
} from "ilha";
const
const Form: Island<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>>
Form
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<boolean, "loading">(key: "loading", init?: StateInit<Record<string, unknown>, boolean> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>, Record<string, never>>
state
("loading", false)
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>, Record<string, never>>.on<"form@submit">(selectorOrCombined: "form@submit", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>, "submit", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>, Record<string, never>> (+1 overload)
on
("form@submit", async ({
state: IslandState<MergeState<Record<string, never>, "loading", boolean>>
state
,
event: SubmitEvent
event
}) => {
event: SubmitEvent
event
.
Event.preventDefault(): void

The preventDefault() method of the Event interface tells the user agent that the event is being explicitly handled, so its default action, such as page scrolling, link navigation, or pasting text, should not be taken.

MDN Reference

preventDefault
();
state: IslandState<MergeState<Record<string, never>, "loading", boolean>>
state
.
loading: MarkedSignalAccessor
(value: boolean) => void (+1 overload)
loading
(true);
try { await
function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
fetch
("/api/submit", {
RequestInit.method?: string | undefined

A string to set request's method.

method
: "POST" });
} finally {
state: IslandState<MergeState<Record<string, never>, "loading", boolean>>
state
.
loading: MarkedSignalAccessor
(value: boolean) => void (+1 overload)
loading
(false);
} }) .
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "loading", boolean>>
render
(
({
state: IslandState<MergeState<Record<string, never>, "loading", boolean>>
state
}) =>
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`
<form> <button type="submit" disabled="${
state: IslandState<MergeState<Record<string, never>, "loading", boolean>>
state
.
loading: MarkedSignalAccessor
() => boolean (+1 overload)
loading
()}">
${
state: IslandState<MergeState<Record<string, never>, "loading", boolean>>
state
.
loading: MarkedSignalAccessor
() => boolean (+1 overload)
loading
() ? "Submitting…" : "Submit"}
</button> </form> `, );

Multiple listeners

Chain .on() as many times as needed. Each call adds an independent listener:

import 
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
, {
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
} from "ilha";
const
const Counter: Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>
Counter
=
const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
    html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml;
    raw: (value: string) => RawHtml;
    mount: (registry: IslandRegistry, options?: MountOptions) => MountResult;
    from: <TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>) => (() => void) | null;
    context: <T>(key: string, initial: T) => ContextSignal<...>;
}
ilha
.
IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.state<number, "count">(key: "count", init?: StateInit<Record<string, unknown>, number> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>
state
("count", 0)
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.on<"[data-action=increment]@click">(selectorOrCombined: "[data-action=increment]@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, "click", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> (+1 overload)
on
("[data-action=increment]@click", ({
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
}) =>
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count
(
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
() => number (+1 overload)
count
() + 1))
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.on<"[data-action=decrement]@click">(selectorOrCombined: "[data-action=decrement]@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, "click", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> (+1 overload)
on
("[data-action=decrement]@click", ({
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
}) =>
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count
(
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
() => number (+1 overload)
count
() - 1))
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.on<"[data-action=reset]@click">(selectorOrCombined: "[data-action=reset]@click", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, "click", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>> (+1 overload)
on
("[data-action=reset]@click", ({
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
}) =>
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
(value: number) => void (+1 overload)
count
(0))
.
IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "count", number>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "count", number>>
render
(
({
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
}) =>
const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml
html
`
<div> <p>${
state: IslandState<MergeState<Record<string, never>, "count", number>>
state
.
count: MarkedSignalAccessor
() => number (+1 overload)
count
()}</p>
<button data-action="increment">+</button> <button data-action="decrement">−</button> <button data-action="reset">Reset</button> </div> `, );

Dev mode warnings

In development, if a selector matches no elements at mount time, ilha logs a warning. This is not an error — the element may not exist yet if it is rendered conditionally. The warning is suppressed in production.

Notes

  • Listeners are attached to the island host and use standard addEventListener under the hood — there is no event delegation layer.
  • Selectors are evaluated with querySelectorAll at mount time and after each re-render. If new matching elements appear after mount, they are picked up automatically on the next re-render cycle.
  • The once modifier tracks fired listeners per entry. If the island re-renders before a once listener fires, the listener is still considered active and will not be re-attached.