Brian Neville-O'Neill
Follow
Brian is the head of content at LogRocket. If the code is wrong or you spot a typo, it's probably his fault.
· 10 min read

An imperative guide to forms in Vue.js

Something almost all web apps have in common is the fact that they need to get input from users, validate it, and act on it. Learning to…

Something almost all web apps have in common is the fact that they need to get input from users, validate it, and act on it. Learning to work with forms properly in our favourite frameworks is valuable, and can save us some time and energy during development. In this tutorial, I will walk you through the process of creating, validating, and utilising inputs from a form in a Vue.js 2.x application.

To follow along with this tutorial, you will need some knowledge of HTML and Vue.js. The entire demo can be forked and played around with on CodePen.

https://codepen.io/olayinkaos/full/GMmpPm/

Setting up

We will start by creating a simple Vue.js app with some basic HTML markup. We will also import Bulma to take advantage of some pre-made styles:

<!DOCTYPE html>
<html>
<head>
  <title>Fun with Forms in Vue.js</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.4/css/bulma.min.css">
</head>

<body>
  <div class="columns" id="app">
    <div class="column is-two-thirds">
      <section class="section">
        <h1 class="title">Fun with Forms in Vue 2.0</h1>
        <p class="subtitle">
          Learn how to work with forms, including <strong>validation</strong>!
        </p>
        <hr>      
        
        <!-- form starts here -->
        <section class="form">

</section>
      </section>
    </div>
  </div>
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>

<script>
  new Vue({
    el: '#app'
  })
</script>

</body>
</html>

Binding input values

We can bind form input and textarea element values to the Vue instance data using the v-model directive.

Text

Let’s get started by creating a simple text input to get a user’s full name:

...
<section class="form">
  <div class="field">
    <label class="label">Name</label>
    <div class="control">
      <input v-model="form.name" class="input" type="text" placeholder="Text input">
    </div>
  </div>
</section>
...

<script>
new Vue({
  el: '#app',
  data: {
    form : {
      name: ''
    }
  }
})
</script>

In the above code, we define the data option in our Vue instance and define a form object which will hold all the properties we want for our form. The first property we define is name which is bound to the text input we also created.

Now that two-way binding exists, we can use the value of form.name anywhere in our application, as it will be the updated value of the text input. We can add a section to view all the properties of our form object:

...
<div class="columns" id="app">
  <!-- // ... -->

<div class="column">
    <section class="section" id="results">
      <div class="box">
        <ul>          
          <!-- loop through all the `form` properties and show their values -->
          <li v-for="(item, k) in form">
            <strong>{{ k }}:</strong> {{ item }}
          </li>
        </ul>
      </div>
    </section>
  </div>
</div>
...

If you’re following along properly, you should have the same result as the fiddle below. Try typing in the input box:

Note that v-model will ignore the value, checked or selected attributes of form inputs and will treat the Vue instance data as the source of truth. This means that you can also specify a default value for form.name in the Vue instance, and that is what the initial value of the form input will be.

Textarea

These work the same way regular input boxes work:

...
<div class="field">
  <label class="label">Message</label>
  <div class="control">
    <textarea class="textarea" placeholder="Message" v-model="form.message"></textarea>
  </div>
</div>
...

And the corresponding value in the form model:

data: {
  form : {
    name: '',  
    message: '' // textarea value
  }
}

It’s important to note that interpolation in textarea — <textarea>{{ form.message}}</textarea> will not work for two-way binding, use the v-model directive instead.

Select

The v-model directive can also be easily plugged in for select boxes. The defined model will be synced with the value of the selected option:

...
<div class="field">
  <label class="label">Inquiry Type</label>
  <div class="control">
    <div class="select">
      <select v-model="form.inquiry_type">
        <option disabled value="">Nothing selected</option>
        <option v-for="option in options.inquiry" v-bind:value="option.value">
          {{ option.text }}
        </option>
      </select>
    </div>
  </div>
</div>
...

In the above code, we chose to load the options dynamically using the v-for directive. This requires us to also define the available options in the Vue instance:

data: {
  form : {
    name: '',
    message: '',
    inquiry_type: '' // single select box value
  },
  options: {
    inquiry: [
      { value: 'feature', text: "Feature Request"},
      { value: 'bug', text: "Bug Report"},
      { value: 'support', text: "Support"}
    ]
  }
}

The process is similar for a multi-select box, the difference being that the selected values for the multi-select box are stored in an array. For example:

...
<div class="field">
  <label class="label">LogRocket Usecases</label>
  <div class="control">
    <div class="select is-multiple">
      <select multiple v-model="form.logrocket_usecases">
        <option>Debugging</option>
        <option>Fixing Errors</option>
        <option>User support</option>
      </select>
    </div>
  </div>
</div>
...

And defining the corresponding model property:

data: {
  form : {
    name: '',
    message: '',
    inquiry_type: '',
    logrocket_usecases: [], // multi select box values
  }, 
  // ..
}

In the example above, the selected values will be added to the logrocket_usecases array.

Checkbox

Single checkboxes which require a boolean (true/false) value can be implemented like this:

...
<div class="field">
  <div class="control">
    <label class="checkbox">
      <input type="checkbox" v-model="form.terms">
      I agree to the <a href="#">terms and conditions</a>
    </label>
  </div>
</div>
...

Defining the corresponding model property:

data: {
  form : {
    name: '',
    message: '',
    inquiry_type: '',
    logrocket_usecases: [],
    terms: false, // single checkbox value
  }, 
  // ..
}

The value of form.terms in the example above will either be true or false depending on whether the checkbox is checked or not. A default value of false is given to the property, hence the initial state of the checkbox will be unchecked.

For multiple checkboxes, they can simply be bound to the same array:

...
<div class="field"> 
  <label>
    <strong>What dev concepts are you interested in?</strong>
  </label>
  <div class="control">
    <label class="checkbox">
      <input type="checkbox" v-model="form.concepts"
        value="promises">
      Promises
    </label> 
    <label class="checkbox">
      <input type="checkbox" v-model="form.concepts" 
        value="testing">
      Testing
    </label>
  </div>
</div>
...

data: {
  form : {
    name: '',
    message: '', 
    inquiry_type: '', 
    logrocket_usecases: [],
    terms: false,
    concepts: [], // multiple checkbox values
  }, 
  // ..
}

Radio

For radio buttons, the model property takes the value of the selected radio button. Here’s an example:

...
<div class="field">
  <label><strong>Is JavaScript awesome?</strong></label>

<div class="control">
    <label class="radio">
      <input v-model="form.js_awesome" type="radio" value="Yes">
      Yes
    </label>
    <label class="radio">
      <input v-model="form.js_awesome" type="radio" value="Yeap!">
      Yeap!
    </label>
  </div>
</div>
...

data: {
  form : {
    name: '',
    message: '',
    inquiry_type: '',
    logrocket_usecases: [],
    terms: false,
    concepts: [],
    js_awesome: '' // radio input value
  }, 
  // ..
}

Validating user inputs

While writing custom validation logic is possible, there is already a great plugin that helps validate inputs easily and display the corresponding errors called vee-validate.

First, we need to include the plugin in our app. This can be done with yarn or npm, but in our case, including via CDN is just fine:

<script src="https://unpkg.com/vee-validate@2.0.0-rc.18/dist/vee-validate.js"></script>

To setup the plugin, we will place this just right above our Vue instance:

Vue.use(VeeValidate);

Now we can use the v-validate directive on our inputs and pass in the rules as string values. Each rule can be separated by a pipe. Taking a simple example, let’s validate the name input we defined earlier:

...
<div class="field">
  <label class="label">Name</label>
  <div class="control">
    <input name="name" 
      v-model="form.name" 
      v-validate="'required|min:3'" 
      class="input" type="text" placeholder="Full name">
  </div> 
</div>
...

In the above example, we defined two rules — the first is that the name field is required (required), the second is that the minimum length of any value typed in the name field should be three (min:3).

Tip: Rules can be defined as objects for more flexibility, for example:v-validate=”{required: true, min: 3}”

To access the errors when these rules aren’t passed, we can use the errors helper object created by the plugin. For example, to display the errors just below the input, we can add this:

<p class="help is-danger" v-show="errors.has('name')">
  {{ errors.first('name') }}
</p>

In the code above, we take advantage of a couple of helper methods from the vee-validate plugin to display the errors:

  • .has() helps us check if there are any errors associated with the input field passed in as a parameter. It returns a boolean value(true/false).
  • .first() returns the first error message associated with the field passed in as a parameter.

Other helpful methods include .collect(), all() and any(). You can read more on them here.

Note that the name attribute needs to be defined on our input fields as this is what vee-validate uses to identify fields.

Finally, we can add an is-danger class (provided by Bulma) to the input field to indicate when there’s a validation error for the field. This would be great for the user experience, as users would immediately see when an input field hasn’t been filled properly. The entire field markup will now look like this:

...
<div class="field">
  <label class="label">Name</label>
  <div class="control">
    <input name="name" 
      v-model="form.name" 
      v-validate="'required|min:3'" 
      v-bind:class="{'is-danger': errors.has('name')}"
      class="input" type="text" placeholder="Full name">
  </div>
  <p class="help is-danger" v-show="errors.has('name')">
    {{ errors.first('name') }}
  </p> 
</div>
...

You can see the work in progress so far in the results tab in the fiddle below:

Vee-validate has a good number of predefined rules that cater to the generic use-cases — the full list of available rules can be found here. Custom validation rules can also be defined if your form has any needs that aren’t covered by the generic rules.

Custom validation rules

We can create custom rules using the Validator.extend() method. Let’s add a validation method that forces our users to be polite when sending messages:

VeeValidate.Validator.extend('polite', { 
  getMessage: field => `You need to be polite in the ${field} field`,
  validate: value => value.toLowerCase().indexOf('please') !== -1
});

Our validator consists of two properties:

  • getMessage(field, params): Returns a string — the error message when validation fails.
  • validate(value, params): Returns a boolean, object, or Promise. If a boolean isn’t returned, the valid(boolean) property needs to be present in the object or Promise.

Vue-validate was also built with localisation in mind. You can view notes on translation and localisation in the full vue-validate custom rules docs.

Form submission

To handle form submissions, we can make use of Vue’s submit event handler. We can also plug in the .prevent modifier to prevent the default action, which in this case would be the page refreshing when the form is submitted:

...
<form v-on:submit.prevent="console.log(form)"> 
  ...
  <div class="field is-grouped">
    <div class="control">
      <button class="button is-primary">Submit</button>
    </div>
  </div>
</form>
...

In the above example, we simply log the entire form model to the console on form submission.

We can add one final touch with vee-validate to make sure that users aren’t allowed to submit an invalid form. We can achieve this using errors.any():

<button 
  v-bind:disabled="errors.any()"
  class="button is-primary">
  Submit
</button>

In the above code, we disable the submit button once any errors exist in our form.

Great! Now we have built a form from scratch, added validation and can use the input values from the form. You can find the entire code we have worked on here on CodePen.

Final notes

Some other key things to note:

  • Some modifiers exist for v-model on form inputs, including .lazy, .number and .trim. You can read all about them here.
  • Dynamic properties can be bound to input values using v-bind:value. Check the docs on Value Bindings for more info.

In this guide, we have learned how to create a form in a Vue.js app, validate it, and work with its field values. Do you have any questions or comments about forms in Vue.js, or maybe how they compare to forms in other frameworks? Feel free to drop a comment and let’s get talking!


Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool 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.