im

Svelte form validation with Yup

Learn what Yup is, why it's awesome and how to use it with Svelte

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.

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. 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 library, because it's a pure 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.

# 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:

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.

// 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 and if you ever used Hapi.js 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.

<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 to render it on the page.

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

{
"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.

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.

<!-- 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.

<!-- 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.

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.

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.

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.

<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.

<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 validator.

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 method. We will now simulate a call to the server to check if the username is available.

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.

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.

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.

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

If you liked the article you will love my book - Svelte Forms: From WTF to FTW!. By reading it you will learn everything about working with forms in Svelte effectively, while learning deeper Svelte concepts at the same time.