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:
- An
input
with av-model
property in yourtemplate
- A [
data()
function(https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function) that returns the initial state of the component's data. Vue needs this to know what properties it needs to watch ondata
.
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!