Vue has grown by leaps and bounds over the last couple years, and overtook Angular as the #2 frontend framework in 2018. According to the State of JS survey, Vue is the #1 frontend framework that isn't associated with a serial bad actor, which makes it worth learning if you're serious about going Facebook-free. In this article, I'll walk through building a simple form with Vue.

Getting Started

One of the major benefits of Vue is that you can get started in vanilla JavaScript, with no outside npm modules or new languages.

The easiest way to get started with Vue is including it via script tag. However, for educational purposes, this article will npm install the vue npm package and create a bundle with webpack.

The first step is to npm install a couple packages. In addition to vue and webpack, install serve to spin up a lightweight web server.

npm install vue@2.6 webpack@4.x webpack-cli@3.x serve@10.x

Create an index.html file that loads a main.js bundle. The #content div is the element Vue will render into, and name-form is a custom component that we'll add to Vue.

<html>
  <head><title>Vue Form Example</title></head>

  <body>
    <div id="content">
      <name-form></name-form>
    </div>

    <script type="text/javascript" src="dist/main.js"></script>
  </body>
</html>

Here's a minimal Webpack config file webpack.config.js that will take a main.js file, which contains the definition of the name-form component, and create a standalone bundle dist/main.js.

module.exports = {
  mode: process.env.NODE_ENV || 'production',
  entry: {
    main: './main.js'
  },
  target: 'web',
  output: {
    path: `${process.cwd()}/dist`,
    filename: '[name].js'
  }
};

Importing Vue via require() is easy, but not quite trivial. Here's how you import Vue with require():

const Vue = require('vue/dist/vue.esm').default;

Here's the full main.js file. The main.js file imports Vue, defines the name-form component to show 'Hello, World', and creates a new Vue instance bound to the #content div.

const Vue = require('vue/dist/vue.esm').default;

Vue.component('name-form', {
  template: `
    <h1>Hello, World</h1>
  `,
});

new Vue({ el: '#content' });

Run ./node_modules/.bin/webpack to compile main.js, and ./node_modules/.bin/serve to start an HTTP server on port 5000.

$ ./node_modules/.bin/webpack
Hash: d3f433b3f83a66e3b75c
Version: webpack 4.29.6
Time: 2217ms
Built at: 2019-03-10 12:56:40
  Asset      Size  Chunks             Chunk Names
main.js  96.7 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] (webpack)/buildin/global.js 472 bytes {0} [built]
[1] ./main.js 158 bytes {0} [built]
    + 4 hidden modules
$ ./node_modules/.bin/serve

Visit http://localhost:5000, and you should see 'Hello, World' show up.

For this simple 'Hello, World' example, Vue's total bundle size is larger than Preact's, but smaller than React's. Here's the bundle sizes that Webpack outputs, assuming unminified code:

  • Preact: 9.6kb
  • Vue: 96.7kb
  • React: 109kb

User Input With data and v-model

Forms are much easier with two way data binding as opposed to React-style one way data flow. To add a two-way binding to the name-form component, you need to add two things to your component:

Here's the full name-form component. It has two inputs, one for firstName and one for lastName. When you click the submit button, the submit() function prints out the current values for firstName and lastName, and then empties both inputs.

Vue.component('name-form', {
  template: `
    <div>
      <div>
        <h2>First Name</h2>
        <input type="text" v-model="firstName" />
      </div>

      <div>
        <h2>Last Name</h2>
        <input type="text" v-model="lastName" />
      </div>

      <div>
        <h2>Submit</h2>
        <input type="submit" value="Submit" v-on:click="submit" />
      </div>
    </div>
  `,
  data: () => ({
    firstName: '',
    lastName: 'foo'
  }),
  methods: {
    submit: function() {
      console.log(this.firstName, this.lastName);

      this.firstName = '';
      this.lastName = '';
    }
  }
});

Here's the form in action:

Vue's two way data binding tracks changes on the input fields and propagates them to the firstName and lastName data properties. When submit() changes firstName and lastName, Vue propagates those changes to the input fields.

Under the hood, Vue uses JavaScript getters and setters to track changes, as opposed to Angular 1's approach of diffing whenever $apply() is called. Conceptually, Vue loops through every property in the object your data() function returns, and uses JavaScript's Object.defineProperty() function to attach a getter and setter for that property.

Submitting the Form with Async/Await

Vue has good support for promises and async/await. In particular, Vue methods support promises, including throwing an error when the returned promise rejects. For example, suppose the submit() method from the previous example were async, and it threw an async error as shown below:

  methods: {
    submit: async function() {
      await new Promise(resolve => setTimeout(resolve, 1000));
      await new Promise((resolve, reject) => reject('Sample Error'));
    }
  }

Vue would bubble up the error as an uncaught exception. This is noteworthy because many other frameworks silently ignore errors in async functions. Ending up as an uncaught exception is much better because then an error handling tool like Sentry can at least track the error, rather than losing it forever.

Below is an example of using an async method to submit an HTTP request to the httpbin API.

Vue.component('name-form', {
  template: `
    <div>
      <div>
        <h2>First Name</h2>
        <input type="text" v-model="firstName" />
      </div>
      <div>
        <h2>Last Name</h2>
        <input type="text" v-model="lastName" />
      </div>
      <div>
        <h2>Submit</h2>
        <input type="submit" value="Submit" v-on:click="submit" />
      </div>
    </div>
  `,
  data: () => ({
    firstName: '',
    lastName: 'foo'
  }),
  methods: {
    submit: async function() {
      const { firstName, lastName } = this;
      const opts = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ firstName, lastName })
      };
      const res = await fetch('https://httpbin.org/post', opts).
        then(res => res.json()).
        then(res => JSON.parse(res.data));
      console.log('done', res.firstName, res.lastName);
      this.firstName = '';
      this.lastName = '';
    }
  }
});

Moving On

Overall, I'm very impressed with how much Vue. Vue takes the best parts of Angular 1 and React, and avoids the worst parts of both. In particular, Vue makes forms much easier than React-style apps. Give Vue a shot with your next weekend project.

Surprised that Vue supports async/await, but React doesn't in general? Want to know how to determine whether your favorite npm modules support async/await without reconciling contradictory answers on StackOverflow? Chapter 4 of Mastering Async/Await explains the core principles for determining whether a given library or framework supports async/await, so get the ebook today!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus