Inputs
useForm Reactive form state with optional schema validation. Works with Zod, Valibot or a plain validator function.
< script >
import { TextField, useForm } from "@codeatlasdev/ui" ;
const form = useForm (
{ email: "" },
{ schema : ( data ) => (data.email. includes ( "@" ) ? {} : { email: "Enter a valid email." }) },
);
</ script >
< TextField
label = "Email"
value = { form.fields.email.value }
error = { form.fields.email.error }
oninput = { ( v ) => form. set ( "email" , v) }
onblur = { () => form. touch ( "email" ) }
/>
useForm has no dependencies. The schema option accepts a plain function that returns an object of field errors, a Zod schema (safeParse) or a Valibot schema. Every field value is a string. Errors only appear after a field is touched (on blur) or after validate(), and then update on each keystroke.
Pass arrays for repeatable row groups, for example useForm({ name: "" }, { arrays: { phones: [{ number: "" }] } }), and use form.arrays.phones.add, remove and move.
No schema Manual validation on submit. touch() and set() drive the per-field state.
Code <script lang="ts">
import { Button, TextField, useForm } from '@codeatlasdev/ui';
const form = useForm({ name: '', email: '' });
function validate() {
const errs: Partial<Record<'name' | 'email', string>> = {};
if (!form.fields.name.value.trim()) errs.name = 'Name is required.';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.fields.email.value)) errs.email = 'Enter a valid email address.';
for (const k of ['name', 'email'] as const) {
form.fields[k].touched = true;
form.fields[k].error = errs[k];
}
return Object.keys(errs).length === 0;
}
</script>
<div class="col">
<TextField
label="Name"
placeholder="Jane Smith"
value={form.fields.name.value}
error={form.fields.name.error}
oninput={(v) => form.set('name', v)}
onblur={() => form.touch('name')}
/>
<TextField
label="Email"
type="email"
placeholder="you@example.com"
value={form.fields.email.value}
error={form.fields.email.error}
oninput={(v) => form.set('email', v)}
onblur={() => form.touch('email')}
/>
<div class="row">
<Button onclick={validate}>Validate</Button>
<Button variant="outlined" onclick={() => form.reset()}>Reset</Button>
</div>
{#if form.dirty}
<p class="hint">dirty: values changed from initial</p>
{/if}
</div>
<style>
.col {
display: flex;
flex-direction: column;
gap: var(--ca-space-4);
width: 100%;
max-width: 420px;
}
.row {
display: flex;
gap: var(--ca-space-2);
}
.hint {
margin: 0;
font-size: var(--ca-font-size-caption);
color: var(--ca-text-secondary);
}
</style>
With schema A plain-function schema runs on every keystroke once a field is touched.
Validate all Reset
valid: false
Code <script lang="ts">
import { Button, TextArea, TextField, useForm } from '@codeatlasdev/ui';
const form = useForm(
{ username: '', bio: '' },
{
schema: (data) => {
const errs: Partial<Record<'username' | 'bio', string>> = {};
if (data.username.length < 3) errs.username = 'At least 3 characters.';
if (data.username.length > 20) errs.username = 'Max 20 characters.';
if (data.bio.length > 160) errs.bio = 'Bio must be 160 characters or fewer.';
return errs;
},
},
);
</script>
<div class="col">
<TextField
label="Username"
placeholder="john_doe"
value={form.fields.username.value}
error={form.fields.username.error}
description="3–20 characters."
oninput={(v) => form.set('username', v)}
onblur={() => form.touch('username')}
/>
<TextArea
label="Bio"
placeholder="Tell us about yourself…"
value={form.fields.bio.value}
error={form.fields.bio.error}
description="{form.fields.bio.value.length}/160"
oninput={(v) => form.set('bio', v)}
onblur={() => form.touch('bio')}
/>
<div class="row">
<Button onclick={() => form.validate()}>Validate all</Button>
<Button variant="outlined" onclick={() => form.reset()}>Reset</Button>
</div>
<p class="hint">valid: {form.valid}</p>
</div>
<style>
.col {
display: flex;
flex-direction: column;
gap: var(--ca-space-4);
width: 100%;
max-width: 420px;
}
.row {
display: flex;
gap: var(--ca-space-2);
}
.hint {
margin: 0;
font-size: var(--ca-font-size-caption);
color: var(--ca-text-secondary);
}
</style>
Submit state validate() gates submission, submitting drives the button loading state, and reset() clears everything after success.
Code <script lang="ts">
import { Button, TextArea, TextField, useForm } from '@codeatlasdev/ui';
const form = useForm(
{ title: '', notes: '' },
{
schema: (data) => {
const errs: Partial<Record<'title' | 'notes', string>> = {};
if (!data.title.trim()) errs.title = 'Title is required.';
return errs;
},
},
);
let result = $state('');
async function submit() {
if (!form.validate()) return;
form.submitting = true;
result = '';
await new Promise((r) => setTimeout(r, 1200));
form.submitting = false;
result = `Submitted: "${form.fields.title.value}"`;
form.reset();
}
</script>
<div class="col">
<TextField
label="Title"
placeholder="My item title"
value={form.fields.title.value}
error={form.fields.title.error}
oninput={(v) => form.set('title', v)}
onblur={() => form.touch('title')}
disabled={form.submitting}
/>
<TextArea
label="Notes"
placeholder="Optional notes…"
value={form.fields.notes.value}
error={form.fields.notes.error}
oninput={(v) => form.set('notes', v)}
onblur={() => form.touch('notes')}
disabled={form.submitting}
/>
<div class="row">
<Button onclick={submit} loading={form.submitting}>
{form.submitting ? 'Saving…' : 'Submit'}
</Button>
<Button
variant="outlined"
onclick={() => {
form.reset();
result = '';
}}
>
Reset
</Button>
</div>
{#if result}
<p class="hint hint--success" role="status">{result}</p>
{/if}
</div>
<style>
.col {
display: flex;
flex-direction: column;
gap: var(--ca-space-4);
width: 100%;
max-width: 420px;
}
.row {
display: flex;
gap: var(--ca-space-2);
}
.hint {
margin: 0;
font-size: var(--ca-font-size-caption);
color: var(--ca-text-secondary);
}
.hint--success {
color: var(--ca-brand-text);
}
</style>
Prop Type Default Description initialValuesRecord<K, string>– Field names and their initial string values. options.schemaValidationSchema<K>– A function (data) => Partial<Record<K, string>>, or a Zod or Valibot schema. options.arrays{ [name]: Item[] }– Initial rows for repeatable field groups, exposed as form.arrays.
Prop Type Default Description fieldsRecord<K, { value: string; error: string | undefined; touched: boolean }>– Reactive state of every field. arrays{ [name]: { items; add(item); remove(index); move(from, to) } }– Repeatable row groups declared with options.arrays. dirtyboolean– True when any value differs from its initial value. validboolean– True when the schema reports no errors, regardless of touched state. submittingboolean– Writable flag for in-flight submits. touch(field: K) => void– Marks a field as touched and validates it. set(field: K, value: string) => void– Sets a value, and re-validates the field if it was touched. validate() => boolean– Validates all fields, marks them touched and returns true when all pass. reset() => void– Restores initial values and clears errors, touched state and submitting.