Multiple forms on the same page
Since there is only one $page
store per route, multiple forms on the same page, like a register and login form, can cause problems since both form actions will update $page.form
, possibly affecting the other form.
With Superforms, multiple forms on the same page are handled automatically if you are using use:enhance
, and the forms have different schema types. When using the same schema for multiple forms, you need to set the id
option:
const form = await superValidate(zod(schema), {
id: string | undefined
});
By setting an id on the server, you’ll ensure that only forms with the matching id on the client will react to the updates.
Here’s an example of how to handle a login and registration form on the same page:
+page.server.ts
import { z } from 'zod';
import { fail } from '@sveltejs/kit';
import { message, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import type { Actions, PageServerLoad } from './$types';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string().min(8)
});
export const load: PageServerLoad = async () => {
// Different schemas, no id required.
const loginForm = await superValidate(zod(loginSchema));
const registerForm = await superValidate(zod(registerSchema));
// Return them both
return { loginForm, registerForm };
};
export const actions = {
login: async ({ request }) => {
const loginForm = await superValidate(request, zod(loginSchema));
if (!loginForm.valid) return fail(400, { loginForm });
// TODO: Login user
return message(loginForm, 'Login form submitted');
},
register: async ({ request }) => {
const registerForm = await superValidate(request, zod(registerSchema));
if (!registerForm.valid) return fail(400, { registerForm });
// TODO: Register user
return message(registerForm, 'Register form submitted');
}
} satisfies Actions;
The code above uses named form actions to determine which form was posted. On the client, you’ll post to these different form actions for the respective form:
+page.svelte
<script lang="ts">
import { superForm } from 'sveltekit-superforms/client';
let { data } = $props();
const { form, errors, enhance, message } = superForm(data.loginForm, {
resetForm: true
});
const {
form: registerForm,
errors: registerErrors,
enhance: registerEnhance,
message: registerMessage
} = superForm(data.registerForm, {
resetForm: true
});
</script>
{#if $message}<h3>{$message}</h3>{/if}
<form method="POST" action="?/login" use:enhance>
E-mail: <input name="email" type="email" bind:value={$form.email} />
Password:
<input name="password" type="password" bind:value={$form.password} />
<button>Submit</button>
</form>
<hr />
{#if $registerMessage}<h3>{$registerMessage}</h3>{/if}
<form method="POST" action="?/register" use:registerEnhance>
E-mail: <input name="email" type="email" bind:value={$registerForm.email} />
Password:
<input name="password" type="password" bind:value={$registerForm.password} />
Confirm password:
<input
name="confirmPassword"
type="password"
bind:value={$registerForm.confirmPassword} />
<button>Submit</button>
</form>
The above works well with forms that posts to a dedicated form action. But for more dynamic scenarios, let’s say a database table where rows can be edited, the form id should correspond to the row id, and you’d want to communicate to the server which id was sent. This can be done by modifying the $formId
store, to let the server know what id
was posted, and what it should respond with.
Setting id on the client
On the client, the id is picked up automatically when you pass data.form
to superForm
, so in general, you don’t have to add it on the client.
// Default behavior: The id is sent along with the form data
// sent from the load function.
const { form, enhance, formId } = superForm(data.loginForm);
// In advanced cases, you can set the id as an option
// and it will take precedence.
const { form, enhance, formId } = superForm(data.form, {
id: 'custom-id'
});
You can also change the id dynamically with the $formId
store, or set it directly in the form with the following method:
Without use:enhance
Multiple forms also work without use:enhance
, though in this case you must add a hidden form field called __superform_id
with the $formId
value:
<script lang="ts">
import { superForm } from 'sveltekit-superforms/client';
let { data } = $props();
const { form, errors, formId } = superForm(data.form);
</script>
<form method="POST" action="?/login">
<input type="hidden" name="__superform_id" bind:value={$formId} />
</form>
This is also required if you’re changing schemas in a form action, as can happen in multi-step forms.
Returning multiple forms
If you have a use case where the data in one form should update another, you can return both forms in the form action: return { loginForm, registerForm }
, but be aware that you may need resetForm: false
on the second form, as it will reset and clear the updated changes, if it’s valid and a successful response is returned.
Hidden forms
Sometimes you want a fetch function for a form field or a list of items, for example checking if a username is valid while entering it, or deleting rows in a list of data. Instead of doing this manually with fetch, which cannot take advantage of Superforms’ loading timers, events and other functionality, you can create a hidden form that does most of the work, with the convenience you get from superForm
:
// First the visible form
const { form, errors, ... } = superForm(...);
// The the hidden form
const { submitting, submit } = superForm(
{ username: '' },
{
invalidateAll: false,
applyAction: false,
multipleSubmits: 'abort',
onSubmit({ cancel, formData }) {
// Using the visible form data
if (!$form.username) cancel();
formData.set('username', $form.username);
},
onUpdated({ form }) {
// Update the other form to show the error message
$errors.username = form.errors.username;
}
}
);
const checkUsername = debounce(300, submit);
Create a form action for it:
const usernameSchema = fullSchema.pick({ username: true });
export const actions: Actions = {
check: async ({ request }) => {
const form = await superValidate(request, zod(usernameSchema));
if (!form.valid) return fail(400, { form });
if(!checkUsername(form.data.username)) {
setError(form, 'username', 'Username is already taken.');
}
return { form };
}
};
And finally, an on:input
event on the input field:
<input
name="username"
aria-invalid={$errors.username ? 'true' : undefined}
bind:value={$form.username}
on:input={checkUsername}
/>
{#if $submitting}<img src={spinner} alt="Checking availability" />
{:else if $errors.username}<div class="invalid">{$errors.username}</div>{/if}
A full example of a username check is available on SvelteLab.
Configuration and troubleshooting
Due to the many different use cases, it’s hard to set sensible default options for multiple forms. A common issue is that when one form is submitted, the other forms’ data are lost. This is due to the page being invalidated by default on a successful response.
If you want to preserve their data, you’d almost certainly want to set invalidateAll: false
or applyAction: false
on them, as in the example above. The use:enhance option explains the differences between them.
Also check out the componentization page for ideas on how to place the forms into separate components, to make +page.svelte
less cluttered.
Test it out
Here we have two forms, with separate id’s and their invalidateAll
option set to false
, to prevent page invalidation, which would otherwise clear the other form when the load function executes again.