An Example of Form Validation with Vue.js

I was a bit torn about today's blog post. When it comes to form validation, I'm a huge fan of validating via HTML attributes. In fact, I just ran across a great post today on the subject by Dave Rupert, "Happier HTML5 Form Validation". However, I know folks have issues with HTML validation, and it doesn't cover every use case, and finally, I thought it would just be plain good practice for me to write up a few quick examples. With that in mind, let's get started.

First Example - Super Simple Form

For my first example, I wanted something as simple as possible. Given a form of three fields, make two required. I began with the HTML:

<form id="app" @submit="checkForm" action="/something" method="post">
  
  <p v-if="errors.length">
    <b>Please correct the following error(s):</b>
    <ul>
      <li v-for="error in errors">{{ error }}</li>
    </ul>
  </p>
  
  <p>
  <label for="name">Name<label>
  <input type="text" name="name" id="name" v-model="name">
  </p>

  <p>
  <label for="age">Age<label>
  <input type="number" name="age" id="age" v-model="age">
  </p>

  <p>
  <label for="movie">Favorite Movie<label>
  <select name="movie" id="movie" v-model="movie">
    <option>Star Wars</option>
    <option>Vanilla Sky</option>
    <option>Atomic Blonde</option>
  </select>
  </p>

  <p>
  <input type="submit" value="Submit">  
  </p>

</form>

Let's cover it from the top. The form tag has an ID that I'll be using for my Vue component. I've got a submit handler that you'll see in a bit, and my action is a fake URL that would point to something real on a server someplace (where you have backup server-side validation of course).

Beneath that I've got a paragraph that shows or hides itself based on an error state. This is a personal preference of mine when building forms. I like a nice simple list of errors on top of the form. You may like error handling by the fields themselves. Use what works. Also note I'm going to fire my validation on submit rather than as every field is modified. Again, this is a personal preference.

The final thing to note is that each of the three fields has a corresponding v-model to connect them to values we will work with in the JavaScript. Now let's look at that!

const app = new Vue({
  el:'#app',
  data:{
    errors:[],
    name:null,
    age:null,
    movie:null
  },
  methods:{
    checkForm:function(e) {
      if(this.name && this.age) return true;
      this.errors = [];
      if(!this.name) this.errors.push("Name required.");
      if(!this.age) this.errors.push("Age required.");
      e.preventDefault();
    }
  }
})

Fairly short and simple. I default an array to hold my errors and set null values for my three form fields. My checkForm logic (which is run on submit remember) checks for name and age only as movie is optional. If they are empty I check each and set a specific error for each. And that's really it. You can run the demo below. Don't forget that on a successful submission it's going to POST to a non-existent URL.

See the Pen form validation 1 by Raymond Camden (@cfjedimaster) on CodePen.

Second Example - Custom Validation

For the second example, I switched the second text field (age) to email and decided to add custom validation. My code is taken from the StackOverflow question, How to validate email address in JavaScript?. This is an awesome question because it makes your most intense Facebook political/religious argument look like a slight disagreement over who makes the best beer. Seriously - it's insane. Here is the HTML, even though it's really close to the first example.

<form id="app" @submit="checkForm" action="/something" method="post" novalidate="true">
  
  <p v-if="errors.length">
    <b>Please correct the following error(s):</b>
    <ul>
      <li v-for="error in errors">{{ error }}</li>
    </ul>
  </p>
  
  <p>
  <label for="name">Name<label>
  <input type="text" name="name" id="name" v-model="name">
  </p>

  <p>
  <label for="email">Email<label>
  <input type="email" name="email" id="email" v-model="email">
  </p>

  <p>
  <label for="movie">Favorite Movie<label>
  <select name="movie" id="movie" v-model="movie">
    <option>Star Wars</option>
    <option>Vanilla Sky</option>
    <option>Atomic Blonde</option>
  </select>
  </p>

  <p>
  <input type="submit" value="Submit">  
  </p>

</form>

While the change here is small, note the novalidate="true" on top. This is important because the browser will attempt to validate the email address in the field when type="email". Frankly I'd be tempted to trust the browser in this case, but as I wanted an example with custom validation, I'm disabling it. Here's the updated JavaScript.

const app = new Vue({
  el:'#app',
  data:{
    errors:[],
    name:null,
    email:null,
    movie:null
  },
  methods:{
    checkForm:function(e) {
      this.errors = [];
      if(!this.name) this.errors.push("Name required.");
      if(!this.email) {
        this.errors.push("Email required.");
      } else if(!this.validEmail(this.email)) {
        this.errors.push("Valid email required.");        
      }
      if(!this.errors.length) return true;
      e.preventDefault();
    },
    validEmail:function(email) {
      var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(email);
    }
  }
})

As you can see, I've added validEmail as a new method and I simply call it from checkForm. Nothing too crazy, but a good example I think. You can play with this example here:

See the Pen form validation 2 by Raymond Camden (@cfjedimaster) on CodePen.

(And yes - before someone comments, I'm aware of the issues of validating emails and even a valid email format doesn't necessarily mean it is an email address that points to a person. Let's just chill, shall we?)

Example Three - Another Custom Validation

For the third example, I built something you've probably seen in survey apps. I'm asking the user to spend a "budget" for a set of features for a new Star Destroyer model. The total must equal 100. First, the HTML.

<form id="app" @submit="checkForm" action="/something" method="post" novalidate="true">
  
  <p v-if="errors.length">
    <b>Please correct the following error(s):</b>
    <ul>
      <li v-for="error in errors">{{ error }}</li>
    </ul>
  </p>

  <p>
    Given a budget of 100 dollars, indicate how much
    you would spend on the following features for the
    next generation Star Destroyer:
  </p>

  <p>
    <input type="number" name="weapons" v-model.number="weapons"> Weapons <br/>
    <input type="number" name="shields" v-model.number="shields"> Shields <br/>
    <input type="number" name="coffee" v-model.number="coffee"> Coffee <br/>
    <input type="number" name="ac" v-model.number="ac"> Air Conditioning <br/>
    <input type="number" name="mousedroids" v-model.number="mousedroids"> Mouse Droids <br/>
  </p>

  <p>
    Current Total: {{total}}
  </p>

  <p>
  <input type="submit" value="Submit">  
  </p>

</form>

Note the set of inputs covering the five different features. Note the addition of .number to the v-model attibute. This tells Vue to cast the value to a number when you use it. However, there is a bug (imo) with this feature such that when the value is blank, it turns back into a string. You'll see my workaround below. To make it a bit easier for the user, I also added a current total right below so they can see, in real time, what their total is. Now let's look at the JavaScript.

const app = new Vue({
  el:'#app',
  data:{
    errors:[],
    weapons:0,
    shields:0,
    coffee:0,
    ac:0,
    mousedroids:0
  },
  computed:{
     total:function() {
       //must parse cuz Vue turns empty value to string
       return Number(this.weapons)+
         Number(this.shields)+
         Number(this.coffee)+
         Number(this.ac+this.mousedroids);
     }
  },
  methods:{
    checkForm:function(e) {
      this.errors = [];
      if(this.total != 100) this.errors.push("Total must be 100!");
      if(!this.errors.length) return true;
      e.preventDefault();
    }
  }
})

I set up the total value as a computed value, and outside of that bug I ran into, it was simple enough to setup. My checkForm method now just needs to see if the total is 100 and that's it. You can play with this here:

See the Pen form validation 3 by Raymond Camden (@cfjedimaster) on CodePen.

Fourth Example - Server-side validation

In my final examlpe, I built something that made use of Ajax to validate at the server. My form will ask you to name a new product and I want to ensure that the name is unique. I wrote a quick OpenWhisk serverless action to do the validation. While it isn't terribly important, here is the logic:

function main(args) {

    return new Promise((resolve, reject) => {

        // bad product names: vista,empire,mbp
        let badNames = ['vista','empire','mbp'];
        if(badNames.includes(args.name)) reject({error:'Existing product'});
        resolve({status:'ok'});

    });

}

Basically any name but "vista", "empire", and "mbp" are acceptable. Ok, so let's look at the form.

<form id="app" @submit="checkForm" method="post">
  
  <p v-if="errors.length">
    <b>Please correct the following error(s):</b>
    <ul>
      <li v-for="error in errors">{{ error }}</li>
    </ul>
  </p>

  <p>
  <label for="name">New Product Name: </label>
  <input type="text" name="name" id="name" v-model="name">
  </p>

  <p>
  <input type="submit" value="Submit">  
  </p>

</form>

There isn't anything special here. So let's go on to the JavaScript.

const apiUrl = 'https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/safeToDelete/productName.json?name=';

const app = new Vue({
  el:'#app',
  data:{
    errors:[],
    name:''
  },
  methods:{
    checkForm:function(e) {
      e.preventDefault();
      this.errors = [];
      if(this.name === '') {
        this.errors.push("Product name is required.");
      } else {
        fetch(apiUrl+encodeURIComponent(this.name))
        .then(res => res.json())
        .then(res => {
          if(res.error) {
            this.errors.push(res.error);
          } else {
            // redirect to a new url, or do something on success
            alert('ok!');
          }
        });
      }
    }
  }
})

I start off with a variable representing the URL of the API I have running on OpenWhisk. Now look at checkForm. In this version, I always prevent the form from submitting (which, by the way, I just remembered I could do in the HTML with Vue as well). You can see a basic check on this.name being empty, and then I hit the API. If it's bad, I add an error as before. If it's good, right now I do nothing (just an alert), but you could navigate the user to a new page with the product name in the URL, or do other actions as well. You can run this demo below:

See the Pen form validation 4 by Raymond Camden (@cfjedimaster) on CodePen.

Wrap Up

As always, remmeber I'm still learning, and I'm sure there are a multitude of different ways of doing what I've shown above. I'm sure there are whole libraries of Vue components you can import and go to town with. But I hope these examples are interesting and if you're still considering whether or not you want to learn Vue, they may help.

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also