Forms
Elur ships a full form module: createForm() for managed forms with
validation and submit handling, elurField() for standalone fields, and a set
of built-in validators. All field state is signal-based, so the UI updates
automatically as the user types.
# createForm()
createForm() takes an initial-values object and an options bag with
validators. It returns a FormState with reactive fields, computed validity,
and a handleSubmit wrapper:
import { createForm, required, email, minLength, html } from "@elurjs/core";
const form = createForm(
{ email: "", password: "" },
{
validators: {
email: [required(), email()],
password: [required(), minLength(8)],
},
validateOn: "blur",
}
);
html`
<form @submit=${form.handleSubmit(async (values) => {
await api.login(values);
})}>
<input
type="email"
value=${() => form.fields.email.value.value}
@input=${form.fields.email.onInput}
@blur=${form.fields.email.onBlur}
/>
${() => form.fields.email.error.value
? html`<p style="color:red">${() => form.fields.email.error.value}</p>`
: null}
<input
type="password"
value=${() => form.fields.password.value.value}
@input=${form.fields.password.onInput}
@blur=${form.fields.password.onBlur}
/>
${() => form.fields.password.error.value
? html`<p style="color:red">${() => form.fields.password.error.value}</p>`
: null}
<button disabled=${() => !form.canSubmit.value || form.isSubmitting.value}>
${() => form.isSubmitting.value ? "Submitting…" : "Log in"}
</button>
</form>
`.mount("#app");# FormState members
| Member | Type | Description |
|---|---|---|
fields |
Record<string, FieldState> |
Per-field state — value, error, event handlers. |
values |
Signal<T> |
Computed snapshot of all field values. |
errors |
Signal<FieldErrors> |
Computed map of currently visible errors. |
valid |
Signal<boolean> |
True when no visible errors (follows validateOn). |
canSubmit |
Signal<boolean> |
True when all validators pass, regardless of visibility. Bind submit buttons to this. |
dirty |
Signal<boolean> |
True when at least one field has been modified. |
touched |
Signal<boolean> |
True when at least one field has lost focus. |
isSubmitting |
Signal<boolean> |
True while the submit callback is running. |
submitCount |
Signal<number> |
Number of submit attempts (including failed validations). |
handleSubmit(fn) |
(fn) => (e: Event) => void |
Wraps a submit callback — prevents default, validates, manages isSubmitting. |
reset(newInitialValues?) |
void |
Reset all fields to initial values. |
setValue(path, value, opts?) |
void |
Set a field by dot-path (supports nested). |
setValues(values, opts?) |
void |
Set multiple fields at once. |
setErrors(errors) |
void |
Inject external errors (e.g., from a server response). |
dispose() |
void |
Dispose all internal computed signals. Call in onUnmount. |
# validateOn
Controls when validation errors become visible:
| Value | Errors appear |
|---|---|
"blur" (default) |
After the field loses focus at least once |
"input" |
As soon as the user types |
"submit" |
Only after the first submit attempt |
const form = createForm({ name: "" }, {
validators: { name: [required()] },
validateOn: "input", // show errors immediately as the user types
});# Built-in validators
| Validator | Description |
|---|---|
required(message?) |
Rejects null, "", and empty arrays |
minLength(n, message?) |
String must be at least n characters |
maxLength(n, message?) |
String must be at most n characters |
email(message?) |
Must match a basic email pattern |
pattern(regex, message?) |
Must match the given regex |
min(n, message?) |
Number must be ≥ n |
max(n, message?) |
Number must be ≤ n |
Validators return an error string when invalid, or null/undefined when
valid. They receive (value, allValues?) — the second argument enables
cross-field validation:
import { createForm, required, createValidator } from "@elurjs/core";
const form = createForm(
{ password: "", confirm: "" },
{
validators: {
password: [required(), minLength(8)],
confirm: [
required(),
createValidator((value, all) =>
value !== all?.password ? "Passwords do not match" : null
),
],
},
}
);# Custom validators
createValidator() is a typed wrapper that makes a function compatible with
elurField and createForm:
import { createValidator } from "@elurjs/core";
const even = createValidator<number>((n) =>
n % 2 !== 0 ? "Must be even" : null
);You can also extend the built-in validators namespace with extendValidators:
import { validators, extendValidators } from "@elurjs/core";
const myValidators = extendValidators(validators, {
even: () => (n: number) => (n % 2 !== 0 ? "Must be even" : null),
});# elurField() — standalone fields
For a single field outside a full form, use elurField():
import { elurField, required, html } from "@elurjs/core";
const name = elurField("", [required()]);
html`
<div>
<input
value=${() => name.value.value}
@input=${name.onInput}
@blur=${name.onBlur}
/>
${() => name.error.value
? html`<p style="color:red">${() => name.error.value}</p>`
: null}
</div>
`.mount("#app");Each FieldState exposes: value (read/write signal), error (visible
error), rawError (error ignoring visibility), touched, dirty, onInput,
onBlur, reset(), and setValue().
# elurFieldArray() — dynamic field lists
For forms with repeating groups (e.g., a list of contacts), use
elurFieldArray():
import { elurFieldArray, required, html, repeat } from "@elurjs/core";
const contacts = elurFieldArray(
[{ name: "" }],
{ name: [required()] }
);
html`
<ul>
${() => repeat(
contacts.fields.value,
(_, i) => i,
(group) => html`
<li>
<input
value=${() => group.name.value.value}
@input=${group.name.onInput}
@blur=${group.name.onBlur}
/>
${() => group.name.error.value
? html`<span style="color:red">${() => group.name.error.value}</span>`
: null}
</li>
`
)}
</ul>
<button @click=${() => contacts.append({ name: "" })}>Add contact</button>
`.mount("#app");elurFieldArray exposes: fields (signal of field groups), append(),
remove(i), move(from, to), replace(i, value), length (signal),
setValues(), patchValues(), and reset().
# Schema-level validation (Zod, Valibot, Yup)
Pass a validate function to run schema validation on submit, after built-in
validators pass. Return a field-to-error map, or null if valid:
import { createForm } from "@elurjs/core";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
age: z.number().min(18),
});
const form = createForm(
{ email: "", age: 0 },
{
validate(values) {
const r = schema.safeParse(values);
if (r.success) return null;
return Object.fromEntries(
Object.entries(r.error.flatten().fieldErrors)
.map(([k, v]) => [k, v?.[0]])
);
},
}
);# Checkbox and select binding
elurField auto-coerces values based on the initial value type. For booleans
(checkboxes) and numbers, it handles the conversion automatically:
import { elurField, html } from "@elurjs/core";
const agree = elurField(false); // boolean → reads .checked
const age = elurField(0); // number → parses to Number
const color = elurField("blue"); // string → reads .value
html`
<label>
<input type="checkbox" checked=${() => agree.value.value} @input=${agree.onInput} />
I agree
</label>
<input type="number" value=${() => age.value.value} @input=${age.onInput} />
<select value=${() => color.value.value} @input=${color.onInput}>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
`.mount("#app");Bind submit buttons to canSubmit (not valid) — canSubmit reflects whether
all validators pass regardless of error visibility, so a pristine form with
empty required fields correctly starts disabled.
# Types
# Validator<T, AllValues>
type Validator<T, AllValues = unknown> = (
value: T,
allValues?: AllValues,
) => string | null | undefined;Returns an error string when invalid, or null/undefined when valid.
# ValidateOn
Controls when validation errors become visible:
type ValidateOn = "blur" | "input" | "submit";"blur"— after the field loses focus (default)"input"— as soon as the user types"submit"— only after the first submit attempt
# ValidatorsBase
type ValidatorsBase = typeof validators;The type of the built-in validators object. Used with extendValidators to
get typed custom validator namespaces.
# DeepPartial<T>
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;Used by FormState.patch() and FormState.setValues() for partial updates.
# FieldState<T>
| Field | Type | Description |
|---|---|---|
value |
Signal<T> |
Current value (read/write) |
error |
Signal<string | null> |
Visible error (follows validateOn) |
rawError |
Signal<string | null> |
Real error ignoring visibility rules |
touched |
Signal<boolean> |
True after first blur |
dirty |
Signal<boolean> |
True after first input |
onInput |
(e: Event) => void |
Attach to @input |
onBlur |
() => void |
Attach to @blur |
reset() |
() => void |
Reset to initial value and clear state |
setValue(value, options?) |
(value: T, options?) => void |
Set value programmatically |
# FieldArrayState<T>
| Field | Type | Description |
|---|---|---|
fields |
Signal<Array<{ [K]: FieldState<T[K]> }>> |
Reactive list of field groups |
length |
Signal<number> |
Number of items (reactive) |
append(value) |
(value: T) => void |
Add item to end |
remove(index) |
(index: number) => void |
Remove item at index |
move(from, to) |
(from: number, to: number) => void |
Move item between indices |
replace(index, value) |
(index: number, value: T) => void |
Replace item at index |
setValues(items) |
(items: T[]) => void |
Replace entire array |
patchValues(items) |
(items: Partial<T>[]) => void |
Patch existing items |
reset(items?) |
(items?: T[]) => void |
Reset to initial or new values |
# FieldErrors<T>
type FieldErrors<T> = { [K in keyof T]?: string | null } & Record<string, string | null | undefined>;# FormState<T>
| Field | Type | Description |
|---|---|---|
fields |
FormFields<T> |
Individual field states |
values |
Signal<T> |
Computed snapshot of all values |
errors |
Signal<FieldErrors<T>> |
Computed visible errors |
valid |
Signal<boolean> |
True when no visible errors |
canSubmit |
Signal<boolean> |
True when all validators pass (regardless of visibility) |
isSubmitting |
Signal<boolean> |
True during async submit |
submit(handler) |
(handler) => Promise<void> |
Run validators + handler |
setErrors(errors) |
(errors: FieldErrors<T>) => void |
Inject server/schema errors |
reset() |
() => void |
Reset all fields |
patch(values) |
(values: DeepPartial<T>) => void |
Partial update |
dispose() |
() => void |
Clean up signals |
# FormOptions<T>
| Field | Type | Default | Description |
|---|---|---|---|
validators |
FormValidators<T>? |
— | Per-field validators |
validateOn |
ValidateOn? |
"blur" |
When errors become visible |
validate |
(values: T) => FieldErrors<T> | null? |
— | Schema-level validator (runs on submit) |
initialValues |
DeepPartial<T>? |
— | Initial values for fields |