Progressive Form Enhancement With SvelteKit
Published Nov 28, 2022
Table of Contents
How Do Forms Work?
By the end of this post youโre going to learn how to use progressive enhancement in SvelteKit and understand the history that led us here.
๐งช You can find the code on GitHub or try it on StackBlitz.
Do you know the default behavior of a form?
I encourage you to try it out yourself inside a SvelteKit project and see what happens.
<form>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
You might be surprised to learn the browser already knows how to send and process the form data.
If you used any modern frontend JavaScript framework in the last couple of years you probably forgot how forms work.
JavaScript was meant to enhance the user experience but instead it became mandatory using event.preventDefault()
that prevents the default form behavior and puts reimplementing the browser behavior on you.
<script lang="ts">
async function handleSubmit(event: SubmitEvent) {
const form = event.target as HTMLFormElement
const data = new FormData(form)
await fetch(form.action, {
method: form.method,
body: data,
// optional, the browser can handle this ๐
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#body
// headers: { 'Content-Type': 'form-data' },
})
}
</script>
<form
method="POST"
action="/api/login"
on:submit|preventDefault={handleSubmit}
>
<label>
<span>Email</span>
<input type="email" name="email" />
</label>
<label>
<span>Password</span>
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</form>
import { redirect } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
// https://kit.svelte.dev/docs/routing#server
export const POST: RequestHandler = async ({ request }) => {
const formData = await request.formData()
const data = Object.fromEntries(formData)
// do whatever you want
console.log(data)
redirect(303, '/')
}
Progressive enhancement is popularized and championed by web frameworks like Remix and SvelteKit where they give you the best of both worlds by having a form work without JavaScript and then if JavaScript is available on the page they use client-side rendering.
This is possible because these frameworks combine the frontend and backend and give you control over both where you can have an endpoint for a form.
Svelte Actions To The Rescue
The previous code is a bit tedious to write for every form, so you can use Svelte actions to make it more reusable.
export function enhance(form: HTMLFormElement) {
async function handleSubmit(event: SubmitEvent) {
event.preventDefault()
const data = new FormData(form)
await fetch(form.action, {
method: form.method,
body: data,
})
}
form.addEventListener('submit', handleSubmit)
return {
destroy: () => {
form.removeEventListener('submit', handleSubmit)
},
}
}
<script lang="ts">
import { enhance } from '$lib/form'
</script>
<form
method="POST"
action="/api/login"
use:enhance
>
<label>
<span>Email</span>
<input type="email" name="email" />
</label>
<label>
<span>Password</span>
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</form>
This is a slight improvement but you also have to invalidate the page data and handle errors but thanks to SvelteKit you donโt have to do this work.
Use The Web Platform
SvelteKit makes working with forms easy with form actions.
Instead of using a standalone endpoint use a page endpoint +page.server.ts
file alongside +page.svelte
.
import { redirect } from '@sveltejs/kit'
import type { Actions, PageServerLoad } from './$types'
let count = 0
// SvelteKit is going to rerun the `load` function and invalidate the data
export const load: PageServerLoad = () => {
console.log('+page.svelte load function')
return { count: count += 1 }
}
export const actions: Actions = {
login: async ({ request }) => {
const formData = await request.formData()
const data = Object.fromEntries(formData)
// do whatever you want
console.log(data)
redirect(303, '/')
},
}
Try using the form without JavaScript! After youโre done add progressive enhancement from SvelteKit.
<script lang="ts">
import { enhance } from '$app/forms'
import type { PageServerData } from './$types'
export let data: PageServerData
$: console.log(data.count)
</script>
<form
method="POST"
action="?/login"
use:enhance
>
<label>
<span>Email</span>
<input type="email" name="email" />
</label>
<label>
<span>Password</span>
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</form>
Thatโs it! ๐ฅ
SvelteKit also makes validating form errors and redirecting simple which Iโm going to cover in a future post.
You can also learn how to customize this behavior and manage pending UI state in the SvelteKit docs.
<form
method="POST"
use:enhance={({ form, data, action, cancel }) => {
// `form` is the `<form>` element
// `data` is its `FormData` object
// `action` is the URL to which the form is posted
// `cancel()` will prevent the submission
return async ({ result, update }) => {
// `result` is an `ActionResult` object
// `update` is a function which triggers the logic that would be triggered if this callback wasn't set
}
}}
>
The action provided by SvelteKit looks a lot like the Svelte action from before doesnโt it? ๐