Nested data
Basic HTML forms can only handle string values, and the <form>
element cannot nest other forms, so there is no standardized way to represent a nested data structure or more complex values like dates. Fortunately, Superforms has a few solutions for this!
Usage
const { form, enhance } = superForm(data.form, {
dataType: 'form' | 'json' = 'form'
})
dataType
By simply setting dataType
to 'json'
, you can store any data structure allowed by devalue in the form
store, and you don’t have to worry about failed coercion, converting strings to objects and arrays, etc.
You don’t even have to set a name on the form fields anymore, since the actual data in $form
is now posted, not the fields in the HTML. They are now simply UI components for modifying a data model (as they should be).
Modifying the form
store programmatically is easy, you can either assign $form.field = ...
directly, which will taint the affected fields. If you don’t want to taint anything, you can use update
with an extra option:
form.update(
($form) => {
$form.name = "New name";
return $form;
},
{ taint: false }
);
The taint
options are:
{ taint: boolean | 'untaint' | 'untaint-all' }
Which can be used to not only prevent tainting, but also untaint the modified field(s), or the whole form.
Requirements
The requirements for nested data to work are as follows:
- JavaScript is enabled in the browser.
- The form has the Superforms use:enhance applied.
Nested errors and constraints
When your schema contains arrays or objects, you can access them through $form
as an ordinary object. But how does it work with errors and constraints?
$errors
and $constraints
actually mirror the $form
data, but with every field or “leaf” in the object replaced with string[]
and InputConstraints
respectively.
Example
Given the following schema, which describes an array of tag objects:
import { z } from 'zod';
export const schema = z.object({
tags: z
.object({
id: z.number().int().min(1),
name: z.string().min(2)
})
.array()
});
const tags = [{ id: 1, name: 'test' }];
export const load = async () => {
const form = await superValidate({ tags }, schema);
return { form };
};
You can build up a HTML form for these tags using an {#each}
loop:
<script lang="ts">
const { form, errors, enhance } = superForm(data.form, {
dataType: 'json'
});
</script>
<form method="POST" use:enhance>
{#each $form.tags as _, i}
<div>
Id
<input
type="number"
data-invalid={$errors.tags?.[i]?.id}
bind:value={$form.tags[i].id} />
Name
<input
data-invalid={$errors.tags?.[i]?.name}
bind:value={$form.tags[i].name} />
{#if $errors.tags?.[i]?.id}
<br />
<span class="invalid">{$errors.tags[i].id}</span>
{/if}
{#if $errors.tags?.[i]?.name}
<br />
<span class="invalid">{$errors.tags[i].name}</span>
{/if}
</div>
{/each}
<button>Submit</button>
</form>
Note that we’re using the index of the loop, so the value can be bound directly to $form
. You can’t use the each loop variable, hence the underscore. This is what it looks like:
Arrays with primitive values
Since you can post multiple HTML elements with the same name, you could, but don’t have to use dataType: 'json'
for arrays of primitive values like numbers and strings. Just add the input fields, all with the same name as the schema field, which can only be at the top level of the schema, of course. Superforms will handle the type coercion to array automatically (just remember the name attribute):
export const schema = z.object({
tags: z.string().min(2).array().max(3)
});
<script lang="ts">
const { form, errors } = superForm(data.form);
</script>
<form method="POST">
<div>Tags</div>
{#if $errors.tags?._errors}
<div class="invalid">{$errors.tags._errors}</div>
{/if}
{#each $form.tags as _, i}
<div>
<input name="tags" bind:value={$form.tags[i]} />
{#if $errors.tags?.[i]}
<span class="invalid">{$errors.tags[i]}</span>
{/if}
</div>
{/each}
<button>Submit</button>
</form>
To summarize, the index i
of the #each
loop is used to access $form.tags
, where the current values are (you cannot use the loop variable), and then the name
attribute is set to the schema field tags
, so its array will be populated when posted.
This example, having a max(3)
limitation of the number of tags, also shows how to display array-level errors with the $errors.tags._errors
field.