DEV Community

Cover image for Sveltekit Changes: Form Actions and Progressive Enhancement
Shivam Meena
Shivam Meena

Posted on

Sveltekit Changes: Form Actions and Progressive Enhancement

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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 };
  }};
Enter fullscreen mode Exit fullscreen mode

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'));
        }
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Just add use:enhance and it will do all browser behaviored things without page reload.It will:

  1. update the form property and invalidate all data on a successful response.
  2. update the form property on a invalid response.
  3. update $page.status on a successful or invalid response.
  4. call goto on a redirect response.
  5. 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
    };
  }}
>
Enter fullscreen mode Exit fullscreen mode

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);
            }
    };
  }}
>
Enter fullscreen mode Exit fullscreen mode

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 to result.status and updates form to result.data
  • redirect — calls goto(result.location)
  • error — renders the nearest +error boundary with result.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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
jdgamble555 profile image
Jonathan Gamble

You really need to use TS to even appreciate this feature.

Collapse
 
theether0 profile image
Shivam Meena

Sure I'll keep that in mind.

Collapse
 
afrowave profile image
Jimmy Gitonga

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.