Clean, Scalable Forms with Vue Composition API
Tags: vue, compositionapi, forms
Published: 2020-03-31 00:00:00

---
layout: post
title:  Clean, Scalable Forms with Vue Composition API
description: "Forms are one of the trickiest parts of frontend development and where you'll likely find a lot of messy code. In this tutorial, you'll see how the new Vue Composition API will make your form code much cleaner and more scalable."
date: 2020/03/31
tags: [vue.js,composition api,forms]
code: xl5
cover_image: https://vuejsdevelopers.com/images/posts/versions/vue_composition_1200.webp
level: intermediate
ad: ev
published: true
---
Forms are one of the trickiest parts of frontend development and where you'll likely find a lot of messy code.

Component-based frameworks like Vue.js 2 have done a lot to improve the scalability of frontend code, but the problem of forms has persisted.

In this tutorial, I'll show you how the new Vue Composition API (coming to Vue 3) will make your form code much cleaner and more scalable.


>*Note: this article was originally posted [here on the Vue.js Developers blog](https://vuejsdevelopers.com/2020/03/31/vue-js-form-composition-api?utm_source=devto&utm_medium=article&utm_campaign=xl5) on 2020/03/30.*


## Why form code often sucks

The key design pattern of component-based frameworks like Vue is *component composition*. This pattern tells us to abstract the features of our app into isolated, single-purpose components that communicate state with props and events.

However, forms can't be abstracted very neatly under this pattern because the functionality and state of a form doesn't clearly belong to any one component and so separating it often causes as many problems as it solves.

Another important reason form code often sucks in Vue apps is that, up until Vue 2, Vue has not had a strong means of reusing code between components. This is important in forms as form inputs are often distinctly different but share many similarities in functionality.

The main method of code reuse offered by Vue 2 is *mixins* which [many would argue are a blatant anti-pattern](https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html). 

## The Vue Composition API

The Composition API is a new way of defining components with Vue.js and will be a core feature of Vue 3. It's also available to use today in Vue 2 as a plugin.

This new API is designed to combat some of the issues I've mentioned (not just in forms but in any aspect of frontend app architecture).

> If you're still new to the Composition API or aren't clear what it's for, I recommend you first read the [docs](https://vue-composition-api-rfc.netlify.com/) and also another article I wrote, [When To Use The New Vue Composition API (And When Not To)](https://vuejsdevelopers.com/2020/02/17/vue-composition-api-when-to-use).

The Composition API is not a replacement for the classic Vue API, but something you can use when it's called for. As you'll see in this article, creating clean and scalable form code is a perfect use case.

## Adding the Composition API to a Vue 2 project

Since I'm writing this tutorial before Vue 3 has been released, let's add the Composition API to a Vue 2 project as a plugin.

We'll begin by creating a new Vue CLI project (just the bare features is all we need - no router, Vuex, etc) and install the Composition API plugin with NPM.

```bash
$ vue create composition-api-form
$ cd composition-api-form
$ npm i -S @vue/composition-api
```

Next, let's add the plugin to our Vue instance in *main.js*.

*src/main.js*

```js
import Vue from "vue";
import App from "./App.vue";

import VueCompositionApi from "@vue/composition-api";
Vue.use(VueCompositionApi);

new Vue({
  render: h => h(App)
}).$mount('#app');
```

## Creating form input components

To make this a simple example, we're going to create a form with just two inputs - a name, and and email. Let's create these as their own separate components.

```bash
$ touch src/components/InputName.vue
$ touch src/components/InputEmail.vue
```

Let's now set up the *InputName* component template in the typical way including an HTML input element with the `v-model` directive creating a two-way binding with the component.

*src/components/InputName.vue*

```html
<template>
  <div>
    <label>
      Name
      <input type="text" v-model="input" name="name" />
    </label>
  </div>
</template>
<script>
export default {
  name: 'InputName'
}
</script>
```

## Setting up the form

Let's leave the input for now and set up the form. You could create this as a separate component to make it reusable, but for simplicity of the tutorial, I'll just declare it in the *App* component template.

We'll add the `novalidate` attribute to let the browser know we'll be supplying custom validation. We'll also listen to the `submit` event of the form, prevent it from auto-submitting, and handle the event with an `onSubmit` method which we'll declare shortly.

We'll then add the `InputName` and `InputEmail` components and bind local state values `name` and `email` to them respectively.

*src/App.vue*

```html
<template>
  <div id="app">
    <form novalidate @submit.prevent="onSubmit">
      <InputName v-model="name" />
      <InputEmail v-model="email" />
      <button type="submit">Submit</button>
    </form>
  </div>
</template>
<script>
import InputName from "@/components/InputName";
import InputEmail from "@/components/InputEmail";
export default {
  name: 'App',
  components: {
    InputName,
    InputEmail
  }
}
</script>
```

Let's now define the form functionality using the Composition API. We'll add a `setup` method to the component definition where we'll declare two state variables `name` and `email` using the `ref` method of the Composition API. This method will need to be imported from the Composition API package.

We'll then declare an `onSubmit` function to handle the form submission. I won't specify any functionality since it's irrelevant to this tutorial.

Finally, we need to return the two state variables and the method we've created from the `setup` function so that they're accessible to the component's template.

*src/App.vue*

```js
...
import { ref } from "@vue/composition-api";

export default {
  name: "App",
  setup () {
    const name = ref("");
    const email = ref("");
    function onSubmit() {
      // submit to backend or whatever you like
      console.log(name.value, email.value);
    }
    return {
      name,
      email,
      onSubmit
    }
  },
  ...
}
```

## Setting up the inputs

Next, we're going to define the functionality of the `InputName` component. 

Since the parent form is using `v-model` with this component, it's important to declare a prop `value` which will be one half of the two-way binding.

Let's create a `setup` function. Props get passed into this method, as does a context object, giving us access to component instance methods. We can destructure this second argument and get the `emit` method. We'll need this to fulfill the other half of the `v-model` two-way binding i.e. to reactively emit new values of the input.

Before we get to that, let's declare a state variable `input` that will be bound to the input HTML element we declared in the template. 

The value of this variable will be something we'll return from a to-be-defined composition function `useInputValidator`. This function will handle all the common validation logic. 

We'll pass in the `value` prop to this method, and the second argument will be a callback function that returns the validated input value. Let's use this callback to emit this input as an event and fulfill the `v-model` contract.

*src/components/InputName.vue*

```js
import useInputValidator from "@/features/useInputValidator";

export default {
  name: "InputName",
  props: {
    value: String
  },
  setup (props, { emit }) {
    const { input } = useInputValidator(
      props.value, 
      value => emit("input", value)
    );
    return {
      input
    }
  }
}
```

## Input validator feature

Let's now create the `useInputValidator` composition function. To do so, we'll first create a `features` folder, and then create a module file for it.

```bash
$ mkdir src/features
$ touch src/features/useInputValidator.js
```

In the module file, we're going to export a function. We just saw it will need two arguments - the `value` prop received from the parent form, which we'll call `startVal`, and a callback method we'll call `onValidate`.

Remember that this function needs to return an `input` state variable, so let's go ahead and declare that, assigning a `ref` which is initialized with the value provided by the prop.

Before we return the `input` value from the function, let's watch its value and call the `onValidate` callback using the input as an argument.

*src/features/useInputValidator.js*

```js
import { ref, watch } from "@vue/composition-api";

export default function (startVal, onValidate) {
  let input = ref(startVal);
  watch(input, value => { 
    onValidate(value);
  });
  return {
    input
  }
}
```

<content-ad />

## Adding validators

The next step is to add validator functions. For the `InputName` component, we just have one validation rule - a *minLength* ensuring the input is three characters or more. The yet-to-be-created `InputEmail` component will need an email validation.

We'll now create these validators in a JavaScript utility module `validators.js` in the `src` folder. In a real project, you'd probably use a third-party library instead.

I won't go through the validator functions in any great detail, but here are two important things to note:

- These are functions that return functions. This architecture allows us to customize the validation by passing arguments that become part of the closure. 
- The returned function from each validator always returns either a string (the error message) or `null` in the case that there is no error.

*src/validators.js*

```js
const minLength = min => {
  return input => input.length < min 
  ? `Value must be at least ${min} characters` 
  : null;
};

const isEmail = () => {
  const re = /\S+@\S+\.\S+/;
  return input => re.test(input)
  ? null
  : "Must be a valid email address";
}

export { minLength, isEmail };
```

Back in the composition function, we want the consuming component to define the validations it needs, so let's begin by adding another argument to the function profile `validators` which should be an array of validation functions.

Inside the `input` watcher, we'll now process the validation functions. Let's use the `map` method of the validators array, passing in the current value of the input to each validator method. 

The return will be captured in a new state variable, `errors`, which we'll also return to the consuming component.

*src/features/useInputValidator.js*

```js
export default function (startVal, validators, onValidate) {
  const input = ref(startVal);
  const errors = ref([]);
  watch(input, value => {
    errors.value = validators.map(validator => validator(value));
    onValidate(value);
  });
  return {
    input,
    errors
  }
}
```

Returning finally to the `InputName` component, we're now going to provide the required three arguments to the `useInputValidator` method. Remember, the second argument is now an array of validators, so let's declare an array in-place and pass in `minLength` which we'll get by importation from the validators file.

`minLength` is a factory function, so we call the function passing in the minimum length we want to specify.

We also get two objects returned from our composition function now - `input` and `errors`. Both of these will be returned from the `setup` method for availability in the component's render context.

*src/components/InputName.vue*
```js
...
import { minLength } from "@/validators";

export default {
  ...
  setup (props, { emit }) {
    const { input, errors } = useInputValidator(
      props.value, 
      [ minLength(3) ],
      value => emit("input", value)
    );
    return {
      input,
      errors
    }
  }
}
```

That's the last of the functionality that we'll add to this component. Before we move on though, it's important to take a moment and appreciate how much more readable this code is than what you'd see if we were using mixins. 

For one thing, we clearly see where our state variables are declared and modified without having to flick over to a separate mixin module file. For another thing, we don't need to be concerned about name clashes between our local variables and the composition function.

## Displaying errors

Going to the template of our `InputName` component, we now have an array of potential errors to display. Let's delegate this to a presentation component called `ErrorDisplay`.

*src/components/InputName.vue*
```html
<template>
  <div>
    <label>
      Name
      <input type="text" v-model="input" name="name" />
    </label>
    <ErrorDisplay :errors="errors" />
  </div>
</template>
<script>
...
import ErrorDisplay from "@/components/ErrorDisplay";

export default: {
  ...
  components: {
    ErrorDisplay
  }
}
</script>
```

The functionality of `ErrorDisplay` is too trivial to show here.

## Reusing code

So that's the basic functionality of our Composition API-based form. The objective of this tutorial was to create clean and scalable form code and I want to prove to you that we've done this by finishing off with the definition of our second custom input, `InputEmail`. 

If the objective of this tutorial has been met you should have no trouble understanding it without my commentary!

*src/components/InputEmail*

```html
<template>
  <div>
    <label>
      Email
      <input type="email" v-model="input" name="email" />
    </label>
    <ErrorDisplay v-if="input" :errors="errors" />
  </div>
</template>
<script>
import useInputValidator from "@/features/useInputValidator";
import { isEmail } from "@/validators";
import ErrorDisplay from "./ErrorDisplay";

export default {
  name: "InputEmail",
  props: {
    value: String
  },
  setup (props, { emit }) {
    const { input, errors } = useInputValidator(
      props.value, 
      [ isEmail() ], 
      value => emit("input", value)
    );
    return {
      input,
      errors
    }
  },
  components: {
    ErrorDisplay
  }
}
</script>
```

* * *

**Enjoy this article?**

Get more articles like this in your inbox weekly with the *Vue.js Developers Newsletter*.

[Click here to join!](https://vuejsdevelopers.com/newsletter?utm_source=devto&utm_medium=article)

* * *