#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.
#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:
| Modifier | Equivalent | Description |
|---|---|---|
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 | nullThe 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.
target as HTMLInputElement).HTMLInputElement.value: stringThe value property of the HTMLInputElement interface represents the current value of the element as a string.
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(): voidThe 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.
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 | undefinedA 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
addEventListenerunder the hood — there is no event delegation layer. - Selectors are evaluated with
querySelectorAllat 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
oncemodifier tracks fired listeners per entry. If the island re-renders before aoncelistener fires, the listener is still considered active and will not be re-attached.