Due to the ever-evolving nature of software development, creating and managing forms remains a crucial yet often difficult task to achieve. Whether for basic contact forms or intricate data input interfaces, form validation is essential to ensure accurate and secure submissions. This step becomes even more complex as an application grows in size.
Imagine developing an app with multiple forms, each demanding unique data formats, required fields, and distinct validation rules. Quickly, the validation logic can become hard to manage. Ensuring consistency in error messages, data handling, and user experience becomes a difficult task.
Superforms, a SvelteKit library, helps with this. This library offers a comprehensive solution to the challenges posed by form validation complexity. Leveraging a Zod validation schema as the single source of truth, Superforms centralizes validation rules, ensuring consistency across the entire application. This approach eliminates code duplication and offers a solution for server and client validation.
In this article, we will learn how to set up and use Superforms in our Svelte applications. We’ll cover the following:
The Superforms library seamlessly validates data on both the server and client using Zod, with output that seamlessly integrates into the client. The library also offers auto-centering and auto-focusing on erroneous form fields, which enhances user-friendly interactions.
Superforms has the ability to detect tainted/dirty forms, safeguarding against data loss when users navigate away from unsaved forms. The library transcends FormData limitations for complex data structures, transparently dispatching forms as devalued JSON.
Additionally, Superforms generates default form values based on validation schemas. It supports intricate nested data structures, snapshots, and accommodates multiple forms on a single page. It works seamlessly on both server-rendered and single-page applications.
Superforms offers proxy objects for hassle-free data conversion to and from string representations. Introducing real-time, client-side validators, it provides instant user feedback for enhanced usability. Its extensive event integration provides complete control over validation data, and ActionResult, offering the option to halt updates at any stage.
Below is a simple unvalidated form for receiving user information. In this section, we will install Superforms and validate our user form with it:
//src/routes/+page.svelte <script lang="ts"> </script> <div class='form-wrapper'> <form method="POST"> <label for="name">First Name</label> <input type="text" name="name" /> <label for="name">Last Name</label> <input type="text" name="name" /> <label for="email">E-mail</label> <input type="email" name="email" /> <label for="email">Employee Number</label> <input type="number" name="employeeNumber" /> <div><button>Submit</button></div> </form> </div> <style> .form-wrapper{ display: flex; justify-content: center; } button { background-color: #008001; border-radius: 5px; border: none; padding: 10px; } button:hover { background-color: #AAD922; } </style>
If you are adding Superforms to an existing project, install it using either of the following commands:
pnpm i -D sveltekit-superforms zod
npm i -D sveltekit-superforms zod
If you are creating a new SvelteKit project, install it with this command before installing Superforms: npm create svelte@latest
.
This section explores how to validate a form on the server side of a SvelteKit application. The first thing we do is create a schema using Zod, which we installed earlier. The schema represents the form data:
//src/routes/+page.server.ts import { z } from "zod" const userSchema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), employeeNumber: z.number().min(1) })
Next, we’ll initialize the form in the load
function using Superforms’ server API, superValidate
. superValidate
takes in the schema we described earlier:
//src/routes/+page.server.ts import { z } from "zod" import { superValidate } from 'sveltekit-superforms/server'; const userSchema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), employeeNumber: z.number().min(1) }) export const load = (async () => { // Server API: const form = await superValidate(userSchema); // Always return { form } in load and form actions. return { form }; });
With this, we are now forwarding the validation data to the client. Our next step involves retrieving this data using the client-side API called superForm
in the Svelte page with export let data;
. After that, we destructure form
from superForm
.
See below for the code:
//src/routes/+page.svelte <script lang="ts"> import { superForm } from "sveltekit-superforms/client" export let data: const { form } = superForm(data.form) </script>
form
is a store containing the properties of our userSchema
schema. We use the prefix $
before form
to access the form store.
Next, we bind the values of each input field to its corresponding form property like so: bind:value={$form.firstName}
. We do this for all the fields.
Read more about schema validation with Zod here.
Before we move on to posting the form data to the server, let me quickly demonstrate a cool component provided by Superforms for dev debugging called SuperDebug
.
This component takes form
as a prop and can be used to show what is happening with the form in real time. It also has a status code that displays error
when the form validation fails, and success
in the absence of any errors. It should only be used during development.
This is how to import and use SuperDebug
:
import SuperDebug from "sveltekit-superforms/client/SuperDebug.svelte"
<SuperDebug data={form}/>
It shows the object at the top of the page so you see the changes happening to the form:
To post form data back to the server, we’ll create a form action. Also, we’ll continue to use the superValidate
function, but this time we’ll fill it with FormData
, which holds the form data.
There are a few ways to do this. The first is by using the event
object, which also has the request:
export const actions = { default: async (event) => { const form = await superValidate(event, userSchema); console.log(form); return { form }; } };
The second way is by using the request
parameter, which includes FormData
:
export const actions = { default: async ({ request }) => { const form = await superValidate(request, userSchema); console.log(form); return { form }; } };
Now let’s fill out and submit the form, and then inspect the console to see the form data being sent to the server from the client:
{ id: 'h7b6xj', valid: true, posted: true, errors: {}, data: { firstName: 'John ', lastName: 'Doe', email: '[email protected]', employeeNumber: '09719293' }, constraints: { firstName: { minlength: 1, required: true }, lastName: { minlength: 1, required: true }, email: { required: true }, employeeNumber: { minlength: 1, required: true } } }
The code above is what you get back from superValidate
. It also gives you everything you need to further customize the form:
id
: This is used to identify the schema, which is useful when dealing with multiple forms on a pagevalid
: This informs you if the form validation was successful or not. This information is useful for both server-side and event-related actionsposted
: This tells us whether the data was sent through the form action or notdata
: This is an object containing the posted data. With this property, you can easily check if the user data is valid or not. If not, it can be sent back to the client using the “fail” approacherrors
: This object contains all the validation errors maintaining the same structure as the data objectmessage
: This field is set to pass a status message for further clarityconstraints
: This is also an object that stores normal HTML validation constraints, which can be applied to input fields thereby adding another layer of validationNow we can add logic to return an error with a code of 400
:
if (!form.valid) { return fail(400, { form }) }
Now if we try to skip a field in the form, we will find that the error object is no longer empty:
{ id: 'h7b6xj', valid: false, posted: true, errors: { lastName: [ 'String must contain at least 1 character(s)' ], email: [ 'Invalid email' ], employeeNumber: [ 'String must contain at least 1 character(s)' ] }, data: { firstName: 'ww', lastName: '', email: '', employeeNumber: '' }, constraints: { firstName: { minlength: 1, required: true }, lastName: { minlength: 1, required: true }, email: { required: true }, employeeNumber: { minlength: 1, required: true } } }
Notice this status code is no longer showing 200
but 400
:
With the updated error object, we can now import the error in the client and show the users:
const { form, errors } = superForm(data.form)
Then, we can use it to show the appropriate error under each input like so:
{#if $errors.firstName} {$errors.firstName} {/if}
This is the updated code for //src/routes/+page.svelte
at this point:
//src/routes/+page.svelte <script lang="ts"> import { superForm } from "sveltekit-superforms/client" import SuperDebug from "sveltekit-superforms/client/SuperDebug.svelte" export let data; const { form, errors } = superForm(data.form) </script> <SuperDebug data={form}/> <div class='form-wrapper'> <form method="POST"> <div> <label for="name">First Name</label> <input type="text" name="firstName" bind:value={$form.firstName} /> {#if $errors.firstName} <p class='error-para'>{$errors.firstName}</p> {/if} </div> <div> <label for="name">Last Name</label> <input type="text" name="lastName" bind:value={$form.lastName} /> {#if $errors.lastName} <p class='error-para'>{$errors.lastName}</p> {/if} </div> <div> <label for="email">E-mail</label> <input type="email" name="email" bind:value={$form.email} /> {#if $errors.email} <p class='error-para'>{$errors.email}</p> {/if} </div> <div> <label for="email">Employee Number</label> <input type="number" name="employeeNumber" bind:value={$form.employeeNumber} /> {#if $errors.employeeNumber} <p class='error-para'>{$errors.employeeNumber}</p> {/if} </div> <div><button>Submit</button></div> </form> </div> <style> .form-wrapper{ display: flex; justify-content: center; } button { background-color: #008001; border-radius: 5px; border: none; padding: 10px; } button:hover { background-color: #AAD922; } .error-para{ color: red; font-size: 12px; } </style>
use:enhance
featureWhen we use the “enhance” feature from superForm
, it brings enhanced client-side interactivity to forms in a SvelteKit application. When you use the enhance
feature, various client-side features become available, such as events, timers, and auto-error focus. These enhancements enhance the user experience by providing real-time feedback and smoother interactions.
Notably, the use:enhance
action doesn’t take any arguments. Instead, events are employed to interact with the default SvelteKit use:enhance
parameters and other functionalities. For a deeper understanding of enhance
and how events work in this context, check out the official Superforms documentation.
It’s essential to acknowledge that without using use:enhance
, the form remains static, lacking the enhanced client-side behavior. Only the constraints
and resetForm
functionalities will operate in this scenario.
In this section, we’ll explore how you can utilize either a Zod schema or a Superforms validation object to achieve thorough real-time validation directly on the client side.
To make use of the existing browser constraints, simply import the $constraints store from superForm
and use it in the appropriate fields. Import it with the following line of code:
const {form, errors, enhance, constraints} = superForm(data.form)
And then apply them to each input like this:
{...$constraints.firstName}
This is the result below:
The browser’s built-in validation might feel a bit limited. For instance, it might be challenging to manage where and how error messages appear. Instead, we have the option to configure certain settings for personalized real-time validation with Superforms. This is how to do that:
const { form, enhance, constraints, validate } = superForm(data.form, { validators: AnyZodObject | { field: (value) => string | string[] | null | undefined; }, validationMethod: 'auto' | 'oninput' | 'onblur' | 'submit-only' = 'auto', defaultValidator: 'keep' | 'clear' = 'keep', customValidity: boolean = false })
The most convenient and advisable way to perform a client-side validation is by configuring the “validators” option with the Zod schema we use on the server side.
But we could also use a Superforms validation object. The object corresponds to the form’s keys and has a function that takes the field value and input. The function returns a string or an array of strings when there is an error with the fields and returns null or undefined if the fields are valid.
Below is an example of client-side validation with both a Superforms validation object and the previously defined userSchema
Zod schema:
//src/routes/+page.svelte const userSchema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), employeeNumber: z.string().min(1) }); // validate the form with our previously defined userSchema zod schema: const { form, errors, enhance } = superForm(data.form, { validators: userSchema; }); // validate the length of the firstName field with Superforms validation object: const { form, errors, enhance, constraints } = superForm(data.form, { validators: { firstName: (firstName) => firstName.length < 3 ? 'Name must be at least 3 characters' : null, } });
The image below demonstrates client validation with our previously defined userSchema
Zod schema:
The image below demonstrates client validation using the Superforms validation object:
As you can see, any one of the choices works. There are many more customizations that can be done on the client side to validate forms.
In addition to the schema validation offered by Zod, an alternative library is Yup. Read more about the two libraries, and compare their functionalities in “Comparing schema validation libraries: Zod vs. Yup.”
Superforms simplifies form validation in SvelteKit applications, streamlining complexity, enhancing user experience, and delivering powerful client-side interactions. Throughout this article, we’ve seen and demonstrated how Superforms helps to make the form validation process a lot simpler, thereby making it an important tool for developers. The code used in the tutorial can be found here.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.