Security News
38% of CISOs Fear They’re Not Moving Fast Enough on AI
CISOs are racing to adopt AI for cybersecurity, but hurdles in budgets and governance may leave some falling behind in the fight against cyber threats.
sveltekit-superforms
Advanced tools
Supercharge your SvelteKit forms with this powerhouse of a library!
Supercharge your SvelteKit forms with this powerhouse of a library!
PageData
and ActionData
- Stop worrying about which one to use and how, just focus on your data.FormData
into correct types.FormData
- Send your forms as devalued JSON, transparently.ActionResult
and validation updates.applyAction
, invalidateAll
, autoFocus
, resetForm
, etc...(p)npm i -D sveltekit-superforms zod
Let's gradually build up a super form, starting with just displaying the data for a name and an email address.
src/routes/+page.server.ts
import type { PageServerLoad } from './$types';
import { z } from 'zod';
import { superValidate } from 'sveltekit-superforms/server';
// See https://zod.dev/?id=primitives for schema syntax
const schema = z.object({
name: z.string().default('Hello world!'), // A default value just to show something
email: z.string().email()
});
export const load = (async (event) => {
const form = await superValidate(event, schema);
// Always return { form } and you'll be fine.
return { form };
}) satisfies PageServerLoad;
superValidate
takes the data as the first parameter, which could be either:
RequestEvent
, as in this caseRequest
FormData
(usually from the request)null
or undefined
src/routes/+page.svelte
<script lang="ts">
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms/client';
export let data: PageData;
// This is where the magic happens.
const { form } = superForm(data.form);
</script>
<h1>sveltekit-superforms</h1>
<form method="POST">
<label for="name">Name</label>
<input type="text" name="name" bind:value={$form.name} />
<label for="email">E-mail</label>
<input type="text" name="email" bind:value={$form.email} />
<div><button>Submit</button></div>
</form>
superForm
is used on the client-side to display the data, which is conveniently supplied from data.form
.
With this, we can at least see that the form is populated. But to get deeper insight, let's add the Super Form Debugging Svelte Component:
src/routes/+page.svelte
<script lang="ts">
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
</script>
<SuperDebug data={$form} />
Edit the fields and see how the $form
store is automatically updated. The component also displays the current page status in the right corner.
Optional: If you're starting from scratch, add this to <head>
for a much nicer visual experience:
src/app.html
<link
rel="stylesheet"
href="https://unpkg.com/normalize.css@8.0.1/normalize.css"
/>
<link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura.css" />
Let's add a minimal form action, to be able to post the data back to the server:
src/routes/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms/server';
export const actions = {
default: async (event) => {
// Same syntax as in the load function
const form = await superValidate(event, schema);
console.log('POST', form);
// Convenient validation check:
if (!form.valid) {
// Again, always return { form } and things will just work.
return fail(400, { form });
}
// TODO: Do something with the validated data
// Yep, return { form } here too
return { form };
}
} satisfies Actions;
Submit the form, and see what's happening on the server:
POST {
valid: false,
errors: { email: [ 'Invalid email' ] },
data: { name: 'Hello world!', email: '' },
empty: false,
message: null,
constraints: {
name: { required: true },
email: { required: true }
}
}
This is the validation object returned from superValidate
, containing all you need to handle the rest of the logic:
valid
- A boolean
which tells you whether the validation succeeded or not.errors
- A Record<string, string[]>
of all validation errors.data
- The coerced posted data, in this case not valid, so it should be promptly returned to the client.empty
- A boolean
which tells you if the data passed to superValidate
was empty, as in the load function.message
- A string
property that can be set as a general information message.constraints
- An object with html validation constraints than can be spread on input fields.And as you see in the example above, the logic for checking validation status is as simple as it gets:
if (!form.valid) {
return fail(400, { form });
}
If you submit the form now, you'll see that the Super Form Debugging Svelte Component shows a 400
status, and there are some errors being sent to the client, so how do we display them?
We do that by adding variables to the destructuring assignment of superForm
:
src/routes/+page.svelte
<script lang="ts">
const { form, errors, constraints } = superForm(data.form);
// ^^^^^^ ^^^^^^^^^^^
</script>
<form method="POST">
<label for="name">Name</label>
<input
type="text"
name="name"
data-invalid={$errors.name}
bind:value={$form.name}
{...$constraints.name}
/>
{#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}
<label for="email">E-mail</label>
<input
type="text"
name="email"
data-invalid={$errors.email}
bind:value={$form.email}
{...$constraints.email}
/>
{#if $errors.email}<span class="invalid">{$errors.email}</span>{/if}
<div><button>Submit</button></div>
</form>
<style>
.invalid {
color: red;
}
</style>
And with that, we have a fully working form, no JavaScript needed, with convenient handling of data and validation on both client and server!
Have we even started on the feature list? Well, let's move into the 2000's and activate JavaScript, and see what will happen.
Let's start with retrieving a simple but most useful variable returned from superForm
, and use it on the <form>
:
<script lang="ts">
const { form, errors, enhance } = superForm(data.form);
// ^^^^^^^
</script>
<form method="POST" use:enhance>
And with that, we're completely client-side. So what is included in this little upgrade?
This is the beginning of a long list of options for superForm
, which can be added as an option object:
const { form, errors, enhance } = superForm(data.form, { lotsOfOptions });
Try to modify the form fields, then close the tab or open another page in the same tab. A confirmation dialog should prevent you from losing the changes.
taintedMessage: string | null | false = '<A default message in english>'
When the page status changes to something between 200-299, the form is automatically marked as untainted.
It's not evident in our small form, but on larger forms it's nice showing the user where the first error is. There are a couple of options for that:
scrollToError: 'smooth' | 'auto' | 'off' = 'smooth'
autoFocusOnError: boolean | 'detect' = 'detect'
errorSelector: string | undefined = '[data-invalid]'
stickyNavbar: string | undefined = undefined
scrollToError
is quite self-explanatory.
autoFocusOnError
: When set to detect
, it checks if the user is on a mobile device, if not it will automatically focus on the first error input field. It's prevented on mobile since auto-focusing will open the on-screen keyboard, most likely hiding the validation error.
errorSelector
is the selector used to find the invalid input fields. The default is [data-invalid]
, and the first one found on the page will be handled according to the two previous settings.
stickyNavbar
- If you have a sticky navbar, set its selector here and it won't hide any errors.
In order of micro-managing the result, from least to most.
onUpdated: ({ form }) => void
If you just want to apply the default behaviour and do something afterwards depending on validation success, onUpdated
is the simplest way.
onUpdate: ({ form, cancel }) => void
A bit more control, onUpdate
lets you enter just before the form update is being applied and gives you the option to modify the form
object (the validation result), or cancel()
the update altogether.
onError: (({ result, message }) => void) | 'set-message' | 'apply' | string = 'set-message'
It's soon explained that ActionResult errors are handled separately, to avoid data loss. onError
gives you more control over the error than the default, which is to set the message
store to the error value.
By setting onError to apply
, the default applyAction
behaviour will be used, effectively rendering the nearest +error
boundary. Or you can set it to a custom error message.
onSubmit: SubmitFunction;
onSubmit
hooks you in to SvelteKit's use:enhance
function. See SvelteKit docs for the SubmitFunction signature.
onResult: ({ result, update, formEl, cancel }) => void
When you want detailed control, onResult
gives you the ActionResult in result
and an update
function, so you can decide if you want to update the form at all.
The update(result, untaint?)
function takes an ActionResult
of type success
or failure
, and an optional untaint
parameter which can be used to untaint the form, so the dialog won't appear when navigating away. If untaint
isn't specified, a result status between 200-299 will untaint the form.
formEl
is the HTMLFormElement
of the form.
cancel()
is a function which will completely cancel the rest of the event chain and any form updates. It's not the same as not calling update
, since without cancelling, the SvelteKit use:enhance behaviour will kick in, with some notable changes:
(Knowing about ActionResult is useful before reading this section.)
The biggest difference is that unless onError
is set to apply
, any error
result is transformed into failure
, to avoid disaster when the nearest +error.svelte
page is rendered, which will wipe out all the form data that was just entered.
If no error occured, you have some options to customize the rest of the behavior:
applyAction: boolean = true;
invalidateAll: boolean = true;
resetForm: boolean = false;
As you see, another difference is that the form isn't resetted by default. This should also be opt-in to avoid data loss, and this isn't always wanted, especially in backend interfaces, where the form data should be persisted.
In any case, since we're binding the fields to $form
, the html form reset behavior doesn't make much sense, so in sveltekit-superforms
, resetting means going back to the initial state of the form data, usually the contents of form
in PageData
. This may not be exactly what you needed, in which case you can use an event to clear the form instead.
It's worth noting that by setting applyAction
to false
, multiple forms on the same page can be handle quite easily, since they won't tamper with $page.form
and $page.status
, in that case.
There is already a browser standard for client-side form validation, and the constraints
store returned from superForm
can be used to follow it with virtually no effort:
<script lang="ts">
const { form, constraints } = superForm(data.form);
</script>
<input name="email" bind:value={$form.email} {...$constraints.email} />
The constraints field is an object, with validation properties mapped from the schema:
{
pattern?: string; // z.string().regex(r)
step?: number; // z.number().step(n)
minlength?: number; // z.string().min(n)
maxlength?: number; // z.string().max(n)
min?: number | string; // number if z.number.min(n), ISO date string if z.date().min(d)
max?: number | string; // number if z.number.max(n), ISO date string if z.date().max(d)
required?: true; // Not nullable, nullish or optional
}
If think the built-in browser validation is too constraining (pun intented), you can set the validators
option:
validators: {
field: (value) => string | null | undefined;
}
It takes an object with the same keys as the form, with a function that receives the field value and should return either a string as a "validation failed" message, or null
or undefined
if the field is valid.
Here's how to validate a string length:
src/routes/+page.svelte
const { form, errors, enhance } = superForm(data.form, {
validators: {
name: (value) =>
value.length < 3 ? 'Name must be at least 3 characters' : null
}
});
There is one additional option for specifying the default client validation behavior, when no custom validator exists for a field:
defaultValidator: 'keep' | 'clear' = 'clear'
The default value clear
, will remove the error when that field value is modified. If set to keep
, validation errors will be kept displayed until the form submits (unless you change it, see next option).
Making the user understand that things are happening when they submit the form is imperative for the best possible user experience. Fortunately, there are plenty of options for that, with sensible defaults.
clearOnSubmit: 'errors' | 'message' | 'errors-and-message' | 'none' = 'errors-and-message'
delayMs: number = 500
timeoutMs: number = 8000
The clearOnSubmit
option decides what should happen to the form when submitting. It can clear all the errors
, the message
, both or none. The default is to clear both. If you don't want any jumping content, which could occur when error messages are removed from the DOM, setting it to none
can be useful.
The delayMs
and timeoutMs
decides how long before the submission changes state. The states are:
Idle -> Submitting -> Delayed -> Timeout
0 ms delayMs timeoutMs
These states affect the readable stores submitting
, delayed
and timeout
returned from superForm
. They are not mutually exclusive, so submitting
won't change to false
when delayed
becomes true
.
A perfect use for these is to show a loading indicator while the form is submitting:
src/routes/+page.svelte
<script lang="ts">
const { form, errors, enhance, delayed } = superForm(data.form);
// ^^^^^^^
</script>
<div>
<button>Submit</button>
{#if $delayed}<span class="delayed">Working...</span>{/if}
</div>
The reason for not using submittting
here is based on the article Response Times: The 3 Important Limits, which states that for short waiting periods, no feedback is required except to display the result. Therefore, delayed
is used to show a loading indicator.
Experimenting with these three timers and the delays between them, is certainly possible to prevent the feeling of unresponsiveness in many cases. Please share your results, if you do!
multipleSubmits: 'prevent' | 'allow' | 'abort' = 'prevent'
This one is more for the sake of the server than the user. When set to prevent
, the form cannot be submitted again until a result is received, or the timeout
state is reached. abort
is the next sensible approach, which will cancel the previous request before submitting again. Finally, allow
will pass through any number of frenetic clicks on the submit button!
The sister library to sveltekit-superforms
is called sveltekit-flash-message, a useful addon since the message
property of Validation<T>
doesn't persist when redirecting to a different page. If you have it installed and configured, you need to specify this option to make things work:
import * as flashModule from 'sveltekit-flash-message/client';
flashMessage: {
module: flashModule,
onError?: (errorResult: ActionResult<'error'>) => App.PageData['flash']
}
The flash message is set automatically for every ActionResult
except error
, so the onError
callback is needed to transform errors into your flash message type, or leave it out to disregard them.
I've been saving the best for last - If you're fine with JavaScript being a requirement for posting, you can bypass the annoyance that everything is a string
when we are posting forms:
dataType: 'form' | 'formdata' | 'json' = 'form'
By simply setting the dataType
to json
, you can store any data structure allowed by devalue in the form, and you don't have to worry about failed coercion, converting arrays to strings, etc!
If this bliss is too much to handle, setting dataType
to formdata
, posts the data as a FormData
instance based on the data structure instead of the content of the <form>
element, so you don't have to set names for the form fields anymore (this also applies when set to json
). This can make the html for a form quite slim:
<form method="POST" use:enhance>
<label>
Name<br /><input data-invalid={$errors.name} bind:value={$form.name} />
{#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}
</label>
<label>
E-mail<br /><input
type="email"
data-invalid={$errors.email}
bind:value={$form.email}
/>
{#if $errors.email}<span class="invalid">{$errors.email}</span>{/if}
</label>
<button>Submit</button>
{#if $delayed}Working...{/if}
</form>
<style>
.invalid {
color: red;
}
</style>
Sometimes the form data must be proxied, which could happen when you get a string
value from an input field, third-party library, etc, and want it to be automatically converted and updating a non-string value in your form data structure. Fortunately, there are a number of objects available for that:
import {
intProxy,
numberProxy,
booleanProxy,
dateProxy
} from 'sveltekit-superforms/client';
The usage for all of them is the same:
// Assume the following schema:
// z.object({ id: z.number().int() })
const { form } = superForm(data.form);
const idProxy = intProxy(form, 'id'); // Writable<string>
Now if you bind to $idProxy
instead of directly to $form.id
, the value will be converted to an integer and $form.id
will be updated automatically.
Available here at the repository wiki.
A more detailed example of how to create a fully working CRUD (Create, Read, Update, Delete) backend in just a few lines of code is available here.
Visit the FAQ for answers to questions about multiple forms, file uploads, and much more.
The library is quite stable so don't expect any major changes, but there could still be minor breaking changes until version 1.0, mostly variable naming.
Ideas, feedback, bug reports, PR:s, etc, are very welcome as a github issue.
FAQs
Making SvelteKit forms a pleasure to use!
The npm package sveltekit-superforms receives a total of 19,583 weekly downloads. As such, sveltekit-superforms popularity was classified as popular.
We found that sveltekit-superforms demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
CISOs are racing to adopt AI for cybersecurity, but hurdles in budgets and governance may leave some falling behind in the fight against cyber threats.
Research
Security News
Socket researchers uncovered a backdoored typosquat of BoltDB in the Go ecosystem, exploiting Go Module Proxy caching to persist undetected for years.
Security News
Company News
Socket is joining TC54 to help develop standards for software supply chain security, contributing to the evolution of SBOMs, CycloneDX, and Package URL specifications.