Single-page applications (SPA)
Even though validation has its given place on the server, it’s possible to use the whole Superforms library on the client in single page applications. A SPA is easy to create with SvelteKit, fully documented here.
Usage
const { form, enhance } = superForm(data, {
SPA: true | { failStatus: number }
validators: false | AnyZodObject | {
field: (value) => string | string[] | null | undefined;
}
})
By setting the SPA
option to true
, the form won’t be sent to the server when submitted. Instead, the client-side validators option will determine the success or failure of the form, which will trigger the event chain, and the validation result will be most conveniently consumed in the onUpdate event.
Using +page.ts instead of +page.server.ts
Since SPA pages don’t have a server representation, you can use +page.ts to load initial data. Combined with a route parameter, we can make a CRUD-like page in a straightforward manner:
src/routes/user/[id]/+page.ts
import { error } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms/client';
import { z } from 'zod';
export const _userSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(2),
email: z.string().email()
});
export const load = async ({ params, fetch }) => {
const id = parseInt(params.id);
const request = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
if (request.status >= 400) throw error(request.status);
const userData = await request.json();
const form = await superValidate(userData, _userSchema);
return { form };
};
Displaying the form
We display the form in +page.svelte
like before, but with the SPA
option added, and the onUpdate
event is used to validate the form data, instead of on the server:
src/routes/user/[id]/+page.svelte
<script lang="ts">
import type { PageData } from './$types';
import { superForm, setMessage, setError } from 'sveltekit-superforms/client';
import { _userSchema } from './+page';
export let data: PageData;
const { form, errors, message, constraints, enhance } = superForm(
data.form,
{
SPA: true,
validators: _userSchema,
onUpdate({ form }) {
// Form validation
if (form.data.email.includes('spam')) {
setError(form, 'email', 'Suspicious email address.');
} else if (form.valid) {
// TODO: Do something with the validated form.data
setMessage(form, 'Valid data!');
}
}
}
);
</script>
<h1>Edit user</h1>
{#if $message}<h3>{$message}</h3>{/if}
<form method="POST" use:enhance>
<label>
Name<br />
<input
aria-invalid={$errors.name ? 'true' : undefined}
bind:value={$form.name}
{...$constraints.name} />
</label>
{#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}
<label>
E-mail<br />
<input
type="email"
aria-invalid={$errors.email ? 'true' : undefined}
bind:value={$form.email}
{...$constraints.email} />
</label>
{#if $errors.email}<span class="invalid">{$errors.email}</span>{/if}
<button>Submit</button>
</form>
The validation in onUpdate
is almost the same as validating in a form action on the server. Nothing needs to be returned at the end since all modifications to form
will reflect in the view after the onUpdate
event is done.
Using superValidate in +page.svelte
Since you can’t use top-level await in Svelte components, you can’t use superValidate
directly in +page.svelte
, but you can import superValidateSync
instead to avoid having a +page.ts
if you just want the default values for a schema:
<script lang="ts">
import { superForm, superValidateSync } from 'sveltekit-superforms/client';
import { loginSchema } from '$lib/schemas';
const { form, errors, enhance } = superForm(
superValidateSync(loginSchema), {
SPA: true,
validators: loginSchema,
onUpdate({ form }) {
if (form.valid) {
// TODO: Do something with the validated form.data
}
}
}
);
</script>
Test it out
The following form has SPA: true
set, and is using +page.ts
for loading the initial data. Take a look in the browser devtools and see that nothing is posted to the server on submit.
Trimming down the bundle size
In the above example, we’re adding both Zod and the Superforms server part to the client. This adds about 65 Kb to the client output size, which hopefully is negligible, but if you’re hunting bytes, you can optimize by skipping the superValidate
part in +page.ts
, construct your own SuperValidated object, and use the Superforms validators in +page.svelte
:
src/lib/schemas.ts
import { z } from 'zod';
export const _userSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(2),
email: z.string().email()
});
export type UserSchema = typeof _userSchema;
src/routes/user/[id]/+page.ts
import type { SuperValidated } from 'sveltekit-superforms';
import type { UserSchema } from '$lib/schemas';
import { error } from '@sveltejs/kit';
export const load = async ({ params, fetch }) => {
const id = parseInt(params.id);
const request = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
if (request.status >= 400) throw error(request.status);
const user = await request.json();
// Validate data here if required
const form : SuperValidated<UserSchema> = {
valid: false, // Or true, depending on validation
posted: false,
data: user,
errors: {},
constraints: {}
}
return { form };
};
src/routes/user/[id]/+page.svelte
<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, {
SPA: true,
validators: {
name: (name) => (name.length <= 2 ? 'Name is too short' : null),
email: (email) =>
email.includes('spam') ? 'Suspicious email address.' : null
},
onUpdate({ form }) {
if (form.valid) {
form.message = 'Valid data!';
}
}
});
</script>
This will reduce the size to just the client-side part of Superforms, around 25 Kb. The downside is that you won’t get any constraints, the initial data won’t be validated, and you have to rely on the Superforms validator scheme, instead of the much more powerful Zod.