Introduction
In this article we gonna learn and talk about SvelteKit form progressive enhancement use action and new form actions. Now in SvelteKit you don't have to worry about form submission handling or how to handle our apps if Javascript is disabled in users browsers. There are a lot of problems related to forms and SvelteKit tried to fix some of them and I think they did the right.
Here we mainly going to focus on use:enhance(Progressive Enhancement)
and form actions
.Progressive Enhancement includes form response handling, applyResults to client and things related to client. Form actions are mainly for server side thing when you submit a form Form actions
comes to play in backend(server side). It includes default actions, named actions, error and redirects.
We are going to start with Form Actions.
Form Actions
Form action is new way to handle forms submitted from client side with post request on server side. This is all going to take place inside +page.server.ts
or +page.server.js
which exports a function by the name actions
. Let me show you with example of default actions
.
- Default Actions:
// src/routes/login/+page.server.js
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const form = await request.formData();
const email = form.get('email');
const password = form.get('password');
// do whatever you want to do with email and password
}
};
// src/routes/login/+page.svelte
<form method="POST">
<input name="email" type="email">
<input name="password" type="password">
<button>Log In</button>
</form>
As you can see in +page.svelte
we made a form with post request to submit to server and on +page.server.js
we have a exported const actions
. This action have default parameter which which is async function and we can get all data from request. Here, If user clicks on Log in
button for will be submitted using POST method using Default Action.
Sometimes, We have a form somewhere in the layout and layout doesnβt have Actions to handle form submission using Action. In these cases, we can have a route to that page and we can use that route as action=/url
.
For example we have a login form in navbar which is in layout and we need to submit the form using Actions. So here we gonna make a login
route (includes action in +page.server.js
which default action) and in form we gonna pass that route.
// src/routes/+layout.svelte
<form method="POST" action="/login">
<!-- content -->
</form>
When using
<form>
, client-side JavaScript is optional, but you can easily progressively enhance your form interactions with JavaScript to provide the best user experience.
- Named Layouts
As we know we might have multiple forms on same page and we need to submit them using actions and default action can we used in one form. Here we gonna take an example if we have register
and login
forms on same route and we need to submit both. Here, Named routes comes to play these works like default but with a name like me and you.
// src/routes/user/+page.server.js
/** @type {import('./$types').Actions} */
export const actions = {
login: async (event) => {
// TODO log the user in
},
register: async (event) => {
// TODO register the user
}
};
// src/routes/user/+page.svelte
<form method="POST" action="?/login">
<!-- content -->
</form>
<form method="POST" action="?/register">
<!-- content -->
</form>
As you can see in +page.server.js
we have defined are actions const and inside that we named our functions login
and register
.
Here, we don't have a default action so how we gonna call them on our form. So basically on forms we can pas a action
parameter with the specific named action. As in above code block on both forms i passed action="?/login"
for login named action and action="?/register"
for register named action. Here ?/
these things something you have seen in URL's where ?
means a parameter is passed on to request and /login
or /register
is the parameters for the actions to be called on server.
As we discussed in default action
for form to be in layout
page then what we can do in named actions is:
<form method="POST" action="/user?/register">
</form>
We just have to add route with parameters of named layout.
- Error handling in Actions
When we add checks for user input and find error we need to return a response so client/user should know what mistake they have made.
import { invalid } from '@sveltejs/kit';
/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
const user = await db.getUser(email);
if (!user) {
return invalid(400, { email, missing: true });
}
if (user.password !== hash(password)) {
return invalid(400, { email, incorrect: true });
}
cookies.set('sessionid', await db.createSession(user));
return { success: true };
}};
// html
<script>
/** @type {import('./$types').PageData} */
export let data;
/** @type {import('./$types').ActionData} */
export let form;
</script>
{#if form?.success}
<!-- this message is ephemeral; it exists because the page was rendered in
response to a form submission. it will vanish if the user reloads -->
<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}
SvelteKit Provides a invlaid
function which handle all the error related issues. You just need to pass two parameters to it. Ist is status code of error and IInd is data and it can be anything you need on client side for error handling.
In above code snippet, You can see I'm checking for user exist in db if not I'm returning return invalid(400, { email, missing: true })
. That's all we need to handle error in actions. If you have no errors you can just simply return a dictionary just like return { success: true };
. This will do the job for errors and success return.
All the errors and success data we return from actions can be accessed using export let form;
and using that you can provide users with more interactive UI/UX.
- Redirects in Actions
// @errors: 2339 2304
import { invalid, redirect } from '@sveltejs/kit';
/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request, url }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
const user = await db.getUser(email);
if (!user) {
return invalid(400, { email, missing: true });
}
if (user.password !== hash(password)) {
return invalid(400, { email, incorrect: true });
}
cookies.set('sessionid', await db.createSession(user));
if (url.searchParams.has('redirectTo')) {
throw redirect(303, url.searchParams.get('redirectTo'));
}
return { success: true };
}};
In above code snippet,We are looking for a redirect parameter so we can redirect to specified location.
if (url.searchParams.has('redirectTo')) {
throw redirect(303, url.searchParams.get('redirectTo'));
}
But what is important here is throw redirect(303, url.searchParams.get('redirectTo'))
. As you can see redirect
function which is responsible for redirecting to a route takes two parameters one is status code
for redirect and second is location
to be redirected. For example if user is authenticated successfully we need to redirect user to dashboard
. So, we can just do
throw redirect(303, '/dashboard')
This will redirect user too dashboard
page.
Now we have completely covered Form Actions. We need to learn about
Progressive Enhancement
.
Progressive Enhancement
Above, we made a /login
action that works without client-side JavaScript β not a fetch in sight. That's great, but when JavaScript is available we can progressively enhance our form interactions to provide a better user experience.
And we can achieve that just adding a single use action
method from svelte. To progressively enhance a form is to add the use:enhance
action:
<script>
import { enhance } from '$app/forms';
/** @type {import('./$types').ActionData} */
export let form;
</script>
<form method="POST" use:enhance>
Just add use:enhance
and it will do all browser behaviored things without page reload.It will:
- update the
form
property and invalidate all data on a successful response. - update the
form
property on a invalid response. - update
$page.status
on a successful or invalid response. - call
goto
on a redirect response. - render the nearest
+error
boundary if an error occurs.
By default the form property is only updated for actions that are in a +page.server.js alongside the +page.svelte because in the native form submission case you would be redirected to the page the action is on.
To customise the behaviour, you can provide a function that runs immediately before the form is submitted, and (optionally) returns a callback that runs with the ActionResult.
<form
method="POST"
use:enhance={({ form, data, cancel }) => {
// `form` is the `<form>` element
// `data` is its `FormData` object response from action
// `cancel()` will prevent the submission
return async ({ result }) => {
// `result` is an `ActionResult` object
};
}}
>
This will help us for UI changes without reloading page on the basis of form state.
- applyAction Method
Sometimes we need to handle our own errors or redirects on client. Here, we have access to all data returned by the form actions in +page.server.js
.
<script>
import { enhance, applyAction } from '$app/forms';
/** @type {import('./$types').ActionData} */
export let form;
</script>
<form
method="POST"
use:enhance={({ form, data, cancel }) => {
// `form` is the `<form>` element
// `data` is its `FormData` object
// `cancel()` will prevent the submission
return async ({ result }) => {
// `result` is an `ActionResult` object
if (result.type === 'error') {
await applyAction(result);
}
};
}}
>
Here we are looking for error
in results
returned by the form actions
. If result.type === 'error'
we gonna use applyAction(result)
on result to handle error to nearest error
page.
The behaviour of applyAction(result)
depends on result.type:
- success, invalid β sets
$page.status
toresult.status
and updates form toresult.data
- redirect β calls
goto(result.location)
error β renders the nearest
+error
boundary withresult.error
Custom event listener
Custom event listeners are same things with what we used to do with forms in sveltekit earlier. I'll hope that you will try to understand this code snippet.
<script>
import { invalidateAll, goto } from '$app/navigation';
import { applyAction } from '$app/forms';
/** @type {import('./$types').ActionData} */
export let form;
/** @type {any} */
let error;
async function handleSubmit(event) {
const data = new FormData(this);
const response = await fetch(this.action, {
method: 'POST',
body: data
});
/** @type {import('@sveltejs/kit').ActionResult} */
const result = await response.json();
if (result.type === 'success') {
// re-run all `load` functions, following the successful update
await invalidateAll();
}
applyAction(result);
}
</script>
<form method="POST" on:submit|preventDefault={handleSubmit}>
<!-- content -->
</form>
This is a straight forward onclick and preventDefault with custom submit.
These all things are making sveltekit much better than what it used to be and breaking changes might be headache but after v1 we are gonna enjoy it much more than we think.
This is me writing for you. If you wanna ask or suggest anything please put it in comment.
Top comments (3)
You really need to use TS to even appreciate this feature.
Sure I'll keep that in mind.
Thank you @theether0. I am like that you did this in JSDoc instead of TS. While TS is more explicit, the code burden for a complex application is a lot.
So to each, his own.