Superforms v2 - Next version
The next major version of Superforms is now available in an alpha version! It’s a huge upgrade, because it now has the potential to support virtually every validation library out there.
Not only that, the client validation part has been rewritten to be much more efficient. File uploads are now supported. And of course, Zod is still perfectly usable with just a small modification to the code.
Test it out!
Install the v2 version with this command:
pnpm i -D sveltekit-superforms@alpha
Missing a library? No problem, writing new adapters is super-simple. Let me know on Discord or Twitter.
Migration and getting started
The headlines show what has changed, so look for them and make the necessary changes in the code.
Changes
The biggest change (IMPORTANT)
The biggest breaking change is that the options now follow the SvelteKit defaults more closely:
- resetForm is now
true
as default - taintedMessage is now
false
as default
But don’t worry, there’s no need to change the options on every form to migrate. Instead, add the following define in vite.config.ts
to keep the original behavior:
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
},
+ define: {
+ SUPERFORMS_LEGACY: true
+ }
});
You can do the same on a form-by-form basis by setting the legacy
option on superForm
to true
as well.
superValidate
Instead of a Zod schema, you now use an adapter for your favorite validation library. The following are currently supported:
Library | Adapter | Requires defaults |
---|---|---|
Arktype | import { arktype } from 'sveltekit-superforms/adapters' |
Yes |
Joi | import { joi } from 'sveltekit-superforms/adapters' |
No |
TypeBox | import { typebox } from 'sveltekit-superforms/adapters' |
No |
Valibot | import { valibot } from 'sveltekit-superforms/adapters' |
No |
Yup | import { yup } from 'sveltekit-superforms/adapters' |
No |
Zod | import { zod } from 'sveltekit-superforms/adapters' |
No |
With the library installed and the adapter imported, all you need to do is wrap the schema with it:
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
const form = await superValidate(zod(schema));
The libraries in the list that requires defaults don’t have full introspection capabilities (yet), in which case you need to supply the default values for the form data as an option:
import { type } from 'arktype';
// Arktype schema, powerful stuff
const schema = type({
name: 'string',
email: 'email',
tags: '(string>=2)[]>=3',
score: 'integer>=0'
});
const defaults = { name: '', email: '', tags: [], score: 0 };
export const load = async () => {
const form = await superValidate(arktype(schema, { defaults }));
return { form };
};
Schema caching
In the example above, both the schema and the defaults are defined outside the load function, on the top level of the module. This is very important to make caching work. The adapter is memoized (cached) with its arguments, so they must be long-lived. Therefore, define the schema and options for the adapter on the top level of a module, so they always refer to the same object.
Optimized client-side validation
The client-side validation is using the smallest possible part of the adapter, to minimize the bundle size for the client. To use it, append Client
to the adapter import, for example:
import { valibotClient } from 'sveltekit-superforms/adapters';
import { schema } from './schema.js';
const { form, errors, enhance } = superForm(data.form, {
validators: valibotClient(schema)
});
For the built-in Superforms validation, import superformClient
. The input parameter can now be undefined
, be sure to check for that case:
import { superformClient } from 'sveltekit-superforms/adapters';
const { form, errors, enhance } = superForm(data.form, {
validators: superformClient({
id: (id?) => { if(id === undefined || isNaN(id) || id < 3) return 'Id must be larger than 2' },
name: (name?) => { if(!name || name.length < 2) return 'Name must be at least two characters' }
})
});
The superform adapter can only to be used on the client, and is in general not a replacement for any other validation library. Hopefully, you can switch to something better now.
SuperValidated type parameters have changed
If you have used type parameters for a call to superValidate
before, or have been using the SuperValidated
type, you now need to wrap the schema parameter with Infer
:
import type { Infer } from 'sveltekit-superforms'
type Message = { status: 'success' | 'failure', text: string }
const form = await superValidate<Infer<typeof schema>, Message>(zod(schema));
import { z } from 'zod';
import type { LoginSchema } from '$lib/schemas';
import type { Infer } from 'sveltekit-superforms'
export let data: SuperValidated<Infer<LoginSchema>>;
Also, constraints
are now optional in the SuperValidated
type, since they won’t be returned when posting data anymore, only when loading data, to save some bandwidth. This is only relevant if you’re changing the constraints before calling superForm
.
superValidateSync is renamed to defaults
The quite popular superValidateSync
function has changed, since it’s not possible to make a synchronous validation anymore, because not all validation libaries are synchronous. So if you’re validating data with superValidateSync
(in the first parameter), be aware that superValidateSync cannot do validation anymore. You need to use a +page.ts
to do proper validation, as described on the SPA page.
Fortunately though, a quick Github search reveals that most of its usages are with the schema only, which requires no validation and no +page.ts
. In that case, just call defaults
with your adapter and eventual initial data, and you’re good to go:
import { defaults } from 'sveltekit-superforms'
// Getting the default values from the schema:
const { form, errors, enhance } = superForm(defaults(zod(schema)), {
SPA: true,
validators: zod(schema),
// ...
})
import { defaults } from 'sveltekit-superforms'
// Supplying initial data (can be partial, won't be validated)
const initialData = { name: 'New user' }
const { form, errors, enhance } = superForm(defaults(initialData, zod(schema)), {
SPA: true,
validators: zod(schema),
// ...
})
Note that superValidate
can be used anywhere but on the top-level of Svelte components, so it’s not removed from the client and SPA usage. But client-side validation is more of a convenience than ensuring data integrity. Always let an external API or a server request do a proper validation of the data before it’s stored or used somewhere.
The id option
It’s not possible to set the id
option to undefined
anymore, which is very rare anyway. By default, the id is automatically set to a string hash of the schema. It’s only for multiple forms on the same page, or dynamically generated schemas, that you may want to change it.
arrayProxy
A simple change: fieldErrors
is renamed to valueErrors
.
Enums in schemas
Previously, it was possible to post the name of the enum as a string, even if it was a numeric enum. That’s not possible anymore:
// Cannot post the string "Delayed" and expect it to be parsed as 2 anymore.
enum FetchStatus {
Idle = 0,
Submitting = 1,
Delayed = 2,
Timeout = 3
}
For string enums, it works to post strings, of course.
Enums must have an explicit default value
Enums don’t have a default “empty” value unlike other types, so it’s not certain what the default value should be. Previously the first enum member was used, but v2 is a bit more strict. Therefore, you must specify a default value for enums explicitly:
export enum Foo {
A = 2,
B = 3
}
const schema = z.object({
foo: z.nativeEnum(Foo).default(Foo.A),
zodEnum: z.enum(['a', 'b', 'c']).default('a')
})
Use isTainted to check tainted status
A new superForm.isTainted
method is available, to check whether any part of the form is tainted. Use it instead of checking the $tainted
store, which may give unexpected results.
const { form, enhance, isTainted } = superForm(form.data);
// Check the whole form
if(isTainted())
// Check a part of the form
if(isTainted('name'))
Speaking of tainted, it now keeps track of the original data, so if you go back to a previous value, it’s not considered tainted anymore.
Schema/validation changes
The underlying data model for Superforms is now JSON Schema, which is what makes it possible to support all the validation libraries. Some changes had to be made for this to work:
No side-effects for default values.
If no data is sent to superValidate
, and no errors should be displayed, as is default in the load function:
const form = await superValidate(zod(schema));
Then the default values won’t be parsed with the schema. In other words, no side-effects like z.refine
will be executed. If you need initial validation of even the default data, set the errors
option to true
, and optionally clear the errors after validation:
const form = await superValidate(zod(schema), { errors: true });
form.errors = {}
Default values aren’t required fields anymore
In hindsight, this should have been the default, given the forgiving nature of the data coercion and parsing. When a default value exists, the field is not required anymore. If that field isn’t posted, the default value will be added to form.data
.
Components
Generic components were previously using Zod types for type safety. It is simpler now:
TextInput.svelte
<script lang="ts" context="module">
type T = Record<string, unknown>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>">
import { formFieldProxy, type SuperForm, type FormPathLeaves } from 'sveltekit-superforms';
export let form: SuperForm<T, unknown>;
export let field: FormPathLeaves<T>;
export let label = '';
const { value, errors, constraints } = formFieldProxy(form, field);
</script>
<label>
{#if label}{label}<br />{/if}
<input
name={field}
type="text"
aria-invalid={$errors ? 'true' : undefined}
bind:value={$value}
{...$constraints}
{...$$restProps}
/>
{#if $errors}<span class="invalid">{$errors}</span>{/if}
</label>
+page.svelte
<script lang="ts">
import { superForm } from 'sveltekit-superforms';
import TextInput from './TextInput.svelte';
export let data;
const supForm = superForm(data.form);
const { form, enhance } = supForm;
</script>
<form method="POST" use:enhance>
<TextInput form={supForm} field="name" />
<button>Submit</button>
</form>
Removed features
superForm.fields is removed
The fields
object returned from superForm
was an inferior version of formFieldProxy, and has now been removed. Use formFieldProxy
to create your own instead.
superForm tainted option does not support specific fields anymore
You could previously choose what specific fields to untaint with a fields
option, when updating $form
. It was a rarely used feature that has now been removed.
onError “message” parameter is removed
Previously, there was a message
parameter in the onError event. It’s gone now, since it was pointing to the message store, and you might as well just assign it directly:
const { form, message, enhance } = superForm(data.form, {
onError({ result }) {
$message = result.error.message
}
})
flashMessage.onError “message” parameter renamed to “flashMessage”
To be more consistent with the message parameter, the rarely used flashMessage
option in superForm
has an onError
event with a message
parameter, but it is now renamed to flashMessage
to signify which message can actually be updated.
New features
Of course, there are some new features, so the migration will be worthwhile.
File upload support!
Finally, it’s possible to handle files with Superforms. Validation even works on the client, with an on:input
handler:
Single file input
const schema = z.object({
image: z
.custom<File>()
.refine((f) => f instanceof File && f.size < 10000, 'Max 10Kb upload size.')
});
const form = await superValidate(formData, zod(schema));
<input
type="file"
name="image"
accept="image/png, image/jpeg"
on:input={(e) => ($form.image = e.currentTarget.files?.item(0) ?? null)}
/>
Multiple files(!)
const schema = z.object({
images: z
.custom<File>()
.refine((f) => f instanceof File && f.size < 10000, 'Max 10Kb upload size.')
.array()
});
const form = await superValidate(formData, zod(schema));
<input
type="file"
multiple
name="images"
accept="image/png, image/jpeg"
on:input={(e) => ($form.images = Array.from(e.currentTarget.files ?? []))}
/>
The only caveat is that in form actions, you must use a special removeFiles
function when you return a form containing files. This is because file objects cannot be serialized, so they must be removed before returning the form data to the client. It’s not a big change:
import { fail } from '@sveltejs/kit';
import { removeFiles } from 'sveltekit-superforms';
// When using fail
if (!form.valid) return fail(400, removeFiles({ form }));
// Vhen returning just the form:
return removeFiles({ form })
// message and setError works as usual:
return message(form, 'Posted OK!');
If you want to prevent file uploads, you can do that with the { allowFiles: false }
option in superValidate
. This will set all files to undefined
, which will also happen if you have defined SUPERFORMS_LEGACY. In that case, set { allowFiles: true }
to allow files.
SuperDebug
Now that file uploads is a feature, SuperDebug displays file objects properly:
Union support!
A requested feature is support for unions, which has always been a bit difficult to handle with FormData
parsing and default values. It’s one thing to have a type system that can define any kind of structure, another to have a form validation library that is supposed to map a list of string values to these types! But unions can now be used in schemas, with a few compromises:
Unions must have an explicit default value
If a schema field can be more than one type, it’s not possible to know what default value should be set for it. Therefore, you must specify a default value for unions explicitly:
const schema = z.object({
undecided: z.union([z.string(), z.number()]).default(0)
})
Multi-type unions can only be used when dataType is ‘json’
As said, unions are also quite hard to make assumptions about in FormData
. If "123"
was posted (as all posted values are strings), should it be parsed as a string or a number, in the above case?
There is no obvious solution, so schemas with unions that have more than one type can only be used when the dataType
option is set to 'json'
(which will bypass the whole FormData
parsing, as the form data is serialized instead).
superForm.validate
The validate
method is very useful for validating the whole form, or a specific field. You can now also call validate({ update: true })
to trigger a full client-side validation.
Simplified imports
You may have noticed in the examples that /client
and /server
isn’t needed anymore, just import everything except adapters from sveltekit-superforms
. The same goes for SuperDebug
, which is now the default export of the library:
import { superForm, superValidate, dateProxy } from 'sveltekit-superforms';
import SuperDebug from 'sveltekit-superforms';
Testing help needed!
Even though this is considered an alpha version, all tests are passing from v1, so v2 is definitely not unstable. With your help, I’m certain that we can reach an official release quite soon. Please try it out, convert some old project or try your favorite validation library with it. Report any issues on Github (preferrably) or on Discord.