Skip to content
CodeAtlas UI

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
svelte
<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.

3–20 characters.

0/160

valid: false

Code
svelte
<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
svelte
<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>

useForm(initialValues, options?)

PropTypeDefaultDescription
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.

Returned form state

PropTypeDefaultDescription
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.