This is the documentation for Superforms version 1. The latest version can be found at superforms.rocks!

Forms and fields in components

By looking at the rather simple get started tutorial, it’s obvious that quite a bit of boilerplate code adds up for a Superform:

<label for="name">Name</label>
<input
  type="text"
  name="name"
  aria-invalid={$errors.name ? 'true' : undefined}
  bind:value={$form.name}
  {...$constraints.name} 
/>
{#if $errors.name}
  <span class="invalid">{$errors.name}</span>
{/if}

And it also gets bad in the script part when you have more than a couple of forms on the page:

<script lang="ts">
  import type { PageData } from './$types';
  import { superForm } from 'sveltekit-superforms/client'

  export let data: PageData;

  const { form, errors, enhance, ... } = superForm(data.form);

  const {
    form: loginForm,
    errors: loginErrors,
    enhance: loginEnhance,
    ...
  } = superForm(data.loginForm);

  const {
    form: registerForm,
    errors: registerErrors,
    enhance: registerEnhance,
    ...
  } = superForm(data.registerForm);
</script>

This leads to the question of whether a form and its fields can be factored out into components?

Factoring out a form

To do this, you need the type of the schema, which can be defined as follows:

src/lib/schemas.ts

export const loginSchema = z.object({
  email: z.string().email(),
  password: ...
});

export type LoginSchema = typeof loginSchema;

Now you can import and use this type in a separate component:

src/routes/LoginForm.svelte

<script lang="ts">
  import type { SuperValidated } from 'sveltekit-superforms';
  import { superForm } from 'sveltekit-superforms/client'
  import type { LoginSchema } from '$lib/schemas';

  export let data: SuperValidated<LoginSchema>;

  const { form, errors, enhance, ... } = superForm(data);
</script>

<form method="POST" use:enhance>
  <!-- Business as usual -->
</form>

Use it by passing the form data from +page.svelte to the component, making it much less cluttered:

+page.svelte

<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<LoginForm data={data.loginForm} />
<RegisterForm data={data.registerForm} />

Factoring out form fields

Since bind is available on Svelte components, we can make a TextInput component quite easily:

TextInput.svelte

<script lang="ts">
  import type { InputConstraint } from 'sveltekit-superforms';

  export let value: string;
  export let label: string | undefined = undefined;
  export let errors: string[] | undefined = undefined;
  export let constraints: InputConstraint | undefined = undefined;
</script>

<label>
  {#if label}<span>{label}</span><br />{/if}
  <input
    type="text"
    bind:value
    aria-invalid={errors ? 'true' : undefined}
    {...constraints}
    {...$$restProps} 
  />
</label>
{#if errors}<span class="invalid">{errors}</span>{/if}

+page.svelte

<form method="POST" use:enhance>
  <TextInput
    name="name"
    label="name"
    bind:value={$form.name}
    errors={$errors.name}
    constraints={$constraints.name} 
  />

  <h4>Tags</h4>

  {#each $form.tags as _, i}
    <TextInput
      name="tags"
      label="Name"
      bind:value={$form.tags[i].name}
      errors={$errors.tags?.[i]?.name}
      constraints={$constraints.tags?.name} 
    />
  {/each}
</form>

(Note that you must bind directly to $form.tags with the index, you cannot use the each loop variable, hence the underscore.)

It’s a little bit better and will certainly help when the components require some styling, but there are still plenty of attributes. Can we do even better?

Using a fieldProxy

You may have used proxy objects for converting an input field string like "2023-04-12" into a Date, but that is just a special usage of proxies. They can actually be used for any part of the form data, to have a store that can modify a part of the $form store. If you want to update just $form.name, for example:

<script lang="ts">
  import type { PageData } from './$types';
  import { superForm, fieldProxy } from 'sveltekit-superforms/client';

  export let data: PageData;

  const { form } = superForm(data.form);

  const name = fieldProxy(form, 'name');
</script>

<div>Name: {$name}</div>
<button on:click={() => ($name = '')}>Clear name</button>

Any updates to $name will reflect in $form.name. Note that this will also taint that field, so if this is not intended, you may want to use the $tainted store and undefine its name field.

A fieldProxy isn’t enough here, however. We’d still have to make proxies for form, errors, and constraints, and then we’re back to the same problem again.

Using a formFieldProxy

The solution is to use a formFieldProxy, which is a helper function for producing the above proxies from a form. To do this, we cannot immediately deconstruct what we need from superForm since formFieldProxy takes the form itself as an argument:

<script lang="ts">
  import type { PageData } from './$types';
  import { superForm, formFieldProxy } from 'sveltekit-superforms/client';

  export let data: PageData;

  const form = superForm(data.form);

  const { path, value, errors, constraints } = formFieldProxy(form, 'name');
</script>

But we didn’t want to pass all those proxies, so let’s imagine a component that will handle even the above proxy creation for us.

<TextField {form} field="name" />

How nice would this be? This can actually be pulled off in a typesafe way with a bit of Svelte magic:

<script lang="ts" context="module">
  import type { AnyZodObject } from 'zod';
  type T = AnyZodObject;
</script>

<script lang="ts" generics="T extends AnyZodObject">
  import type { z } from 'zod';
  import type { ZodValidation, FormPathLeaves } from 'sveltekit-superforms';
  import { formFieldProxy, type SuperForm } from 'sveltekit-superforms/client';

  export let form: SuperForm<ZodValidation<T>, unknown>;
  export let field: FormPathLeaves<z.infer<T>>;

  const { value, errors, constraints } = formFieldProxy(form, field);
</script>

<label>
  {field}<br />
  <input
    name={field}
    type="text"
    aria-invalid={$errors ? 'true' : undefined}
    bind:value={$value}
    {...$constraints}
    {...$$restProps} />
</label>
{#if $errors}<span class="invalid">{$errors}</span>{/if}

The Svelte 4 syntax requires AnyZodObject to be defined before its used in the generics attribute, so we have to import it in a module context. Now when T is defined as AnyZodObject (the schema object type), we can use it in the form prop to ensure that only a SuperForm matching the field prop is used.

Type explanations

The ZodValidation<T> type is required because we can use refine/superRefine/transform on the schema object, which will wrap it in a ZodEffects type, so it’s not a AnyZodObject anymore. The ZodValidation type will unwrap the actual object, which may be several levels deep.

We also need the actual schema data, not the schema object itself. z.infer<T> is used for that. And it’s wrapped in FormPathLeaves, that produces the valid paths of an object as strings, so we can use nested data in a type-safe manner.

A minor issue: Checkboxes

A workaround is required for checkboxes, since they don’t bind with bind:value, rather with bind:checked, which requires a boolean.

Because our component is generic, value returned from formFieldProxy can’t be boolean specifically, so we need to make a specific checkbox component to use it, or cast it with a dynamic declaration:

<script lang="ts">
  import type { Writable } from 'svelte/store';
  // ... other imports and props

  const { value, errors, constraints } = formFieldProxy(form, field);

  $: boolValue = value as Writable<boolean>;
</script>

<input
  name={field}
  type="checkbox"
  class="checkbox"
  bind:checked={$boolValue}
  {...$constraints}
  {...$$restProps} />

Checkboxes, especially grouped ones, can be tricky to handle. Read the Svelte tutorial about bind:group, and see the Ice cream example on Stackblitz if you’re having trouble with it.

Using the componentized field in awesome ways

As said, using this component is now as simple as:

<TextField {form} field="name" />

But to show off some super proxy power, let’s recreate the tags example above with the TextField component:

<form method="POST" use:enhance>
  <TextField name="name" {form} field="name" />

  <h4>Tags</h4>

  {#each $form.tags as _, i}
    <TextField name="tags" {form} field="tags[{i}].name" />
  {/each}
</form>

We can now produce a text field for any object inside our data, which will update the $form store.

In general, nested data requires the dataType option to be set to 'json', but this example works without it, even without use:enhance, since arrays of primitive values are coerced automatically.

I hope you now feel under your fingers the superpowers that Superforms bring! 💥