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.

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 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:

ElementBound propertyTrigger event
<input> (text, email, etc.)valueinput
<input type="number">valueAsNumberinput
<input type="checkbox">checkedchange
<input type="radio">checked / valuechange
<select>valuechange
<textarea>valueinput

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 number state receives valueAsNumber, with NaN falling back to 0.
  • A boolean state 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): boolean

Returns 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() sets checked on the radio whose value attribute 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.