#Bind
Two-way binds a form element to a state key or external signal. When the state changes, the element updates. When the user interacts with the element, the state updates.
#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 Form: Island<Record<string, unknown>, MergeState<Record<string, never>, "name", string>> 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<string, "name">(key: "name", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "name", string>, Record<string, never>> state ("name", "")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "name", string>, Record<string, never>>.bind(selector: string, stateKey: string): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "name", string>, Record<string, never>> (+1 overload) bind ("input.name", "name")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "name", string>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<Record<string, never>, "name", string>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<Record<string, never>, "name", string>> render (
({ state: IslandState<MergeState<Record<string, never>, "name", string>> state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `
<form>
<input class="name" />
<p>Hello, ${state: IslandState<MergeState<Record<string, never>, "name", string>> state .name: MarkedSignalAccessor
() => string (+1 overload)
name ()}!</p>
</form>
`,
);#Supported elements
.bind() handles the correct property and event for each element type automatically:
| Element | Bound property | Trigger event |
|---|---|---|
<input> (text, email, etc.) | value | input |
<input type="number"> | valueAsNumber | input |
<input type="checkbox"> | checked | change |
<input type="radio"> | checked / value | change |
<select> | value | change |
<textarea> | value | input |
No configuration needed — the element type is detected at runtime.
#Multiple bindings
Chain .bind() for each form element:
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 Settings: Island<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>> Settings = 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, "username">(key: "username", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "username", string>, Record<string, never>> state ("username", "")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "username", string>, Record<string, never>>.state<boolean, "notifications">(key: "notifications", init?: StateInit<Record<string, unknown>, boolean> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, Record<string, never>> state ("notifications", true)
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, Record<string, never>>.state<string, "role">(key: "role", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<string, never>> state ("role", "viewer")
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<...>>.bind(selector: string, stateKey: string): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<string, never>> (+1 overload) bind ("input.username", "username")
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<...>>.bind(selector: string, stateKey: string): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<string, never>> (+1 overload) bind ("input[type=checkbox]", "notifications")
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<...>>.bind(selector: string, stateKey: string): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<string, never>> (+1 overload) bind ("select.role", "role")
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<...>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>> render (
({ state: IslandState<MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>> state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `
<form>
<input class="username" />
<input type="checkbox" />
<select class="role">
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
<p>${state: IslandState<MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>> state .username: SignalAccessor<string> username } · ${state: IslandState<MergeState<MergeState<MergeState<Record<string, never>, "username", string>, "notifications", boolean>, "role", string>> state .role: SignalAccessor<string> role }</p>
</form>
`,
);#Type coercion
.bind() reads the current state value to determine the expected type and coerces the element's raw string output accordingly:
- A
numberstate receivesvalueAsNumber, withNaNfalling back to0. - A
booleanstate receives a boolean coercion of the element's value. - Everything else is treated as a string.
This means you rarely need to parse or cast values manually in your handlers.
#Binding to an external signal
Instead of a state key, you can pass a signal created with context(). This is useful when multiple islands need to share the same form value:
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 , const context: <T>(key: string, initial: T) => ContextSignal<T> context } from "ilha";
const const theme: ContextSignal<string> theme = context<string>(key: string, initial: string): ContextSignal<string> context ("app.theme", "light");
const const ThemePicker: Island<Record<string, unknown>, Record<string, never>> ThemePicker = 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>>.bind<string>(selector: string, externalSignal: ExternalSignal<string>): IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> (+1 overload) bind ("select", const theme: ContextSignal<string> theme ).IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, Record<string, never>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, Record<string, never>> render (
() => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `
<select>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
`,
);#Binding the host element
Pass an empty string as the selector to bind the island host element itself:
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>>.bind(selector: string, stateKey: string): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "value", string>, Record<string, never>> (+1 overload) bind ("", "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 ()}" />`);#Combining with .on()
.bind() handles value synchronization. If you also need to react to the same change — for example to trigger validation — combine it with .on():
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 EmailForm: Island<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> EmailForm = 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, "email">(key: "email", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "email", string>, Record<string, never>> state ("email", "")
.IlhaBuilder<Record<string, unknown>, MergeState<Record<string, never>, "email", string>, Record<string, never>>.state<string, "error">(key: "error", init?: StateInit<Record<string, unknown>, string> | undefined): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, Record<string, never>> state ("error", "")
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, Record<string, never>>.bind(selector: string, stateKey: string): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, Record<string, never>> (+1 overload) bind ("input", "email")
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, Record<string, never>>.on<"input@input">(selectorOrCombined: "input@input", handler: (ctx: HandlerContextFor<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, "input", Record<string, never>>) => void | Promise<void>): IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, Record<string, never>> (+1 overload) on ("input@input", ({ state: IslandState<MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> state }) => {
const const valid: boolean valid = state: IslandState<MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> state .email: MarkedSignalAccessor
() => string (+1 overload)
email ().String.includes(searchString: string, position?: number): booleanReturns true if searchString appears as a substring of the result of converting this
object to a String, at one or more positions that are
greater than or equal to position; otherwise, returns false.
@paramsearchString search string@paramposition If position is undefined, 0 is assumed, so as to search all of the String. includes ("@");
state: IslandState<MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> state .error: MarkedSignalAccessor
(value: string) => void (+1 overload)
error (const valid: boolean valid ? "" : "Enter a valid email");
})
.IlhaBuilder<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, Record<string, never>>.render(fn: (ctx: RenderContext<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>, Record<string, never>>) => string | RawHtml): Island<Record<string, unknown>, MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> render (
({ state: IslandState<MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> state }) => const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `
<input type="email" />
${state: IslandState<MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> state .error: MarkedSignalAccessor
() => string (+1 overload)
error () ? const html: (strings: TemplateStringsArray, ...values: unknown[]) => RawHtml html `<p>${state: IslandState<MergeState<MergeState<Record<string, never>, "email", string>, "error", string>> state .error: SignalAccessor<string> error }</p>` : ""}
`,
);#Dev mode warnings
In development, if the selector matches no elements at mount time, ilha logs a warning. Check that the element exists in your render output and that the selector is correct.
#Notes
.bind()initializes the element's value from state on mount, so the element always reflects the current state on first render.- For radio inputs,
.bind()setscheckedon the radio whosevalueattribute matches the current state value. Writing a new value to state checks the matching radio automatically. .bind()does not replace.on()— it only handles value synchronization. Use.on()for anything beyond reading and writing the element's value.