Intro to SvelteKit: Form Actions
In my previous article, we discussed an introduction to SvelteKit, the meta framework for Svelte with support for server-rendering, code-splitting, and other features that make it a powerful front-end development tool. We also discussed how SvelteKit makes it easy to create components, handle routing, and integrate with external APIs. In this article, we will explore how to work with forms using SvelteKit, including creating forms, handling form data, validating form input, and submitting forms. We will also discuss best practices for working with forms in SvelteKit and how to troubleshoot any issues that arise.
Setup
Before we begin working with forms, let’s use pnpm
to create a new SvelteKit project:
pnpm create svelte@latest sveltekit-forms
The code above will prompt you to choose from a few options and generate a basic SvelteKit project based on your selection. The generated project should look something like this:
Once you have selected the options, you will need to install the dependencies for the project, which can be done by running the following command from the project’s directory:
# cd sveltekit-forms
pnpm install
Incorporating Tailwind CSS for styling
In addition, we can also install Tailwindcss, Daisyui and the Tailwindcss typography plugin to enhance the styling and design capabilities of our project, because why not. Fortunately, there is a useful tool called “svelte-add” that is specifically designed for this purpose.
npx svelte-add@latest tailwindcss --daisyui --typography
To install these new dependencies, you will need to use the following command:
pnpm install
Now open the project in your preferred editor; mine is vscode.
How to create a form action in SvelteKit
Forms are an essential part of any web application or website. Form actions are the mechanisms used to handle the data input from the user. SvelteKit is an open-source framework that provides users with a highly customizable and straightforward way to create forms, and the form actions associated with them. In this article, we’ll explore what form actions are and how SvelteKit makes them easier to create, utilize, and manage. We’ll also discuss some of the best practices for using form actions in SvelteKit and review examples of SvelteKit form actions
in action.
Let’s start by creating a simple form on our homepage that will capture a user’s name, email, and message. This form will be used for a contact form.
<section class="container mx-auto px-4 py-4">
<div class="flex flex-col justify-center items-center min-h-[40rem]">
<h1 class="text-4xl text-slate-500">Sveltekit Forms</h1>
<div class="w-3/6 mt-4 rounded-sm p-8 bg-slate-100 ">
<form class="my-4 " action="?/contactus" method= "POST">
<div>
<label class="label text-slate-500 mr-4" for="name">Name </label>
<input type="text" name="name" placeholder="Type here" class="ring-1 ring-slate-200 focus:outline-none rounded-sm input w-full" />
</div>
<div>
<label class="label text-slate-500 mr-4" for="name">Email </label>
<input type="email" name="email" placeholder="Type here" class="ring-1 ring-slate-200 focus:outline-none rounded-sm input w-full" />
</div>
<div>
<label class="label text-slate-500 mr-4" for="name">Message </label>
<textarea class="textarea ring-1 ring-slate-200 focus:outline-none rounded-sm w-full" placeholder="Message"></textarea>
</div>
</form>
</div>
</div>
</section>
In the code above, we used TailwindCSS and DaisyUI to create a contact page with a form to collect user data. It is important to note that all form elements have a name
attribute with values that will help us identify the form elements. This will be especially useful if we need to process the user's inputs on the server. We also added a button that will submit the user's input when clicked and make the method
POST
. Sveltekit forms are configured to support only POST actions, so they cannot be used for any other type of action.
Now that we have our form, Let’s send the form data to our backend using SvelteKit form actions
. SvelteKit provides a convenient way to capture user inputs from forms. We need to create a +page.server.js
file that exports an async
function called actions
. This function has a named action that will use the request
object to get the form data. The data is then stored in a formData
object which we can destructure to get the values or use the get()
method on the data object. We can then use the values however we want. For instance, send it into a database or manipulate it in some way. We can also send the formData
to the page component.
NB: It is important to note that sensitive user information should not be sent to the front-end
export const actions = {
contactus: async ({ request }) => {
const formData = Object.fromEntries(await request.formData());
// const name = res.get('name');
// const email = res.get('email');
// const message = res.get('message');
// console.log(formdata);
return {
success: true,
formData
};
}
};
If no name is specified for the action, SvelteKit will automatically set it as the default action, which will be invoked when the user presses the submit
button.
Displaying the form data
Let’s display the user’s data in our component. To do that, add a script
to the component and export the form
object. This object contains the form data, which we will use in our page only if the user hits the submit button.
<script>
export let form;
</script>
....
{#if form?.success }
<div class="p-4 bg-slate-100 w-3/6 mt-4">
<p>Name: {form?.formData.name}</p>
<p>Message: {form?.formData.message}</p>
</div>
{/if}
.....
Your app should now look like this when you submit after adding some values:
Form Validation
Although our form functions properly, it lacks proper validation. Currently, the form can be submitted without any input, which can lead to the submission of empty forms to the server. This is not acceptable and requires attention.
Form validation is the process of ensuring that the data entered into a form is valid and correct. It is typically used in web development to ensure data is entered correctly into web forms before it is submitted to the server. Form validation can be done in the browser, on the server, or both. It typically involves checking the form fields for correct data types, valid ranges, required fields, and other criteria. It can also involve checking for valid formatting such as email addresses, phone numbers, and URLs. Form validation also helps to protect against malicious input such as SQL injections.
In this example, we will implement server-side validation to ensure that the data received from the client is correct before it is processed by the server. We will also provide clear and appropriate error feedback to the client. To achieve this, we will utilize the Zod
library for form validation. To get started, we first need to install Zod
by running the following command:
pnpm add zod
To ensure proper validation of our form, we will integrate Zod
validation. This can be achieved by creating a Zod
object and specifying the required validation rules for each field within the form. Then, we will modify the form's action to include this Zod
validation.
import { z } from 'zod';
const contactSchema = z.object({
name: z
.string({ required_error: 'Name is required' })
.min(2)
.max(64, { message: 'Name must be less than 64 characters' })
.trim(),
email: z
.string({ required_error: 'Email is required' })
.trim()
.max(64, { message: 'Name must be less than 64 characters' })
.email({ message: 'Email must be a valid email address' }),
message: z
.string({ required_error: 'Message is required' })
.min(5, { message: 'Message must be at least 5 characters' })
.trim()
});
Zod offers a wide range of field-specific validation options, including validation for strings, numbers, and more. The library’s official documentation provides in-depth information on the various validation options available and how to use them effectively.
Now let’s add the validation schema we created to our form data:
export const actions = {
contactUs: async ({ request }) => {
const formData = Object.fromEntries(await request.formData());
// const name = formdata.name;
// const email = formdata.email;
// const message = formdata.message;
try {
const result = contactSchema.parse(formData);
console.log('success!');
console.log(result);
} catch (err) {
const { fieldErrors: errors } = err.flatten();
const { name, email, message } = formData;
return {
name,
email,
message,
errors
};
}
}
};
The code above uses the contactSchema.parse(formdata)
to parse the form data by using a predefined contactSchema
object which is expected to have validation rules for the form fields.
If the parsing is successful, it will print “success!” and the data in the console. But if it fails, the catch
block will execute and the error object will be flattened with the help of err.flatten()
and the field errors will be extracted using fieldErrors: errors
The function then returns an object with keys formdata
values, and errors
which can be used by the client-side to handle the validation errors.
To display client-side validation errors, we will need to incorporate them into the form elements within the +page.svelte
file.
...
<div>
<label class="label text-slate-500 mr-4" for="name">Name </label>
<input type="text" name="name" placeholder="Type here" class="ring-1 ring-slate-200 focus:outline-none rounded-sm input w-full" />
{#if form?.errors?.name }
<span class="py-2 px-1 text-red-400">{form?.errors?.name[0]}</span>
{/if}
</div>
...
If you run the app now, you will see the following if you submit the form with erorrs:
To enhance the user experience, it would be beneficial to retain the user’s input values in the form fields upon encountering validation errors, rather than requiring them to re-enter all of their information. This can be achieved by adding a value
attribute with a condition that checks for and returns the user's input value for each form field. An example will look like this:
...
<input type="text" name="name" placeholder="Type here" class="ring-1 ring-slate-200 focus:outline-none rounded-sm input w-full" value={form?.name ?? ""} />
...
The value
attribute is used to define the value of the input field, this attribute takes an expression that is evaluated to return the value of the input field. In this case, it's using the ??
operator to check if the form.name
exists and if it does, it returns that value, otherwise it returns an empty string.
The ??
is nullish coalescing operator, it returns the value of the left hand side operand if it's not null or undefined, otherwise it returns the right hand side operand. It is used here to avoid accessing a property of an undefined object and throw an error.
Now our form interaction should look like this:
Progressive Enhancement
The form created in this article can be submitted even with JavaScript disabled. However, to enhance the user experience for those who have modern browsers with JavaScript support, we can make the form submission process more efficient and smooth. This can be achieved by adding progressive enhancement to handle the form submission, providing instant feedback to the user, and reducing the need for a page refresh.
Thankfully, SvelteKit offers a convenient use:enhance
action that allows for easy progressive enhancement without the need for additional configuration. While more advanced progressive enhancement techniques, such as custom submit functions, can also be implemented, in this article, we will use the simple use:enhance
action to enhance the form submission process. This will ensure that users with modern browsers will have a smoother and more efficient form submission experience without the need for a full page refresh.
To implement the use:enhance
action on the form, include the following code in the +page.svelte
component:
<script>
import {enhance} from "$app/forms"
...
</script>
<form class="my-4 " action="?/contactUs" method= "POST" use:enhance>
...
By doing this, when the form is submitted, the page will not reload, providing a smoother and more efficient experience for the user.
Conclusion
In conclusion, SvelteKit offers a convenient and efficient solution for handling forms and their actions. By utilizing the built-in request object and libraries like Zod, we can easily access and validate form data on the server-side. By incorporating progressive enhancement, retaining user input values, and displaying clear validation errors, we can enhance the user experience and improve the overall form submission process. This article provided a basic introduction to implementing form actions in SvelteKit, and with the knowledge gained, we can now confidently create and implement forms in our SvelteKit applications.
The code for this article can be found here.