Vue.js is an easy to use web app framework that we can use to develop interactive front end apps.
In this article, we’ll look at the basics of building a Vue app.
Script Tag
We can include the Vue.js framework with a script
tag.
To do this we can write:
<script src='https://vuejs.org/js/vue.js'></script>
before our app code loads. The URL above has the development version, which has all the warnings but it’s not minified.
To include the production version, we can write:
<script src='https://vuejs.org/js/vue.min.js'></script>
To fix the version to 2.6.11, we can write:
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
We can also import the module in our browser if we don’t need to support Internet Explorer as follows:
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.esm.browser.js'
</script>
The vue.esm.browser.js
file is already optimized for browsers so it can be used without creating too many requests.
Example
To create our first app, we can create the index.html
file as follows:
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
<script src="https://vuejs.org/js/vue.js"></script>
</head> <body>
<div id="app"></div>
<script src="./src/index.js"></script>
</body>
</html>
Then in src/index.js
, we can add:
new Vue({
template: "<div>{{ 'hello' }}</div>",
el: "#app"
});
Then we should see hello
on the screen, since we have a div
with the ID app
, which is used by Vue.js to render our Vue
instance.
The Vue.js compiler is responsible for converting the template into HTML.
In the template above, 'hello'
is a JavaScript string, which was converted to HTML text when the Vue compiler runs.
We specified that the template has a div
with the text 'hello'
in it.
Different Builds of the Vue.js Framework
There are several different builds of the Vue.js framework. They’re:
- Full — Builds that have the compiler and the runtime. That’s the one we used in the example above.
- Compiler — Code that’s responsible for compiling template strings into JavaScript render functions
- Runtime — Code that’s responsible for creating Vue instances, rendering and updating virtual DOM, etc.
- UMD — A module that can be directly included with a
script
tag. It includes the runtime and the compiler - CommonJS — A module that can be used with older bundlers like Browserify or Webpack 1.
- ES Module — New to version 2.6. It can be used directly by browsers via
<script type='module'>
or be used with bundlers like Webpack 2 or Rollup.
Runtime and Compiler
The runtime is used for creating the Vue
instance via new Vue
.
The compiler is for compiling templates specified by the template
option.
This means that:
new Vue({
template: "<div>{{ 'hello' }}</div>",
el: "#app"
});
needs the compiler and:
new Vue({
el: "#app",
render(h) {
return h("div", "hello");
}
});
doesn’t.
Template syntax will be much more convenient when our apps get more complex.
When we’re using vue-loader
or vueify
, templates inside .vue
files are precompiled into JavaScript, so we don’t need the compiler in the final bundle.
When we don’t need to use the compiler, then we should use the runtime only build since it’s 30% smaller.
We can include different builds depending on the bundler that we’re using. For example, in Parcel, we can write:
"alias": {
"vue" : "./node_modules/vue/dist/vue.common.js"
}
in package.json
.
Vue CLI
We can Vue-CLI automatically create our app and build it so we don’t have to worry about which bundles to choose.
To install it, we can run:
npm install -g @vue/cli
or:
yarn global add @vue/cli
We can then create a project with it by running:
vue create project
or run:
vue ui
to show the GUI to let us create a Vue project in our browser.
Also, we can run the Vue CLI directly with npx
by running:
npx vue create project
Running vue create
will present us with a wizard with various options like:
Vue CLI v3.11.0
┌───────────────────────────┐
│ Update available: 4.1.2 │
└───────────────────────────┘
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
( ) Router
( ) Vuex
( ) CSS Pre-processors
(*) Linter / Formatter
( ) Unit Testing
( ) E2E Testing
This screen is presented when we choose Manually Select Features
. We can choose Default
if we don’t want to choose anything from this screen.
To run the Vue UI, we can run:
npx vue ui
Then our browser will automatically go to http://localhost:8000/project/select, where we see a Create link to create a project.
We can then create a project by clicking Create a new project here once we clicked on the Create tab.
Characteristics of the Vue Instance
Each Vue.js app begins by defining a new Vue
instance. The Vue
constructor takes an options object that takes various properties.
We often use vm
to refer to a Vue instance, where vm
stands for ViewModel.
A Vue app roughly follows the Model-View-ViewModel pattern, where the ViewModel has the business logic of the app, View has the markup that users see, and Model has the data.
For example, we can define a Vue
instance as follows:
const vm = new Vue({ });
Each Vue app consists of a root Vue instance and it’s created with new Vue
. We can organize it in a tree for easier maintenance.
Data and Methods
The options object that pass into the Vue
constructor can have data and methods.
For example, if we define a Vue
instance as follows:
const vm = new Vue({
el: "#app",
data: { foo: "bar" }
});
Then when we add:
console.log(vm.foo);
below our vm
definition, we get 'bar'
since data.foo
has the value 'bar'
.
In other words, if we have:
const data = { foo: "bar" };
const vm = new Vue({
el: "#app",
data
});
console.log(vm.foo === data.foo);
Then the console.log
will log true
.
When the data changes, the app will re-render with the new data.
If we create a new property in vm
and set it as follows:
let data = { foo: "bar" };
const vm = new Vue({
el: "#app",
data
});
vm.a = 1;
The app won’t re-render. On the other hand, if we write:
let data = { foo: "bar", a: 1 };
const vm = new Vue({
el: "#app",
data
});
Then the app will re-render. This means that we have to put our data that we want to render in the data
field.
If we freeze the object that we pass to data with Object.freeze()
, then the Vue app won’t re-render since properties can’t be changed, so new changes can’t propagate since they aren’t set in the object.
So if we have:
let data = { foo: "bar", a: 1 };
Object.freeze(data);
const vm = new Vue({
el: "#app",
data
});
No changes can be made after the initial render since we froze data
with Object.freeze
.
The Vue instance also exposes a number of instance properties and methods.
They’re prefixed with the $
so that we know they’re part of the Vue instance.
$el
We have the $el
property to get the DOM element that the Vue instance resides in.
For example, if we have:
let data = { foo: "bar" };
const vm = new Vue({
el: "#app",
data
});
console.log(vm.$el === document.getElementById("app"));
Then the console.log
will log true
since our Vue instance resides in an element with ID app
.
$data
The $data
property will get us the value of the data
property that we set in the options object that we passed into the Vue
constructor.
So if we have:
let data = { foo: "bar" };
const vm = new Vue({
el: "#app",
data
});
console.log(vm.$data === data);
Then we get true
from the console.log
since data
references the same object as vm.$data
since we set it as the value of the data
property of the options object that we passed into the Vue
constructor.
$watch
$watch
is an instance method that lets us watch for changes in the data
object that we set as the value of the options
object.
For example, if we have:
let data = { foo: "bar" };
const vm = new Vue({
el: "#app",
data
});
vm.$watch("foo", (newValue, oldValue) => {
console.log(newValue, oldValue);
});
in src/index.js
and:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<input type="text" v-model="foo" />
</div>
<script src="./src/index.js"></script>
</body>
</html>
in index.html
. Then when we type in something to the input box, we should get some console.log
output from:
vm.$watch("foo", (newValue, oldValue) => {
console.log(newValue, oldValue);
});
This is because changes are constantly being watched in the data.foo
property. v-model
automatically updates the value for foo
as we type into the box.
So as we’re typing, the changes to foo
are being watched and it’s logged by the handler function that we passed into the $watch
method.
Vue Instance Lifecycle Hooks
Each Vue instance goes through a lifecycle. The lifecycle events can be handled with functions, which are the lifecycle hooks.
For example, when a Vue instance is created, the created
hook is called.
We can handle the created
event emitted by Vue.js as follows:
new Vue({
data: {
foo: "bar"
},
created() {
console.log(`foo is ${this.foo}`);
}
});
Then we should see foo is bar
from the console.log
. Once the Vue instance is created and the initial data is set, the created
hook is called.
This is why we see such output from the console.log
.
Note that we can’t use arrow functions for lifecycle hooks since we reference the properties from this
, which is the Vue instance.
There’re also other lifecycle hooks like mounted
, updated
, destroyed
.
Lifecycle Diagram
The full lifecycle diagram is below. It outlines the whole workflow for creating a new Vue instance.
The red rounded rectangles have the hooks that are called.
Courtesy of vuejs.org
Creation Hooks
Creation hooks are called when the Vue instance begins its initialization process. It lets us do things before the Vue instance is added to the DOM.
This hook is also run during server-side rendering
We don’t have access to this.$el
(the target mounting element) or the DOM within this hook since no DOM manipulation is done at this point.
beforeCreate
The beforeCreate
hook runs during initialization. data
has been made reactive and events
aren’t set up yet.
For example, if we have:
new Vue({
data: {
foo: "bar"
},
beforeCreate() {
console.log("beforeCreated called");
},
created() {
console.log(`foo is ${this.foo}`);
}
});
Then we see:
beforeCreated called
foo is bar
which confirms that beforeCreate
is called before created
.
created
data
and events
are available in the created
hook since the Vue instance has been initialized.
For example, if we write:
new Vue({
data: {
foo: "bar"
},
created() {
this.foo = "baz";
console.log(`foo is ${this.foo}`);
}
});
Then we get:
foo is baz
since we changed the value of this.foo
in the created
hook before logging its value.
Mounting Hooks
The mounting hooks are run when DOM manipulation is being done, including mounting the Vue instance to the specified element.
We can use it to access or modify the DOM of our component.
beforeMount
The beforeMount
hook runs before the initial render happens and after the template or render functions are compiled.
It doesn’t get called during server-side rendering.
For instance, if we write:
new Vue({
el: "#app",
data: {
foo: "bar"
},
created() {
console.log(`created called`);
},
beforeMount() {
console.log(`beforeMount called`);
}
});
Then we get:
created called
beforeMount called
mounted
When the mounted
hook is called, we have access to the component, template, and the DOM.
It’s used frequently for the initialization of our own data like fetching data over the network and integrate non-Vue libraries.
For example, if we have the following in src/index.js
:
new Vue({
el: "#app",
data: {
foo: "bar"
},
beforeMount() {
console.log(this.$el.textContent);
}
});
and the following in index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">foo</div>
<script src="./src/index.js"></script>
</body>
</html>
Then we get foo
logged since we have foo
in the div
with ID app
.
Updating Hooks
Updating hooks are called when a reactive property used by our component changes or we some triggers re-rendering manually.
They’re useful for checking when a component is re-rendering.
However, we should use computed properties or watchers to track reactive property changes.
beforeUpdate
beforeUpdate
runs after data changes in our component, right before the DOM is updated and re-rendered.
It lets us get the new state before the DOM is re-rendered.
For example, if we have the following in src/index.html
:
new Vue({
el: "#app",
data: {
foo: "bar"
},
beforeUpdate() {
console.log(this.foo);
},
methods: {
toggle() {
this.foo = this.foo === "bar" ? "baz" : "bar";
}
}
});
and the following in index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<p>{{foo}}</p>
<button @click="toggle">Toggle</button>
</div>
<script src="./src/index.js"></script>
</body>
</html>
Then when we click the Toggle button, we can see the value changes logged in the console.log
call of the beforeUpdate
method.
updated
The updated
hook runs after data changes on our Vue instance and the DOM re-renders.
For example, if we have the following in src/index.js
:
new Vue({
el: "#app",
data: {
foo: "bar"
},
updated() {
console.log(this.$refs.foo.textContent);
},
methods: {
toggle() {
this.foo = this.foo === "bar" ? "baz" : "bar";
}
}
});
and the following in index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<p ref="foo">{{foo}}</p>
<button @click="toggle">Toggle</button>
</div>
<script src="./src/index.js"></script>
</body>
</html>
Then we can get the text content of the p
element with ref
set to foo
by logging the value as follows:
console.log(this.$refs.foo.textContent);
in the updated
hook and we can see the new value for it as we click the Toggle button.
Destruction Hooks
These hooks are run when the Vue instance is destroyed. Therefore, we can run clean up code in there.
These hooks aren’t run during server-side rendering.
beforeDestroy
beforeDestroy
is run before teardown. Our Vue instance is still present and functional.
Therefore, we can run our clean up code in here.
For example, we can use it as follows:
new Vue({
el: "#app",
data: {
foo: "bar"
},
beforeDestroy() {
this.foo = null;
}
});
destroyed
This is called after the Vue instance is destroyed. All the directives are unbound, event listeners are removed and child Vue instances are destroyed.
For example, we can use it as follows:
new Vue({
el: "#app",
data: {
foo: "bar"
},
destroyed() {
this.foo = null;
}
});
Templates
Vue templates let us bind data to the DOM.
Vue compiles the template into Virtual DOM render functions. It can figure out the minimal number of components to re-render and minimize DOM manipulation when data changes.
Interpolations
The most basic form of data binding is text interpolation with double curly braces.
For example, we can write:
<p>{{foo}}</p>
To show the value of foo
from a Vue instance or component.
The code above will be updated when foo
updates.
We can keep it from updating after the first render by using the v-once
directive as follows:
<p v-once>{{foo}}</p>
Raw HTML
We can also put raw HTML into an element with the v-html
directive.
This lets us add HTML elements and formatting into an element.
For example, if we have the following in src/index.js
:
new Vue({
el: "#app",
data: {
rawHtml: "<b>bar</b>"
}
});
and the following in index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<p v-html="rawHtml"></p>
</div>
<script src="./src/index.js"></script>
</body>
</html>
Then we get bar
bolded displayed since the inner HTML of the p
element is <b>bar</b>
.
When we’re using this, we have to sanitize the HTML so that it won’t contain any code. This is to prevent cross-site scripting attacks since code will be run if there are any when we pass in a value to the v-html
directive.
Attributes
We can set values of HTML element attributes dynamically with v-bind
as follows.
In src/index.js
, we write:
new Vue({
el: "#app",
data: {
isButtonDisabled: true
}
});
Then when we write the following in index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<button v-bind:disabled="isButtonDisabled">Button</button>
</div>
<script src="./src/index.js"></script>
</body>
</html>
Then since we set the isButtonDisabled
property to true
, the button will be disabled. This also apply to any other truthy value.
On the other hand, if we set it to false
, we see that the button is disabled. This also applies to other falsy values like null
, undefined
, 0 and empty string.
JavaScript Expressions
We can add any other JavaScript expressions between the curly braces.
For example, we can write:
{{ number + 10 }}
{{ yes ? 'YES' : 'NO' }}
{{ str.split('').reverse('').join('') }}
Also, we can write expressions as the value of the v-bind
directive as follows:
<div v-bind:id="`div-${id}`"></div>
Statements will not run inside the curly braces. For example:
{{ let a = 1 }}
will give us an error. We’ll get the error ‘avoid using JavaScript keyword as the property name: “let”’ in the console.
We also can’t run conditional statements within curly braces:
{{ if (id) { return id } }}
We’ll get the error ‘avoid using JavaScript keyword as the property name: “if”’.
Directives
Directives are special attributes with the v-
prefix. We can pass in a single JavaScript expression as the value.
v-for
can accept more than one expression separated by ;
.
For example, we can conditionally display content with v-if
:
<p v-if="show">Hello</p>
If show
is truthy, then we’ll see Hello
. Otherwise, it won’t be shown.
Arguments
Some directive takes an argument, which is denoted by a colon after the directive name.
For example, we can set the href
value of an a
element by writing:
<a v-bind:href="'https://medium.com'">Medium </a>
The v-on
directive also takes an argument. For example, we can handle the click event of an element by writing:
<a v-on:click="onClick"> Click Me </a>
Then the onClick
method in our Vue instance or component will be run when we click on the a
element.
Dynamic Arguments
Since Vue.js 2.6.0, we can use a JavaScript expression as an argument for a directive.
For example, we can use it as follows. In src/index.js
, we write:
new Vue({
el: "#app",
data: {
click: `${"cl"}${"ick"}`
},
methods: {
onClick() {
alert("clicked");
}
}
});
Then in index.html
, we can use a dynamic argument as follows:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<a v-on:click="onClick"> Click Me </a>
</div>
<script src="./src/index.js"></script>
</body>
</html>
This will reference the value of the click
property of the data
property in our Vue instance, and so we actually have v-on:click
.
And therefore, when we click the a
tag, we get an alert box with the clicked
text displayed.
Dynamic argument expressions must evaluate to a string or null
.
Spaces and quotes also aren’t valid inside the brackets.
When Should We Use Watchers?
We can use watchers when we want to watch for data changes in reactive properties and do some asynchronous or expensive options as the value changes.
For example, we can use it to watch for changes for an input and then get a joke from the Chuck Norris API when we type in something.
First, we put the following in src/index.js
:
new Vue({
el: "#app",
data: {
message: "",
joke: ""
},
created() {
this.debounceGetAllCapsMessage = _.debounce(this.getAllCapsMessage, 500);
},
methods: {
async getAllCapsMessage() {
const res = await fetch("https://api.icndb.com/jokes/random");
const result = await res.json();
this.joke = result.value.joke;
}
},
watch: {
message(newMessage, oldMessage) {
this.debounceGetAllCapsMessage();
}
}
});
The code above watches for the change in the message
property and then call the debounceGetAllCapsMessage
method, which gets a jokes after a half a second delay.
The watch
method both takes the new value as the first parameter and the old value as the second parameter.
Then we put the following in index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
</head> <body>
<div id="app">
<input type="text" v-model="message" />
<p>{{joke}}</p>
</div>
<script src="./src/index.js"></script>
</body>
</html>
As we can see, we can run an asynchronous operation, add a delay before we perform the operation, and then get the final result.
We can also use vm.$watch
to watch for a value. It takes up to 3 arguments.
The first is a path to a property, which is a string. It can also be a function with one or multiple things combined to watch.
The second argument is a function that runs when the value changes.
The third argument for vm.$watch
is an object with some properties. They include:
immediate
— watches for the setting of the initial value
The method returns a function to stop watching for values. This is an optional argument.
We can use it as follows. In src/index.js
, we have:
const vm = new Vue({
el: "#app",
data: {
message: "",
joke: ""
}
});
vm.$watch(
"message",
_.debounce(async function() {
const res = await fetch("https://api.icndb.com/jokes/random");
const result = await res.json();
this.joke = result.value.joke;
}, 500)
);
Then index.html
, we have:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
</head> <body>
<div id="app">
<input type="text" v-model="message" />
<p>{{joke}}</p>
</div>
<script src="./src/index.js"></script>
</body>
</html>
which is the same as what we had before.
In both examples, we should get a new joke after half a second delay displayed.
Using v-for to Map Array to Elements
We can use the v-for
directive to render a list of items from an array.
For example, we can use it as follows:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: [{ name: "Joe" }, { name: "Jane" }, { name: "Mary" }]
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="person in persons">
{{person.name}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
In the code above, we have the v-for
directive, and then we write person in persons
to loop through the data.persons
array.
Then we get the name
property from each entry and display it.
person
is an alias for the array element being iterated on.
Then we get:
JoeJaneMary
All the properties of the array entry is available during iteration.
We can also add an optional second argument to access the index of the array. For example, we can write the following:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: [{ name: "Joe" }, { name: "Jane" }, { name: "Mary" }]
}
});
index.html
;
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="(person, index) in persons">
{{index}} - {{person.name}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then we get:
0 - Joe1 - Jane2 - Mary
We can also use of
instead of in
to match the for...of
loop syntax in JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="person of persons">
{{person.name}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
v-for with an Object
We can also use v-for
to loop through the values of an object.
So if we write the following:
src/index.js
:
new Vue({
el: "#app",
data: {
book: {
title: "JavaScript Book",
author: "John Smith",
publishedYear: 2019
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="value in book">
{{value}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
And we get:
JavaScript BookJohn Smith2019
We can loop through the keys of the object by providing a second argument to the expression as follows:
src/index.js
:
new Vue({
el: "#app",
data: {
book: {
title: "JavaScript Book",
author: "John Smith",
publishedYear: 2019
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="(value, key) in book">
{{key}} - {{value}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then we get:
title - JavaScript Bookauthor - John SmithpublishedYear - 2019
Also, we can provide a third argument for the index
of the object entry.
To do this, we can write:
src/index.js:
new Vue({
el: "#app",
data: {
book: {
title: "JavaScript Book",
author: "John Smith",
publishedYear: 2019
}
}
});
index.html
:
`<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="(value, key, index) in book">
{{index}}: {{key}} - {{value}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then we get:
0: title - JavaScript Book1: author - John Smith2: publishedYear - 2019
The order of iteration over an object is based on the enumeration order of Object.keys()
, which isn’t guaranteed to be consistent across JavaScript engine implementations.
Maintaining State
v-for
by default updates the items in place instead of moving the DOM elements to match the order of the items to make sure it reflects what should be rendered at a particular index.
Therefore, it’s only suitable when the list doesn’t rely on child component state or temporary DOM state like input values.
To make sure Vue knows which elements are unique, we should provide a key
attribute via the v-bind:key
directive.
For example, we can write:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: [{ name: "Joe" }, { name: "Jane" }, { name: "Mary" }]
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="person in persons" v-bind:key="person.name">
{{person.name}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
We have to use primitive values as keys.
The key
attribute is a generic mechanism for Vue to identify nodes.
v-show
The v-show
directive is used to show something when the expression we passed into it is truthy.
For example, we can use it as follows:
<h1 v-show="show">Hi</h1>
The difference between v-show
and v-if
are that v-show
elements remain in the DOM even though is hidden.
On the other hand, v-if
elements are hidden by removing them from the DOM.
Also, v-if
removes event listeners and child components inside conditional blocks are destroyed if they’re hidden and recreated when v-if
‘s expression is truthy.
v-if
elements are also different in that if the condition is falsy during conditional render, it won’t do anything. It won’t be rendered until the expression becomes truthy.
v-if
has higher toggling than v-show
since DOM manipulating and attaching event listeners have to be done when things are toggled.
Therefore, if things need to be toggled often, v-show
is more efficient.
v-if with v-for
v-for
has higher priority than v-if
if they’re used together.
However, we shouldn’t use them together because of the higher precedence of v-for
over v-if
.
If we want to filter some elements out when we render items from an array, we should use a computed property.
If we only render a small fraction of array elements, it has to iterate over the entire list and then check if the expression we set it truthy.
For example, the following JavaScript and HTML code:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: ["Joe", "Jane", "Mary"]
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="person in persons" v-if='person !== "Joe"'>
{{person}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
is not very efficient since every time the loop is rendered, Vue has to iterate through every element and then check if person !== “Joe”
is true
.
Instead, we should use a computed property as follows:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: ["Joe", "Jane", "Mary"]
},
computed: {
personsWithoutJoe() {
return this.persons.filter(p => p !== "Joe");
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="person in personsWithoutJoe">
{{person}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
The code above is more efficient because personsWithoutJoe
is only recomputed when persons
change.
Also, only the items in personsWithoutJoe
is iterated through during render, so v-for
doesn’t have to loop through all the items.
There’s also less logic in the template, which keeps it clean. Maintenance is much easier.
We can also get performance benefits from moving v-if
to the parent element that has the v-for
, so whatever’s inside is only rendered when the condition is met.
For example, if we have:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: ["Joe", "Jane", "Mary"],
show: false
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-if="show">
<div v-for="person in persons">
{{person}}
</div>
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then nothing is rendered since show
is false
. Vue doesn’t have to do the work to look at whatever’s inside.
This is much better than:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<div v-for="person in persons" v-if="show">
{{person}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
In the code above, Vue has to loop through each entry and check if the condition in the v-if
returns a truthy value.
As we can see, even though the code is only slightly different, but the performance implications are big.
Array Change Detection
Vue wraps the following array mutation methods so that when these methods are called, a view update will be triggered. They’re the following:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
For example, if we have a button that triggers a push of a new item to an array, it’ll update:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: [{ name: "Joe" }, { name: "Jane" }]
},
methods: {
addPerson() {
this.persons.push({ name: "Mary" });
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<button @click="addPerson">Add Person</button>
<div v-for="person in persons">
{{person.name}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then we get:
JoeJane
When the array is first rendered. Then when we click Add Button, we get:
JoeJaneMary
since we called the addPerson
method with the button click, which called the push
method to add a new entry.
Replacing an Array
Non-mutating array method are ones that always return a new array. We can replace the original array with the returned array to trigger a view update.
Vue doesn’t re-render the list from scratch when we update the array. Instead, it’ll try to maximum DOM element reuse.
For example, if we have the following:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: [{ name: "Joe" }, { name: "Jane" }, { name: "Mary" }]
},
methods: {
filterPerson() {
this.persons = this.persons.filter(p => p.name !== "Joe");
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js)"></script>
</head> <body>
<div id="app">
<button @click="filterPerson">Filter Person</button>
<div v-for="person in persons">
{{person.name}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then when our page first loads, we get the following names:
JoeJaneMary
Once we click Filter Person, we get:
JaneMary
since we called the filterPerson
method with the button click, which called the filter
method to return an array with the filtered items.
Then the new array is assigned to this.persons
and then the view is refreshed.
Catches
Vue can’t detect directly assign an item to an array by setting the index or when the length of the array is modified.
So:
vm.item[indexOfItem] = 'item';
and:
vm.items.length = 2;
won’t be picked up by Vue and update the view with updated data.
We can either call Vue.set
or splice
to set an entry to a given index.
Vue.set
For example, we can write the following code:
src/index.js
new Vue({
el: "#app",
data: {
persons: ["Joe", "Mary"]
},
methods: {
addPerson() {
Vue.set(this.persons, 5, "Jane");
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js)"></script>
</head> <body>
<div id="app">
<button @click="addPerson">Add Person</button>
<div v-for="(person, index) in persons">
{{index}} - {{person}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
When the page first loads, we get the following entries rendered:
0 - Joe1 - Mary
Then when we click Add Person, we get:
0 - Joe1 - Mary2 -3 -4 -5 - Jane
Splice
We can do the same with splice
, but the length has to be set first:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: ["Joe", "Mary"]
},
methods: {
addPerson() {
this.persons.length = 5;
this.persons.splice(5, 1, "Jane");
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<button @click="addPerson">Add Person</button>
<div v-for="(person, index) in persons">
{{index}} - {{person}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then we get the same results as before.
We can also use splice
to set the length of the array and trigger view update. To do this, we can write:
this.persons.splice(5);
vm.$set
We can call vm.$set
as follows, which is the same as Vue.set
except that it’s available to the Vue instance instead of a global object.
For example, we can write:
src/index.js
:
new Vue({
el: "#app",
data: {
persons: ["Joe", "Mary"]
},
methods: {
addPerson() {
this.$set(this.persons, 5, "Jane");
}
}
});
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<button @click="addPerson">Add Person</button>
<div v-for="(person, index) in persons">
{{index}} - {{person}}
</div>
</div>
<script src="src/index.js"></script>
</body>
</html>
Transitioning Between Elements
With Vue, we can transition elements between v-if
and v-else
.
When toggling between elements that have the same tag name, we must tell Vue that they’re distinct elements giving them unique key
attributes. Otherwise, the Vue compiler will only replace the content of the element for efficiency.
It’s always a good idea to key multiple items within a transition element.
For example, we can transition between 2 p
elements as follows:
src/index.js
:
new Vue({
el: "#app",
data: { show: false }
});
src/style.css
:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link
rel="stylesheet"
type="text/css"
href="./src/styles.css"
media="screen"
/>
</head>
<body>
<div id="app">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show" key="hi">hi</p>
<p v-else key="bye">bye</p>
</transition>
</div>
<script src="src/index.js"></script>
</body>
</html>
Then we should see fade effects when we transition between ‘hi’ and ‘bye’.
We added key
attributes to each so they’ll always be rendered from scratch.
Also, we can use the key
attribute to transition between different states of the same element.
We can do that as follows:
src/index.js
:
new Vue({
el: "#app",
data: { show: false }
});
src/styles.css
:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link
rel="stylesheet"
type="text/css"
href="./src/styles.css"
media="screen"
/>
</head>
<body>
<div id="app">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-bind:key="show">
{{ show ? 'hi' : 'bye' }}
</p>
</transition>
</div>
<script src="src/index.js"></script>
</body>
</html>
In the code above, we replaced:
<transition name="fade">
<p v-if="show" key="hi">hi</p>
<p v-else key="bye">bye</p>
</transition>
with:
<transition name="fade">
<p v-bind:key="show">
{{ show ? 'hi' : 'bye' }}
</p>
</transition>
Everything else remained the same.
The transition can be done between any number of elements with v-if
or binding a single element to a dynamic property.
For example, we can write:
src/index.js
:
new Vue({
el: "#app",
data: { states: ["foo", "bar", "baz"], state: "", index: 0 },
methods: {
rotate() {
this.index = (this.index + 1) % this.states.length;
this.state = this.states[this.index];
}
}
});
src/styles.css
:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link
rel="stylesheet"
type="text/css"
href="./src/styles.css"
media="screen"
/>
</head>
<body>
<div id="app">
<button v-on:click="rotate">
Rotate
</button>
<transition name="fade">
<p v-bind:key="state">
{{ state }}
</p>
</transition>
</div>
<script src="src/index.js"></script>
</body>
</html>
We can rotate to different state
value from the states
array. Then we see transition each time the value of state
changes.
Also, we can write out the v-if
statements for each item as follows:
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link
rel="stylesheet"
type="text/css"
href="./src/styles.css"
media="screen"
/>
</head>
<body>
<div id="app">
<button v-on:click="rotate">
Rotate
</button>
<transition name="fade">
<p v-if="state === 'foo'" key="foo">
foo
</p>
<p v-if="state === 'bar'" key="bar">
bar
</p>
<p v-if="state === 'baz'" key="baz">
baz
</p>
</transition>
</div>
<script src="src/index.js"></script>
</body>
</html>
The rest are unchanged.
Transition Modes
We can set the transition modes to control how old elements are transition to new elements.
There’re 2 transition modes:
in-out
— new element transitions in first, then the current element transitions outout-in
— current element transitions our first and then the new element transitions in
For example, we can use it as follows:
src/index.js
:
new Vue({
el: "#app",
data: { states: ["foo", "bar", "baz"], state: "", index: 0 },
methods: {
rotate() {
this.index = (this.index + 1) % this.states.length;
this.state = this.states[this.index];
}
}
});
src/styles.css
:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link
rel="stylesheet"
type="text/css"
href="./src/styles.css"
media="screen"
/>
</head>
<body>
<div id="app">
<button v-on:click="rotate">
Rotate
</button>
<transition name="fade" mode="out-in">
<p v-bind:key="state">
{{ state }}
</p>
</transition>
</div>
<script src="src/index.js"></script>
</body>
</html>
With the mode
attribute added, we no longer have the jumpiness that the previous example has when transitioning between elements since old and new elements won’t exist on the same screen.
Computed Properties
To make more template expressions more concise and reusable, we can create computed properties to compute results from existing properties as their value changes.
We can define computed properties in a Vue instance by putting them in the computed
property of the options object that we pass into the Vue
constructor.
For example, we can write the following in src/index.js
:
new Vue({
el: "#app",
data: {
message: "foo"
},
computed: {
spelledMessage() {
return this.message.split("").join("-");
}
}
});
Then we can use it like any other field in our HTML template. So in index.html
, we can write:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<p>{{message}}</p>
<p>{{spelledMessage}}</p>
</div>
<script src="./src/index.js"></script>
</body>
</html>
We should then get:
foof-o-o
on our screen.
The spelledMessage
method is a getter that’s used by Vue.js. It takes the returned value of it and then put it in place of {{spelledMessage}}
.
Therefore, the name of the placeholder in the template must be the same as the name of the method that returns the value that we want in the computed
object.
We can also get the value from the returned Vue instance as follows:
const vm = new Vue({
el: "#app",
data: {
message: "foo"
},
computed: {
spelledMessage() {
return this.message.split("").join("-");
}
}
});console.log(vm.spelledMessage);
We should see f-o-o
logged from the console.log
.
We can bind to computed properties in templates like any other property. Vue knows that vm.spelledMessage
depends on vm.message
, so the bindings will be updated when vm.message
is updated.
The computed getter method doesn’t commit side effects, which makes it easy to test and understand.
Computed Caching vs Methods
One good feature of computed properties is that the result of it is cached.
There’s no caching for results returned from methods.
So instead of using methods, we should use computed properties for cases where we’re computing something from existing properties.
A computed property is only re-evaluated once a reactive property is updated.
For example, if we have:
const vm = new Vue({
el: "#app",
data: {
message: "foo"
},
computed: {
now() {
return Date.now();
}
}
});
It’ll never update after its first computed since it doesn’t depend on any reactive property from the data
property.
Without caching computed properties keep getting computed as reactive properties that they depend on change, which means the app gets slow as reactive properties that the computed property depends on are changing.
Computed vs Watched Property
Watch properties are useful for watching for changes in a property as the name suggests.
However, because of the caching feature of computed properties, we should use them as much as we can.
For instance, we can simplify the following:
new Vue({
el: "#app",
data: {
name: "Joe",
age: 1,
person: undefined
},
watch: {
name: {
immediate: true,
handler(newVal) {
this.person = `${newVal} - ${this.age}`;
}
},
age: {
immediate: true,
handler(newVal) {
this.person = `${this.name} - ${newVal}`;
}
}
}
});
to:
new Vue({
el: "#app",
data: {
name: "Joe",
age: 1
},
computed: {
person() {
return `${this.name} - ${this.age}`;
}
}
});
As we can see, the first example was much more complex. We have to set immediate
to true
in each property so that it’ll watch the initial value change. Then we have to define the value change handler for each reactive property.
Then in each handler, we have to set the person
property so that we can combine name
and age
.
On the other hand, with computed properties, we only have one method which returns the fields combined in the way we want.
It’s much more convenient for us and the app that we create is also faster because of caching.
Computed Setter
We can also create a setter function for computed properties.
For example, we can write:
new Vue({
el: "#app",
data: {
name: "Joe",
age: 1
},
computed: {
person: {
get() {
return `${this.name} - ${this.age}`;
},
set(newValue) {
const [name, age] = newValue.split("-");
this.name = name.trim();
this.age = age.trim();
}
}
}
});
The getter function returns the same thing as before, but now we also have a setter function.
The setter function splits the newValue
, which has the computed value from the getter function, we split the result and set the result back to the corresponding reactive properties.
We can use a setter as follows. We can put the following in src/index.js
:
new Vue({
el: "#app",
data: {
firstName: "Joe",
lastName: "Smith"
},
computed: {
name: {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(newValue) {
const [firstName, lastName] = newValue.split(" ");
this.firstName = (firstName || "").trim();
this.lastName = (lastName || "").trim();
}
}
}
});
And in index.html
, we put:
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> <body>
<div id="app">
<p>{{name}}</p>
<input type="text" v-model="name" />
</div>
<script src="./src/index.js"></script>
</body>
</html>
Then when we change the value in the input, we get the new values assigned back to the firstName
and lastName
fields with the setter.
Conclusion
Vue makes building front end apps easy.
We just have to start by creating a Vue instance.
Then we can add some markup to a template and use JavaScript to create some logic.
In the template, we can render things conditionally and we can render arrays of data.
Then to make them fancier, we can add transitions.