In this tutorial, you are going to learn the basics of Vue.js. While we learn, we are going to build a Todo app that will help us to put in practice what we learn.

A good way to learn a new framework, It’s by doing a Todo app. It’s an excellent way to compare framework features. It’s quick to implement and easy to understand. However, don’t be fooled by the simplicity, we are going to take it to the next level. We are going to explore advanced topics as well such as Vue Routing, Components, directives and many more!

Let’s first setup the dev environment, so we can focus on Vue! 🖖

Setup

We are going to start with essential HTML elements and CSS files and no JavaScript. You will learn how to add all the JavaScript functionality using Vue.js.

To get started quickly, clone the following repo and check out the start-here branch:

1
2
3
4
5
6
git clone https://github.com/amejiarosario/vue-todo-app.git
cd vue-todo-app
git checkout start-here

npm install
npm start

After running npm start, your browser should open on port http://127.0.0.1:8080 and show the todo app.

todo-app

Try to interact with it. You cannot create a new Todos, nor can you delete them or edit them. We are going to implement that!

Open your favorite code editor (I recommend Code) on vue-todo-app directory.

Package.json

Take a look at the package.json dependencies:

package.json (fragment)
1
2
3
4
5
6
7
8
"dependencies": {
"todomvc-app-css": "2.1.2",
"vue": "2.5.17",
"vue-router": "3.0.1"
},
"devDependencies": {
"live-server": "1.2.0"
}

We installed Vue and VueRouter dependencies. Also, we have the nice CSS library for Todo apps and live-server to serve and reload the page when we make changes. That’s all we would need for this tutorial.

index.html

Open the index.html file. There we have the basic HTML structure for the Todo app that we are going to build upon:

  • Line 9: Loads the CSS from NPM module node_modules/todomvc-app-css/index.css.
  • Line 24: We have the ul and some hard-coded todo lists. We are going to change this in a bit.
  • Line 75: we have multiple script files that load Vue, VueRouter and an empty app.js.

Now, you know the basic structure where we are going to work on. Let’s get started with Vue! 🖖

Getting started with Vue

As you might know…

Vue.js is a reactive JavaScript framework to build UI components.

It’s reactive because the data and the DOM are linked. That means, that when data changes, it automatically updates the DOM. Let’s try that!

Vue Data & v-text

Go to app.js and type the following:

app.js
1
2
3
4
5
6
const todoApp = new Vue({
el: '.todoapp',
data: {
title: 'Hello Vue!'
}
});

Notice the 2nd line with el: '.todoapp'. The el is the element where Vue is going to be mounted.

If you notice in the index.html that’s the section part. As shown in the fragment below.

index.html (fragment)
1
2
3
<body>

<section class="todoapp">

Going back to the app.js file, let’s now take a look into the data attribute, that binds the title. The data object is reactive. It keeps track of changes and re-render the DOM if needed. Go to the index.html page and change <h1>todos</h1> for <h1>{{ title }}</h1>. The rest remains the same:

index.html (fragment)
1
2
3
4
5
6
<section class="todoapp">
<header class="header">
<h1>{{ title }}</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<!-- ... -->

If you have npm start running you will see that the title changed!

You can also go to the console and change it todoApp.title = "Bucket List" and see that it updates the DOM.

vue

Note: besides the curly braces you can also use v-text:

index.html (fragment)
1
<h1 v-text="title"></h1>

Let’s go back to app.js and do something useful inside the data object. Let’s put an initial todo list:

app.js (fragment)
1
2
3
4
5
6
7
8
9
10
11
const todoApp = new Vue({
el: '.todoapp',
data: {
title: 'Todos',
todos: [
{ text: 'Learn JavaScript ES6+ goodies', isDone: true },
{ text: 'Learn Vue', isDone: false },
{ text: 'Build something awesome', isDone: false },
],
}
});

Now that we have the list on the data, we need to replace the <li> elements in index.html with each of the elements in the data.todos array.

Let’s do the CRUD (Create-Read-Update-Delete) of a Todo application.

review diff

READ: List rendering with v-for

As you can see everything starting with v- is defined by the Vue library.

We can iterate through elements using v-for as follows:

index.html (fragment)
1
2
3
4
5
6
7
8
<li v-for="todo in todos">
<div class="view">
<input class="toggle" type="checkbox">
<label>{{todo.text}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Rule the web">
</li>

You can remove the other <li> tag that was just a placeholder.

review diff

CREATE Todo and event directives

We are going to implement the create functionality. We have a textbox, and when we press enter, we would like to add whatever we typed to the list.

In Vue, we can listen to an event using v-on:EVENT_NAME. E.g.:

  • v-on:click
  • v-on:dbclick
  • v-on:keyup
  • v-on:keyup.enter

Protip: since v-on: is used a lot, there’s a shortcut @. E.g. Instead of v-on:keyup.enter it can be @keyup.enter.

Let’s use the keyup.enter to create a todo:

index.html (fragment)
1
2
3
<input class="new-todo" placeholder="What needs to be done?"
v-on:keyup.enter="createTodo"
autofocus>

On enter we are calling createTodo method, but it’s not defined yet. Let’s define it on app.js as follows:

app.js (fragment)
1
2
3
4
5
6
7
methods: {
createTodo(event) {
const textbox = event.target;
this.todos.push({ text: textbox.value, isDone: false });
textbox.value = '';
}
}

review diff

Applying classes dynamically & Vue v-bind

If you click the checkbox (or checkcirlcle) we would like the class completed to be applied to the element. We can accomplish this by using the v-bind directive.

v-bind can be applied to any HTML attribute such as class, title and so forth. Since v-bind is used a lot we can have a shortcut :, so instead of v-bind:class it becomes :class.

index.html (fragment)
1
<li v-for="todo in todos" :class="{ completed: todo.isDone }">

Now if a Todo list is completed, it will become cross out. However, if we click on the checkbox, it doesn’t update the isDone property. Let’s fix that next.

review diff

Keep DOM and data in sync with Vue v-model

The todos have a property called isDone if it’s true we want the checkbox to be marked. That’s data -> DOM. We also want if we change the DOM (click the checkbox) we want to update the data (DOM -> data). This bi-directional communication is easy to do using v-model, it will keep it in sync for you!

1
<input class="toggle" type="checkbox" v-model="todo.isDone">

If you test the app now, you can see when you click the checkbox; also the text gets cross out. Yay!

You can also go to the console and verify that if you change the data directly, it will immediately update the HTML. Type the following in the browser console where you todo app is running:

1
todoApp.todos[2].isDone = true

You should see the update. Cool!

UPDATE todo list with a double-click

We want to double click on any list and that it automatically becomes a checkbox. We have some CSS magic to do that, the only thing we need to do is to apply the editing class.

index.html (fragment)
1
2
3
4
5
6
7
8
9
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
<li v-for="todo in todos" :class="{ completed: todo.isDone }">
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.isDone">
<label>{{todo.text}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Rule the web">
</li>

Similar to what we did with the completed class, we need to add a condition when we start editing.

Starting with the label, we want to start editing when we double-click on it. Vue provides v-on:dblclick or shorthand @dblclick:

1
<label @dblclick="startEditing(todo)">{{todo.text}}</label>

In the app.js we can define start editing as follows:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const todoApp = new Vue({
el: '.todoapp',
data: {
title: 'Todos',
todos: [
{ text: 'Learn JavaScript ES6+ goodies', isDone: true },
{ text: 'Learn Vue', isDone: false },
{ text: 'Build something awesome', isDone: false },
],
editing: null,
},
methods: {
createTodo(event) {
const textbox = event.target;
this.todos.push({ text: textbox.value, isDone: false });
textbox.value = '';
},
startEditing(todo) {
this.editing = todo;
},
}
});

We created a new variable editing in data. We just set whatever todo we are currently editing. We want only to edit one at a time, so this works perfectly. When you double-click the label, the startEditing function is called and set the editing variable to the current todo element.

Next, we need to apply the editing class:

index.html (fragment)
1
<li v-for="todo in todos" :class="{ completed: todo.isDone, editing: todo === editing }">

When data.editing matches the todo , then we apply the CSS class. Try it out!

If you try it out, you will notice you can enter on edit mode, but there’s no way to exit from it (yet). Let’s fix that.

index.html (fragment)
1
2
3
4
5
<input class="edit"
@keyup.esc="cancelEditing"
@keyup.enter="finishEditing"
@blur="finishEditing"
:value="todo.text">

First, we want the input textbox to have the value of the todo.text when we enter to the editing mode. We can accomplish this using :value="todo.text". Remember that colon : is a shorthand for v-bind.

Before, we implemented the startEditing function. Now, we need to complete the edit functionality with these two more methods:

  • finishEditing: applies changes to the todo.text. This is triggered by pressing enter or clicking elsewhere (blur).
  • cancelEditing: discard the changes and leave todos list untouched. This happens when you press the esc key.

Let’s go to the app.js and define these two functions.

app.js (fragment)
1
2
3
4
5
6
7
8
9
finishEditing(event) {
if (!this.editing) { return; }
const textbox = event.target;
this.editing.text = textbox.value;
this.editing = null;
},
cancelEditing() {
this.editing = null;
}

Cancel is pretty straightforward. It just set editing to null.

finishEditing will take the input current’s value (event.target.value) and copy over the todo element that is currently being edited. That’s it!

review diff

DELETE todo list on @click event

Finally, the last step to complete the CRUD operations is deleting. We are going to listen for click events on the destroy icon:

index.html (fragment)
1
<button class="destroy" @click="destroyTodo(todo)"></button>

also, destroyTodo implementation is as follows:

app.js (fragment)
1
2
3
4
destroyTodo(todo) {
const index = this.todos.indexOf(todo);
this.todos.splice(index, 1);
},

review diff

Trimming inputs

It’s always a good idea to trim user inputs, so any accidental whitespace doesn’t get in the way with textbox.value.trim().

review diff

Items left count with computed properties

Right now the item left count is always 0. We want the number of remaining tasks. We could do something like this:

anti-example
1
<strong>{{ todos.filter(t => !t.isDone).length }}</strong> item(s) left</span>

That’s a little ugly to stick out all that logic into the template. That’s why Vue has the computed section!

app.js (fragment)
1
2
3
4
5
computed: {
activeTodos() {
return this.todos.filter(t => !t.isDone);
}
}

Now the template is cleaner:

index.html (fragment)
1
<strong>{{ activeTodos.length }}</strong> item(s) left</span>

You might ask, why use a computed property when we can create a method instead?

Computed vs. Methods. Computed properties are cached and updated when their dependencies changes. The computed property would return immediately without having to evaluate the function if no changes happened. On the other hand, Methods will always run the function.

Try completing other tasks and verify that the count gets updated.

items-left

review diff

Clearing completed tasks & conditional rendering with v-show

We want to show clear completed button only if there are any completed task. We can accomplish this with the v-show directive:

index.html (fragment)
1
<button class="clear-completed" @click="clearCompleted" v-show="completedTodos.length">Clear completed</button>

The v-show will hide the element if the expression evaluates to false or 0.

One way to clearing out completed tasks is by assigning the activeTodos property to the todos:

app.js (fragment)
1
2
3
clearCompleted() {
this.todos = this.activeTodos;
}

Also, we have to add the computed property completedTodos that we use in the v-show

app.js (fragment)
1
2
3
completedTodos() {
return this.todos.filter(t => t.isDone);
}

review diff

Vue Conditional Rendering: v-show vs v-if

v-show and v-if looks very similar, but they work differently. v-if removes the element from the DOM and disable events, while v-show hides it with the CSS display: none;. So, v-if is more expensive than v-show.

If you foresee the element being toggling visibility very often then you should use v-show. If not, then use v-if.

We can hide the footer and central section if there’s no todo list.

index.html (fragment)
1
2
<section class="main" v-if="todos.length">... </section>
<footer class="footer" v-if="todos.length">...</footer>

review diff

Local Storage

On every refresh, our list gets reset. This is useful for dev but not for users. Let’s persist our Todos in the local storage.

Local storage vs. Session storage. Session data goes away when you close the window or expire after a specific time. Local storage doesn’t have an expiration time.

The way localStorage works is straightforward. It is global variable and has only 4 methods:

  • localStorage.setItem(key, value): key/value storage. key and value are coerced into a string.
  • localStorage.getItem(key): get the item by key.
  • localStorage.removeItem(key): remove item matching the key.
  • localStorage.clear(): clear all items for the current hostname.

We are going to use getItem and setItem. First we need to define a storage key:

app.js (fragment)
1
const LOCAL_STORAGE_KEY = 'todo-app-vue';

Then we replace data.todos to get items (if any) from the local storage:

app.js (fragment)
1
2
3
4
5
6
7
8
9
data: {
title: 'Todos',
todos: JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [
{ text: 'Learn JavaScript ES6+ goodies', isDone: true },
{ text: 'Learn Vue', isDone: false },
{ text: 'Build something awesome', isDone: false },
],
editing: null,
},

We have to use JSON.parse because everything gets stored as a string and we need to convert it to an object.

getItem will retrieve the saved todos from the localstorage. However, we are saying it yet. Let’s see how we can do that.

Vue Watchers

For saving, we are going to use the Vue watchers.

Vue watchers vs. Computed properties. Computed properties are usually used to “compute” and cache the value of 2 or more properties. Watchers are more low level than computed properties. Watchers allow you to “watch” for changes on a single property. This is useful for performing expensive operations like saving to DB, API calls and so on.

app.js (fragment)
1
2
3
4
5
6
7
8
watch: {
todos: {
deep: true,
handler(newValue) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newValue));
}
}
},

This expression watches for changes in our todos data. Deep means that it recursively watches for changes in the values inside arrays and objects. If there’s a change, we save them to the local storage.

review diff

Once you change some todos, you will see they are stored in the local storage. You can access them using the browser’s dev tools:

local storage

The last part to implement is the routing! However, for that, we need to explain some more concepts and will do that in the next post.


In the next tutorial, we are going to switch gears a little bit and go deeper into Vue Components, Routing, and Local Storage. Stay tuned!

Summary: Vue cheatsheet

We learned a lot! Here is a summary:

Binders
Name Description Examples
Mustache Variable that is replaced with variable's value
<h1>{{ title }}</h1>
v-bind Bind to HTML attribute
          
<span v-bind:title="tooltip"></span>
<div v-bind:id="dynamicId"></div>
<button v-bind:disabled="isButtonDisabled">Button</button>
          
          
: Shortcut for v-bind
          
<span :title="tooltip"></span>
<li v-bind:class="{completed: todo.isDone }"></li>
          
          
v-text Inject text into the element
          
<h1 v-text="title"></h1>
          
          
v-html Inject raw HTML into the element
          
<blog-post v-html="content"></blog-post>
          
          
List Rendering
Name Description Examples
v-for Iterate over elements
          
<li v-for="todo in todos"> {{todo.text}}</li>
          
          
v-for Iterate with index
          
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
          
          
v-for Iterate over object's values
          
<li v-for="value in object">
  {{ value }}  
</li>
          
          
v-for Iterate over object's keys/values
          
<li v-for="(value, key) in object">
  {{ key }}: {{ value }}  
</li>
          
          
v-for Iterate with keys, values and index
          
<li v-for="(value, key, index) in object">
  {{index}}.{{ key }}: {{ value }}  
</li>
          
          
Events
Name Description Examples
v-on:click Invoke callback on click
          
<button class="destroy" v-on:click="destroyTodo(todo)"></button>
          
          
@ `@` is shorcut for `v-on:`
          
<input class="edit"
    @keyup.esc="cancelEditing"
    @keyup.enter="finishEditing"
    @blur="finishEditing">
          
          
v-on:dblclick Invoke callback on double-click
          
<label @dblclick="startEditing(todo)">{{todo.text}}</label>
          
          
@keyup.enter Invoke callback on keyup enter
          
<input @keyup.enter="createTodo">
          
          
@keyup.esc Invoke callback on keyup esc
          
<input @keyup.esc="cancelEditing">
          
          
Conditional Rendering
Name Description Examples
v-show Show or hide the element if the expression evaluates to truthy
          
<button v-show="completedTodos.length">Clear completed</button>
          
          
v-if Remove or add the element if the expression evaluates to truthy
          
<footer v-if="todos.length">...</footer>
          
          
Automatic Data<->DOM Sync
Name Description Examples
v-model Keep data and DOM in sync automatially
          
<input class="toggle" type="checkbox" v-model="todo.isDone">
          
          
Vue instance
Example with all attributes
          
// Vue Instance
const todoApp = new Vue({
  // element matcher
  el: '.todoapp',

// Reactive data, when something changes here it gets updated on the templates // data should be a function so every instance get’s a different data data() { return { title: ‘Todos’, editing: null, } },

// invoke this functions on event handlers, etc. methods: { createTodo(event) { const textbox = event.target; this.todos.push({ text: textbox.value.trim(), isDone: false }); textbox.value = ‘’; }, },

// cached methods (only get invokes when data changes) computed: { activeTodos() { return this.todos.filter(t => !t.isDone); }, },

// watch for changes on the data watch: { todos: { deep: true, handler(newValue, oldValue) { localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newValue)); } } }, });