Nefe James Nefe is a frontend developer who enjoys learning new things and sharing his knowledge with others.

Form validation in Svelte

9 min read 2628

Forms remain an integral part of how users interact with the web. When dealing with forms, we have to handle tracking user input, validating and displaying errors, and handling the form submission.

In this article, we will learn how input bindings work in Svelte, how to validate forms with Yup, and how svelte-forms-lib makes managing forms easier. We will also build a sample form and validate it with these tools to demonstrate the many options you have when building and validating forms in Svelte.

Understanding Svelte input bindings

We need a way to track and store the value of input fields as the user type. Svelte provides two directives to achieve that: on:input and bind.

on:input

This event listener is called whenever an input event takes place:

<script>
  let email = "";
  const handleInput = (e) => {
    email = e.target.value;
  };
</script>

<input type="email" name="email" on:input={handleInput} />
<p>{email}</p>

In the code above, we define a handleInput and pass it to the email input field. Whenever the user types, the email variable gets updated with the value of the field.

bind:value

The bind directive is a cleaner way to work with tracking form values in Svelte:

<input type="email" name="email" bind:value={email} />

Instead of creating a handleInput event and having to set event.target.value for each input field in a given form, bind handles that for us, and whenever we fill the input, the email variable gets updated.

In this article, we will use the bind directive to track and store form values, as it is an easier way to work.

Validation with Yup

Yup is a JavaScript object schema validator. Yup ensures that the data in an object is in the form and shape we want it to be:

import * as yup from 'yup';

let values = {
    email: "",
    password: "",
    confirmPassword: "",
    hobby: "",
    checkbox: false,
};

const schema = yup.object().shape({
  email: yup.string().required("Email is required")
    .email("Email is invalid"),
  password: yup.string().required("Password is required"),
  confirmPassword: yup.string().required("Please confirm your password")
    .oneOf([yup.ref("password"), null], "Passwords do not match"),
  hobby: yup.string().required("Hobby is required"),
  checkbox: yup.bool().required("Checkbox must be accepted")
    .oneOf([true], "Checkbox must be accepted"),
});

const validationResult = schema
    .validate(values, { abortEarly: false })
    .then(() => {
      alert(JSON.stringify(values, null, 2));
    })
    .catch((err) => {
      console.log(err.errors);
    });

    //console.log message 
    //[ "Email is invalid", "Passwords do not match", "Hobby is required", "Che//ckbox must be accepted" ]

In schema, we define how we want the data of our form values to look. This ensures that the data that gets sent to the server is valid.

We made a custom demo for .
No really. Click here to check it out.

We validate objects in Yup using its validate method. We can call this method on any schema we define.

Creating a profile form

Now that we understand how form binding works in Svelte and how Yup validates object values, let us set up a sample profile form and validate it:

<script>
  import schema from './schema';
  let values = {
    //store form data that will then be validated
  };

  const handleSubmit = () => {
    //validate form and submit data
  };
</script>

<div class="container">
  <h1>Profile Form</h1>
  <form on:submit|preventDefault={handleSubmit}>
    <div>
      <input type="text" name="email" bind:value={values.email} 
        placeholder="Email"
      />
    </div>
    <div>
      <input type="password" name="password" bind:value={values.password}
        placeholder="Password"
      />
    </div>
    <div>
      <input type="password" name="confirmPassword"
        bind:value={values.confirmPassword}
        placeholder="Confirm password"
      />
    </div>
    <div>
      <select name="hobby" bind:value={values.hobby}>
        <option value="">Select a hobby</option>
        <option value="Eating">Eating</option>
        <option value="Reading">Reading</option>
        <option value="Sleeping">Sleeping</option>
      </select>
    </div>
    <div>
      <label for="checkbox">Check this box</label>
      <input name="checkbox" type="checkbox" bind:checked={values.checkbox} />
    </div>
  </form>
</div>

We start by setting up a simple profile form to capture a user’s data. We bind the form fields to a values object. This object is where we will store the data from the form.

Validating the profile form

Now that we have created the profile form, we need to validate it.

Unlike what we did when we logged the errors to the console, we want to display them for the user to see:

<script>
  let errors = {};

  const handleSubmit = async () => {
    try {
      await schema.validate(values, { abortEarly: false });
      alert(JSON.stringify(values, null, 2));
      errors = {};
    } catch (err) {
      errors = err.inner.reduce((acc, err) => {
        return { ...acc, [err.path]: err.message };
      }, {});
    }
  };
</script>

In this code block, we create an errors object where we will store the errors we get back from the validate call. Then, we create an async function, handleSubmit. Here, we will handle the form validation and submission.

We pass the data we want to validate to this method. In this case, we will be validating the values received from a form.

Validate can take a second parameter, an options object. Validation returns on the first error by default. To get all the errors returned, we must set abortEarly to false.

If there are no errors, we display the form values. If there are, we display the errors. However, before we can display the errors, we must access them:

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

To access the errors, we loop over Yup’s validation error.inner array and return a new object consisting of fields and their error messages. Then we update the errors object with the errors for each corresponding input field.

Displaying validation errors

Now that we have the errors object that holds the error for each input field, we need to display them:

 <div>
      <input type="email" />
      {#if errors.email}
        <span class="error">{errors.email}</span>
      {/if}
    </div>
    <div>
      <input type="password" />
      {#if errors.password}
        <span class="error">{errors.password}</span>
      {/if}
    </div>
    <div>
      <input type="password" />
      {#if errors.confirmPassword}
        <span class="error">{errors.confirmPassword}</span>
      {/if}
    </div>
    <div>
      <select name="hobby" bind:value={values.hobby}>
        <option value="">Select a hobby</option>
        <option value="Eating">Eating</option>
        <option value="Reading">Reading</option>
        <option value="Sleeping">Sleeping</option>
      </select>
      {#if errors.hobby}
        <span class="error">{errors.hobby}</span>
      {/if}
    </div>
    <div>
      <input name="checkbox" type="checkbox" bind:checked={values.checkbox} />
      {#if errors.checkbox}
        <span class="error">{errors.checkbox}</span>
      {/if}
    </div>

We set up an if block to handle displaying the error. If an error exists for a particular field, we display the error of that field. This CodeSandbox link holds the code for this section.

Validation with svelte-forms-lib

Svelte forms lib is a Formik-inspired library for building forms easily in a Svelte project.

You can install svelte-forms-lib with the following:

npm i svelte-forms-lib

First, we import the createForm function from svelte-forms-lib:

import { createForm } from "svelte-forms-lib";

This function is the core part of integrating svelte-forms-lib into a form.

CreateForm gives us access to useful form helps like handleChange and handleSubmit, among others. We will need these helper functions to set up the form:

<script>
  import { createForm } from "svelte-forms-lib";
  const { form, handleChange, handleSubmit } = createForm({
    initialValues: {
      email: "",
      password: "",
      confirmPassword: "",
      hobby: "",
      checkbox: "",
    },
    onSubmit: (values) => {
      alert(JSON.stringify(values));
    },
  });
</script>
<div class="container">
  <h1>Registration Form</h1>
  <form on:submit|preventDefault={handleSubmit}>
    <div>
      <input
        type="text"
        name="email"
        bind:value={$form.email}
        placeholder="Email"
        on:change={handleChange}
      />
    </div>
    <div>
      <input
        type="password"
        name="password"
        bind:value={$form.password}
        placeholder="Password"
        on:change={handleChange}
      />
    </div>
    <div>
      <input
        type="password"
        name="confirmPassword"
        bind:value={$form.confirmPassword}
        placeholder="Confirm password"
        on:change={handleChange}
      />
    </div>
    <div>
      <select name="hobby" bind:value={$form.hobby} on:blur={handleChange}>
        <option value="">Select a hobby</option>
        <option value="Eating">Eating</option>
        <option value="Reading">Reading</option>
        <option value="Sleeping">Sleeping</option>
      </select>
    </div>
    <div>
      <label for="checkbox">Check this box</label>
      <input
        name="checkbox"
        type="checkbox"
        bind:checked={$form.checkbox}
        on:change={handleChange}
      />
    </div>
    <div>
      <button type="submit">Register</button>
    </div>
  </form>
</div>

Aside from helper functions, svelte-forms-lib exposes observable values that give us information on the current state of the form. In this article, we will focus on working with the form and errors observables. However, you can check out the full list of available observables here.

We pass a config object as an argument to createForm. Here, we define the initialValues of the form and an onSubmit handler that will handle the form submission.

Having configured createForm, we need to hook the profile form up to svelte-forms-lib, so it can track the form values and handle the submission.

To do that, we pass the handleSubmit helper to the form element. We also pass handleChange to the input fields and bind their values to the form observable.

Custom validation in svelte-forms-lib

Now that we know how to integrate svelte-forms-lib into a form, we need to handle the form validation:

<script>
  import { createForm } from "svelte-forms-lib";
  const { form, errors, handleChange, handleSubmit } = createForm({
    initialValues: {},
    validate: (values) => {
      let errors = {};
      if (!values.email) {
        errors.email = "Email is Required";
      } else if (!/^[^\[email protected]][email protected][^\[email protected]]+\.[^\[email protected]]+$/.test(values.email)) {
        errors.email = "Invalid emaill address";
      }
      if (!values.password) {
        errors["password"] = "password is required";
      }
      if (!values.confirmPassword) {
        errors["confirmPassword"] = "confirm password is required";
      } else if (values.confirmPassword !== values.password) {
        errors["confirmPassword"] = "password does not match";
      }
      if (!values.hobby) {
        errors["hobby"] = "hobby is required";
      }
      if (!values.checkbox) {
        errors.checkbox = "You must accept our terms";
      }
      return errors;
    },
    onSubmit: (values) => {
      alert(JSON.stringify(values));
    },
  });
</script>
<div class="container">
  <h1>Registration Form</h1>
  <form on:submit|preventDefault={handleSubmit}>
    <div>
      <input
        type="text"
        name="email"
        bind:value={$form.email}
        placeholder="Email"
        on:change={handleChange}
      />
      {#if $errors.email}
        <span class="error">{$errors.email}</span>
      {/if}
    </div>
    <div>
      <input
        type="password"
        name="password"
        bind:value={$form.password}
        placeholder="Password"
        on:change={handleChange}
      />
      {#if $errors.password}
        <span class="error">{$errors.password}</span>
      {/if}
    </div>
    <div>
      <input
        type="password"
        name="confirmPassword"
        bind:value={$form.confirmPassword}
        placeholder="Confirm password"
        on:change={handleChange}
      />
      {#if $errors.confirmPassword}
        <span class="error">{$errors.confirmPassword}</span>
      {/if}
    </div>
    <div>
      <select name="hobby" bind:value={$form.hobby} on:blur={handleChange}>
        <option value="">Select a hobby</option>
        <option value="Eating">Eating</option>
        <option value="Reading">Reading</option>
        <option value="Sleeping">Sleeping</option>
      </select>
      {#if $errors.hobby}
        <span class="error">{$errors.hobby}</span>
      {/if}
    </div>
    <div>
      <label for="checkbox">Check this box</label>
      <input
        name="checkbox"
        type="checkbox"
        bind:checked={$form.checkbox}
        on:change={handleChange}
      />
      {#if $errors.checkbox}
        <span class="error">{$errors.checkbox}</span>
      {/if}
    </div>
    <div>
      <button type="submit">Register</button>
    </div>
  </form>
</div>

Aside from configuring createForm with an initialValues object and an onSubmit function, we can also add a validate callback to handle the form validation.

Here, we check the state of each input field and based on that state, update the errors object. Whenever there are errors in any input field, we display them in an if block.

Yup validation in svelte-forms-lib

While we can create a custom validation for our form, we also have the option of passing that responsibility to Yup.

We will work with the same schema validation object we just created:

<script>
  import schema from "./schema";
  import { createForm } from "svelte-forms-lib";
  const { form, errors, handleChange, handleSubmit } = createForm({
    initialValues: {
      //initial values here
    },
    validationSchema: schema,
    onSubmit: (values) => {
      alert(JSON.stringify(values));
    },
  });
</script>

//profile form below

Svelte-forms-lib provides support with Yup validation through a validationSchema prop which takes in a schema object. We pass in the schema we defined. You can find the CodeSandbox link to this section here.

Custom form components in svelte-forms-lib

So far, we have had to pass in handleSubmit to the form, bind each field to their respective value, and pass in handleChange to each field.

While this gets the job done, svelte-forms-lib provides a better and less repetitive way to work with forms: custom components.

These components will reduce the boilerplate and make the form code very concise:

<script>
  import { Form, Field, ErrorMessage, Select } from "svelte-forms-lib";
  import schema from "./schema";
  const formProps = {
    initialValues: {},
    validationSchema: schema,
    onSubmit: (values) => {
      alert(JSON.stringify(values));
    },
  };
</script>
<div class="container">
  <h1>Registration Form</h1>
  <Form {...formProps}>
    <div>
      <Field type="email" name="email" placeholder="Email" />
      <ErrorMessage name="email" />
    </div>
    <div>
      <Field type="password" name="password" placeholder="Password" />
      <ErrorMessage name="password" />
    </div>
    <div>
      <Field type="password" name="confirmPassword" placeholder="Password" />
      <ErrorMessage name="confirmPassword" />
    </div>
    <div>
      <Select name="hobby">
        <option value="">Select a hobby</option>
        <option value="Eating">Eating</option>
        <option value="Reading">Reading</option>
        <option value="Sleeping">Sleeping</option>
      </Select>
      <ErrorMessage name="hobby" />
    </div>
    <div>
      <label for="checkbox">Check this box</label>
      <Field type="checkbox" name="checkbox" />
      <ErrorMessage name="hobby" />
    </div>
    <div>
      <button type="submit">Register</button>
    </div>
  </Form>
</div>

//profile form below

Here, we make use of the <Form/> , <Field/> , <Select/>, and <ErrorMessage/> components.

We pass in initialValues, onSubmit, and validationSchema to <Form/> through the formProps variable we define. The name and type are needed for <Field/> to work properly and render the appropriate input type.

For <ErrorMessage/>, we pass the in the name of the input field we want to track, and if there is an error for that input, <ErrorMessage/> will display the error. We no longer need to conditionally render the error ourselves.

You can find the CodeSandbox link to this section here.

Conclusion

Creating forms in Svelte can be both simple and very complicated. In this article, we have learned how to track and store input values in Svelte, handle validation with Yup, how svelte-forms-lib works, and the different ways we can integrate this awesome library into our forms.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Nefe James Nefe is a frontend developer who enjoys learning new things and sharing his knowledge with others.

Leave a Reply