Svelte form validation with Yup
Tags: svelte, webdev, javascript
Published: 2020-06-11 08:05:14

Form validation is hard. That's why there are so many different form handling libraries for the popular web frameworks. It's usually not something that is built-it, because everyone has a different need and there is no one-fit-all solution.

> Repost of https://codechips.me/svelte-form-validation-with-yup/

Svelte is no exception. There are a few form handling frameworks in the market, but most of them look abandoned. However, there is one specific library that comes to mind that is being actively maintained - [svelte-forms-lib](https://svelte-forms-lib-sapper-docs.now.sh/introduction). It's pretty good and I've used it myself. Check it out!

I work a lot with forms and nowadays I don't use any library. Instead, I've developed a set of abstractions on top of Svelte that work well for me and my needs.

Today I am going to teach you how to do a simple form validation using the awesome [Yup](https://github.com/jquense/yup) library, because it's a pure [Joi](https://github.com/hapijs/joi) to use. Pun intended.

We will build a simple registration form where we will validate user's name and email, if passwords match and also check if the username is available.

Onward.

## What is Yup?

Yup is a library that validates your objects using a validation schema that you provide. You validate the shapes of your objects and their values. Let me illustrate with an example.

### Bootstrap the project

If you want to follow along here is how you can quickly create a new Svelte app.

```bash
# scaffold a new Svelte app first
$ npx create-snowpack-app svelte-yup-form-validation --template @snowpack/app-template-svelte

# add yup as a dependency
$ npm add -D yup
```

## Define the schema

We will be validating fields in the registration form which consists of the following fields:

- name
- email
- username
- password
- password confirm

To start off gently we will only validate that field values are not empty. We will also validate that email address has correct format.

Create an new file in `src` directory called `schema.js`.

```js
// schema.js

import * as yup from 'yup';

const regSchema = yup.object().shape({
  name: yup.string().required(),
  email: yup.string().required().email(),
  username: yup.string().required(),
  password: yup.string().required(),
  passwordConfirm: yup.string().required()
});

export { regSchema };
```

As you can see we defined a schema to validate an object's shape. The properties of the object match the names of the fields and it's not hard to read the validation schema thanks to Yup's expressive DSL. It should pretty much be self-explanatory.

There are a lot of different validators available in Yup that you can mix and match to create very advanced and extremely expressive validation rules.

Yup itself is heavily inspired by [Joi](https://github.com/hapijs/joi) and if you ever used [Hapi.js](https://hapi.dev/) you probably used Joi too.

## Validating an object

Let's do the actual validation of an object by using our schema. Replace `App.svelte` with the following code.

```html
<script>
  import { regSchema } from './schema';

  let values = {
    name: 'Ilia',
    email: 'ilia@example', // wrong email format
    username: 'ilia',
    password: 'qwerty'
  };

  const result = regSchema.validate(values);
</script>

<div>
  {#await result}
  {:then value}
    <h2>Validation Result</h2>
    <pre>{JSON.stringify(value, null, 2)}</pre>
  {:catch value}
    <h2>Validation Error</h2>
    <pre>{JSON.stringify(value, null, 2)}</pre>
  {/await}
</div>
```

The `validate` method returns a promise and we can use Svelte's [await](https://svelte.dev/docs#await) to render it on the page.

When you start the app you will the following validation error exception.

```json
{
  "name": "ValidationError",
  "value": {
    "name": "Ilia",
    "email": "ilia@example",
    "username": "ilia",
    "password": "qwerty"
  },
  "path": "passwordConfirm",
  "type": "required",
  "errors": [
    "passwordConfirm is a required field"
  ],
  "inner": [],
  "message": "passwordConfirm is a required field",
  "params": {
    "path": "passwordConfirm"
  }
}
```

Although we provided a wrong email address our schema doesn't catch that and only tells us that we didn't provide the required `passwordConfirm` property.

How come? It's because Yup has a default setting `abortEarly` set to `true`, which means it will abort on the first error and `required` validator comes before the email format validation.

Try providing the `passwordConfirm` property and you will see that now Yup will give back "email must be a valid email" error.

If we want to validate the whole object we can pass a config to the `validate` call.

```js
const result = regSchema.validate(values, { abortEarly: false });
```

I recommend that you play around by passing in different values to get a feel for what errors are returns before continuing.

## Building a registration form

Next, we need to build a simple registration form. Replace `App.svelte` with the following code.

```html
<!-- App.svelte -->

<style>
  form * + * {
    margin-top: 1em;
  }
</style>

<script>
  import { regSchema } from './schema';
</script>

<div>
  <h1>Please register</h1>
  <form>
    <div>
      <input type="text" name="name" placeholder="Your name" />
    </div>
    <div>
      <input type="text" name="email" placeholder="Your email" />
    </div>
    <div>
      <input type="text" name="username" placeholder="Choose username" />
    </div>
    <div>
      <input type="password" name="password" placeholder="Password" />
    </div>
    <div>
      <input type="password" name="passwordConfirm" placeholder="Confirm password" />
    </div>
    <div>
      <button type="submit">Register</button>
    </div>
  </form>
</div>

```

I omitted the labels and styling because they don't provide any value in this context right now.

## Form binding and submitting

Now we need to bind the form fields to an object that we will later validate.

If you want to know more about how Svelte `bind` works, check out my article - [Svelte bind directive explained in-depth](https://codechips.me/svelte-bind-directive/).

```html
<!-- App.svelte -->

<style>
  form * + * {
    margin-top: 1em;
  }
</style>

<script>
  import { regSchema } from './schema';
  let values = {};

  const submitHandler = () => {
    alert(JSON.stringify(values, null, 2));
  };
</script>

<div>
  <h1>Please register</h1>
  <form on:submit|preventDefault={submitHandler}>
    <div>
      <input
        type="text"
        name="name"
        bind:value={values.name}
        placeholder="Your name"
      />
    </div>
    <div>
      <input
        type="text"
        name="email"
        bind:value={values.email}
        placeholder="Your email"
      />
    </div>
    <div>
      <input
        type="text"
        name="username"
        bind:value={values.username}
        placeholder="Choose username"
      />
    </div>
    <div>
      <input
        type="password"
        name="password"
        bind:value={values.password}
        placeholder="Password"
      />
    </div>
    <div>
      <input
        type="password"
        name="passwordConfirm"
        bind:value={values.passwordConfirm}
        placeholder="Confirm password"
      />
    </div>
    <div>
      <button type="submit">Register</button>
    </div>
  </form>
</div>
```

Nothing fancy yet. We can fill out the form and submit it. Next, we will add validation and then gradually improve it.

## Validating the form

Now we will try to add our Yup validation schema in the mix. The one we created in the beginning. We can do that in our `submitHandler` so that when the user clicks the form we will first validate the values before submitting the form.

The only thing we need to do is to change our `submitHandler` to this.

```js
const submitHandler = () => {
  regSchema
    .validate(values, { abortEarly: false })
    .then(() => {
      alert(JSON.stringify(values, null, 2));
    })
    .catch(console.log);
};
```

If the form is valid you will get an alert popup with the form values, otherwise we just log the errors to the console.

## Creating custom errors object

Wouldn't it be nice if we could show the errors to the user? Yes, it would!

To achieve that we first need to extract our errors to an object that we can use to display the errors.

For that we will create a helper function.

```js
const extractErrors = ({ inner }) => {
  return inner.reduce((acc, err) => {
    return { ...acc, [err.path]: err.message };
  }, {});
};
```

It might look like a pretty advanced function, but what it basically does is to loop over the Yup's validation `error.inner` array and return a new object consisting of fields and their error messages.

We can now add it to our validation chain. Like this.

```js
const submitHandler = () => {
  regSchema
    .validate(values, { abortEarly: false })
    .then(() => {
      alert(JSON.stringify(values, null, 2));
    })
    .catch(err => console.log(extractErrors(err)));
};
```

If you look at the console output now you will see our custom errors object being logged.

Are you with me so far?

## Displaying errors

Now we need somehow display those errors in correct place. Next to invalid form field.

This is how our new code in `script` tag looks now.

```html
<script>
  import { regSchema } from './schema';

  let values = {};
  let errors = {};

  const extractErrors = err => {
    return err.inner.reduce((acc, err) => {
      return { ...acc, [err.path]: err.message };
    }, {});
  };

  const submitHandler = () => {
    regSchema
      .validate(values, { abortEarly: false })
      .then(() => {
        // submit a form to the server here, etc
        alert(JSON.stringify(values, null, 2));
        // clear the errors
        errors = {};
      })
      .catch(err => (errors = extractErrors(err)));
  };
</script>
```

We have introduced `errors` object that we assign when we submit the form. Now we also need to add individual errors next to our input fields.

```html
<div>
  <h1>Please register</h1>
  <form on:submit|preventDefault={submitHandler}>
    <div>
      <input
        type="text"
        name="name"
        bind:value={values.name}
        placeholder="Your name"
      />
      {#if errors.name}{errors.name}{/if}
    </div>
    <div>
      <input
        type="text"
        name="email"
        bind:value={values.email}
        placeholder="Your email"
      />
      {#if errors.email}{errors.email}{/if}
    </div>
    <div>
      <input
        type="text"
        name="username"
        bind:value={values.username}
        placeholder="Choose username"
      />
      {#if errors.username}{errors.username}{/if}
    </div>
    <div>
      <input
        type="password"
        name="password"
        bind:value={values.password}
        placeholder="Password"
      />
      {#if errors.password}{errors.password}{/if}
    </div>
    <div>
      <input
        type="password"
        name="passwordConfirm"
        bind:value={values.passwordConfirm}
        placeholder="Confirm password"
      />
      {#if errors.passwordConfirm}{errors.passwordConfirm}{/if}
    </div>
    <div>
      <button type="submit">Register</button>
    </div>
  </form>
</div>
```

If add that code and try to submit the form you will see the validation errors. It doesn't look pretty, but it works!

## Adding password validation

We now need to check if the passwords match and therefore we need to go back to our validation schema.

As I wrote in the beginning you can do some advanced validation gymnastics in Yup. To compare if our two passwords match we will use Yup's [oneOf](https://github.com/jquense/yup#mixedoneofarrayofvalues-arrayany-message-string--function-schema-alias-equals) validator.

```js
import * as yup from 'yup';

const regSchema = yup.object().shape({
  name: yup.string().required(),
  email: yup.string().required().email(),
  username: yup.string().required(),
  password: yup.string().required(),
  passwordConfirm: yup
    .string()
    .required()
    .oneOf([yup.ref('password'), null], 'Passwords do not match')
});

export { regSchema };
```
Now if the passwords don't match Yup will show us the error "Passwords do not match".

## Checking the username availability

Not many people know this, but you can also do custom validation in Yup by using the [test](https://github.com/jquense/yup#mixedtestname-string-message-string--function-test-function-schema) method. We will now simulate a call to the server to check if the username is available.

```js
import * as yup from 'yup';

// simulate a network or database call
const checkUsername = username =>
  new Promise(resolve => {
    const takenUsernames = ['jane', 'john', 'elon', 'foo'];
    const available = !takenUsernames.includes(username);
    // if we return `true` then validation has passed
    setTimeout(() => resolve(available), 500);
  });

const regSchema = yup.object().shape({
  name: yup.string().required(),
  email: yup.string().required().email(),
  username: yup
    .string()
    .required()
    .test('usernameTaken', 'Please choose another username', checkUsername),
  password: yup.string().required(),
  passwordConfirm: yup
    .string()
    .required()
    .oneOf([yup.ref('password'), null], 'Passwords do not match')
});

export { regSchema };

```

The test function needs to return a boolean. If `false` is returned then the validation will not pass and error will be displayed.

Notice that we introduced 500ms timeout to username check and since we validate the whole form it will take 500ms for our form to validate itself. The slowest wins.

The case would be different if we validated individual fields instead.

## Providing custom error messages

The message "passwordConfirm is a required field" is not very user friendly. You can provide your own error messages to Yup.

```js
import * as yup from 'yup';

// simulate a network or database call
const checkUsername = username =>
  new Promise(resolve => {
    const takenUsernames = ['jane', 'john', 'elon', 'foo'];
    const available = !takenUsernames.includes(username);
    // if we return `true` then validation has passed
    setTimeout(() => resolve(available), 500);
  });

const regSchema = yup.object().shape({
  name: yup.string().required('Please enter your name'),
  email: yup
    .string()
    .required('Please provide your email')
    .email("Email doesn't look right"),
  username: yup
    .string()
    .required('Username is a manadatory field')
    .test('usernameTaken', 'Please choose another username', checkUsername),
  password: yup.string().required('Password is required'),
  passwordConfirm: yup
    .string()
    .required('Please confirm your password')
    .oneOf([yup.ref('password'), null], 'Passwords do not match')
});

export { regSchema };
```

Ah! Much better!

## Prefer async?

If you fancy async/await over promise chains this is how you can rewrite the `submitHandler`.

```js
const submitHandler = async () => {
  try {
    await regSchema.validate(values, { abortEarly: false });
    alert(JSON.stringify(values, null, 2));
    errors = {};
  } catch (err) {
    errors = extractErrors(err);
  }
};
```

## Summary

This was a very basic example of how you can do custom form validation in Svelte with the help of external and specialized validation library - Yup. Hope that you got the idea.

Form validation is a big area to explore and everything would not fit into a single article. I've not included `onfocus` and `onblur` field validations for example. Not error CSS classes and nested forms either.

I am thinking of **writing a short book** about all the things I've learned when working with **Svelte forms** like different kinds of validation, dynamic fields and clever abstractions. Let me know if you would be interested.

Here is the full code https://github.com/codechips/svelte-yup-form-validation

Thank you for reading!