DATA DRIVEN VIEWS WITH VUE.JS

muyiwa akin-ogundeji
11 min readNov 27, 2015

--

The quality of the UI places a major role in the quality of the UX. In any app built with the MV* pattern, the View is responsible for the UI parts of the app.

For many of us, a common way of implementing the View involves manually manipulating the DOM when data needs to be rendered. While this is all well and good, in this article we will discover the power of using data driven views specifically as implemented in Vue.js. Along the way we will build a simple Todo app which allows todos to be added to a list, marked as complete and also renders various views.

Let’s begin.

The app we wish to build will look like this:

sample Vue.js Todo App

In a previous article i discussed how Vue.js implements the MVVM model. We will begin with the directory structure like this:

/
|- /libs
| |- /css
| | |- bStrap.css
| | |- normalize.css
| |- /js
| |- jQ-2.js
| |- vue.js
|- app.js
|- index.html
NB:
jQ-2 == jQuery 2.1.4
bStrap == Bootstrap 3.3.6
vue == vue 1.0.10

Next we create the base markup:

Base markup for the Vue.js Todo app

With the base markup in hand, we turn our attention to the main app located in app.js. Recall that Vue.js implements MVVM and the central construct in a Vue.js app is the VM. A Vue VM is an instance of the Vue global constructor function.

A Vue VM has the 3 major attributes or properties.

  1. A DOM element identified by the ‘el’ property. This property represents the View to which the VM will be bound.
  2. A Model object which holds the data the VM will use to provide functionality to the app and pass data to the View for rendering. This property is identified by the ‘data’ attribute of the VM.
  3. A ‘methods’ object property which holds the event handlers which the VM uses to handle UI actions passed to it from the View.

Another important property of the VM is a ‘computed’ object which holds data that is dynamically computed by the VM for use in the View. The purpose of the ‘computed’ property is to limit the amount of logic used in the Views to single expressions. This is a very useful feature.

Thus our Vue VM looks like the following:

Basic Vue.js VM for the Todo app

If we run the app as is, we’ll get an error (in the console) from Vue telling us that the ‘#todo’ element doesn’t exist. So we update index.html as follows:

Updated markup for the Vue Todo app

Now when we run the app, we get a blank page with no errors. Which comes first — the VM or the View? The answer is both, we build a little and test a little, ensuring that both VM and View are in sync all the way.

Next step, we define our data properties. We’re going to need the following properties (i’m cheating here :-) ):

  • a ‘todoItem’ representing a single todo item. This will be a string.
  • an array holding incomplete and new todos, lets call it ‘remaining’.
  • an array to hold completed todos. Lets call it ‘completed’.
  • a property which represents the name/identifier collection/list being presently rendered. Lets call it ‘listName’.
  • And not least a property which represents the data content of the collection/list being rendered.

The above looks like this:

updated data object of the Vue Todo app VM

If we run this, nothing happens. Why? Because the View hasn’t been updated. Lets do that next.

<!DOCTYPE html>
<head>
<meta charset=”utf-8">
<title>VUE Todo App | Telios WebDev</title>
<link rel=”stylesheet” href=”libs/css/normalize.css”>
<link rel=”stylesheet” href=”libs/css/bStrap.css”>
<script src=”libs/js/jQ-2.js”></script>
<script src=”libs/js/vue.js”></script>
</head>
<body>
<div class=”container”>
<div class=”row”>
<div class=”col-md-6 col-md-offset-3" id=”todo”>
<h1 class=”text-center”>Vue Todo App</h1>
<div class=”border” id=”todoList”>
<div class=”input-group”>
<input type=”text” class=”form-control” v-model=”todoItem”
@keyup.enter.stop.prevent=”addTodo”>
<button class=”btn btn-success btn-default” id=”addTodo”
@click.stop.prevent=”addTodo”>Add Todo</button>
</div>
<div class=”border” id=”todoListItems”>
<h3 v-show=”showListName”>{{listName}} Items</h3>
<p v-for=”item in displayList”
@dblClick.stop.prevent=”editItem(item, listName)”>
<input type=”checkbox” @click.stop.prevent=”markComplete(item, $event)”>
<span>{{item}}</span>
<button @click.stop.prevent=”removeItem(item, listName)”>X</button>
</p>
</div>
</div>
<pre>{{$data | json}}</pre>
</div>
</div>
</div>
<script src=”app.js”></script>
</body>

If you run this it should look like the following:

updated View of the Vue Todo app

So lets examine what we’ve done so far.

<input type=”text” class=”form-control” v-model=”todoItem”
@keyup.enter=”addTodo”>
<button class=”btn btn-success btn-default” id=”addTodo”
@click=”addTodo”>Add Todo</button>

The code above creates an input element for adding new todos to the collection. Note the use of the ‘v-model’ directive. This provides a 2 way data binding between the input element and the ‘todoItem’ property of the VM. These types of bindings are an important feature of MVVM libraries and frameworks which allows for data driven views i.e. Views which are rendered to reflect the data state of the underlying VM and not as a result of direct DOM manipulations.

Note also the ‘@keyup.enter’ directive which is shorthand for ‘v-on:keyup’ directive to bind the ‘keyup’ event (solely for the ‘enter key) which happens on the input element to the ‘addTodo’ method of the VM. We also use the ‘@click’ directive to bind a click event on the ‘Add Todo’ button to the same handler.

Next notice the ‘.stop.prevent’ directive modifiers which allow us to prevent default action as well as cancel/stop event propagation. If you looked at your console you’d see that Vue displays error message warning us about the ‘addTodo’ function being undefined. We’ll handle that soon.

Next up:

<h3 v-show=”showListName”>{{listName}} Items</h3>
<p v-for=”item in displayList”
@dblClick.stop.prevent=”editItem(item, listName)”>
<input type=”checkbox” @click.stop.prevent=”markComplete(item, $event)”>
<span>{{item}}</span>
<button @click.stop.prevent=”removeItem(item, listName)”>X</button>
</p>

In the snippet above, we see a ‘v-show’ directive binding to a ‘showListName’ property on the VM. I’m sure you’ve noticed that no such property exists on the data Model of the VM, and you’re right. This property is an example of a ‘computed property’. Basically it causes the ‘h3’ tag to be displayed if the ‘showListName’ property is truthy.

Next there’s a text interpolation ‘{{listName}} which simply replaces this with the string value of the ‘listName’ property of the VM data Model. Following that we have a ‘p’ which holds an ‘input’, ‘span’ and ‘button’ which do various things. The ‘p’ itself has a ‘v-for’ directive which causes it to render the contents of a list identified by ‘displayList’ on the VM. ‘displayList’ is presently ‘null’.

The ‘p’ also has a ‘@dblClick’ directive with modifiers which are bound to the ‘editItem’ event handler of the VM. The handler is passed 2 arguments ‘item & listName’. The ‘input’ is a checkbox which when clicked triggers the ‘markComplete’ handler also passing it 2 arguments. The ‘span’ simply displays an item in the ‘displayList’ collection while the button is bound to the ‘removeItem’ handler and passes 2 arguments to it.

<pre>{{$data | json}}</pre>

The snippet above simply allows us to see the content of the data Model of the VM.

At this stage, the View is ahead of the VM, it has bindings for several methods and properties which the VM hasn’t exposed yet. So we’ll play catch up as follows;

//Create the VM
var todoApp = new Vue({
//define the DOM View element
el: ‘#todo’,
//define the Data Model
data: {
todoItem: ‘’,
remaining: [],
completed: [],
listName: ‘’,
displayList: null
},
//define Computed properties
computed: {
showListName: function () {
if(this.all.length) {
return true;
}
return false;
},
all: function () {
return this.remaining.concat(this.completed);
},
remainingItems: function () {
return this.remaining.length;
},
completedItems: function () {
return this.completed.length;
},
allItems: function () {
return this.all.length;
}
},
//define View methods
methods: {
addTodo: function () {
this.remaining.push(this.todoItem);
this.listName = ‘Remaining’;
this.displayList = this.remaining;
return this.todoItem = ‘’;
},
markComplete : function (item, e) {
var index = this.remaining.indexOf(item);
if(e.target.checked === true) {
this.completed.push(item);
return this.remaining.splice(index, 1);
}
return console.log(item +’ was marked incomplete’);
},
markAllItemsComplete: function () {
console.log(‘mark all complete button clicked’);
var self = this;
this.remaining.forEach(function (todo) {
self.completed.push(todo);
});
this.remaining = [];
},
showRemaining: function () {
this.listName = ‘Remaining’;
this.displayList = this.remaining;
return console.log(‘show remaining was clicked’);
},
showCompleted: function () {
this.listName = ‘Completed’;
this.displayList = this.completed;
return console.log(‘show completed was clicked’);
},
showAll: function () {
this.listName = ‘All’;
this.displayList = this.all;
return console.log(‘show all was clicked’);
},
editItem: function (item, listName) {
console.log(‘edit was clicked on ‘, item);
if(!this.todoItem) {
this.removeItem(item, listName);
this.todoItem = item;
return
}
return alert(‘You must finish editing the current item’);
},
removeItem: function (item, listName) {
var
list = listName.toLowerCase(),
index = this[list].indexOf(item);
return this[list].splice(index, 1);
}
}
});

Running the app now will have no errors. We can add todos, mark them as complete and delete them as shown below:

updated Vue.js Todo app

Notice there are 2 items in the ‘completed’ list and a single item in the ‘remaining’ list. I’m sure you can begin to see the power of ‘data driven views’.

Lets examine what we’ve done so far in the main app.

//define Computed properties
computed: {
showListName: function () {
if(this.all.length) {
return true;
}
return false;
},
all: function () {
return this.remaining.concat(this.completed);
},
remainingItems: function () {
return this.remaining.length;
},
completedItems: function () {
return this.completed.length;
},
allItems: function () {
return this.all.length;
}
},

We’ve defined ‘computed’ properties which expose values which the View uses to format the data presented to the User. We finally see the ‘showListName’ property which is ‘truthy’ when there’s something in the ‘all’ collection. The ‘all’ collection itself is a merging of the ‘remaining’ collection and the ‘completed’ collection. Then there are 3 properties which return numeric values — ‘remainingItems’, ‘completedItems’ and ‘allItems’. These simply return the length or number of items in their respective collections.

Next up are the event handlers for the VM.

//define View methods
methods: {
addTodo: function () {
this.remaining.push(this.todoItem);
this.listName = ‘Remaining’;
this.displayList = this.remaining;
return this.todoItem = ‘’;
},
markComplete : function (item, e) {
var index = this.remaining.indexOf(item);
if(e.target.checked === true) {
this.completed.push(item);
return this.remaining.splice(index, 1);
}
return console.log(item +’ was marked incomplete’);
},
markAllItemsComplete: function () {
console.log(‘mark all complete button clicked’);
var self = this;
this.remaining.forEach(function (todo) {
self.completed.push(todo);
});
this.remaining = [];
},
showRemaining: function () {
this.listName = ‘Remaining’;
this.displayList = this.remaining;
return console.log(‘show remaining was clicked’);
},
showCompleted: function () {
this.listName = ‘Completed’;
this.displayList = this.completed;
return console.log(‘show completed was clicked’);
},
showAll: function () {
this.listName = ‘All’;
this.displayList = this.all;
return console.log(‘show all was clicked’);
},
editItem: function (item, listName) {
console.log(‘edit was clicked on ‘, item);
if(!this.todoItem) {
this.removeItem(item, listName);
this.todoItem = item;
return
}
return alert(‘You must finish editing the current item’);
},
removeItem: function (item, listName) {
var
list = listName.toLowerCase(),
index = this[list].indexOf(item);
return this[list].splice(index, 1);
}
}

I guess their use is pretty self explanatory, but take note that in none of these handlers is their any direct DOM manipulation. All the handlers do is receive arguments from the View where appropriate and then manipulate the data within the VM, the change in the data/state of the VM is sync’d with the View and thus we have a full reactive, data driven View-VM relationship.

At this point, the VM is far ahead of the View so we play catch up again.

<!DOCTYPE html>
<head>
<meta charset=”utf-8">
<title>VUE Todo App | Telios WebDev</title>
<link rel=”stylesheet” href=”libs/css/normalize.css”>
<link rel=”stylesheet” href=”libs/css/bStrap.css”>
<script src=”libs/js/jQ-2.js”></script>
<script src=”libs/js/vue.js”></script>
<style>
.border {
border: 1px solid black;
border-radius: 10px;
}
#todo {
margin-top: 60px;
}
#todo li {
display: inline-block;
margin: 0 auto;
padding-left: 10px;
}
.input-group {
position: relative;
}
#addTodo {
float: right;
}
#markAllComplete {
float: left;
}
#todoList {
padding: 10px;
}
#todoList input, #todoList button {
margin-top: 5px;
}
#todoListItems {
margin-top: 10px;
}
#todoListItems button {
float: right;
background: none;
border: none;
color: red;
}
#todoListItems p {
padding: 10px;
font-size: 18px;
}
#todoListItems input[type=”checkbox”] {
padding: 5px;
}
#todoListItems h3 {
text-align: center;
}
</style>
</head>
<body>
<div class=”container”>
<div class=”row”>
<div class=”col-md-6 col-md-offset-3" id=”todo”>
<h1 class=”text-center”>Vue Todo App</h1>
<div class=”border” id=”todoList”>
<div class=”input-group”>
<input type=”text” class=”form-control” v-model=”todoItem”
@keyup.enter=”addTodo”>
<button class=”btn btn-success btn-default” id=”addTodo”
@click=”addTodo”>Add Todo</button>
<button v-show=”remainingItems” class=”btn btn-info btn-default”
@click=”markAllItemsComplete”
id=”markAllComplete”>Mark All Complete</button>
</div>
<br>
<ul>
<li><a href=””
@click.stop.prevent=”showAll”>All Items: {{allItems}}</a></li>
<li><a href=””
@click.stop.prevent=”showRemaining”>Remaning Items: {{remainingItems}}</a></li>
<li><a href=””
@click.stop.prevent=”showCompleted”>Completed Items: {{completedItems}}</a></li>
</ul>
<div class=”border” id=”todoListItems”>
<h3 v-show=”showListName”>{{listName}} Items</h3>
<p v-for=”item in displayList”
@dblClick.stop.prevent=”editItem(item, listName)”>
<input type=”checkbox” @click.stop.prevent=”markComplete(item, $event)”>
<span>{{item}}</span>
<button @click.stop.prevent=”removeItem(item, listName)”>X</button>
</p>
</div>
</div>
<pre>{{$data | json}}</pre>
</div>
</div>
</div>
<script src=”app.js”></script>
</body>

We add some CSS to make the app look nicer and we should have the following as the default View:

Final view of the Vue.js Todo App

Ok, so what do we have?

<button v-show=”remainingItems” class=”btn btn-info btn-default”
@click=”markAllItemsComplete”
id=”markAllComplete”>Mark All Complete</button>

The snippet above adds a ‘Mark All Complete’ button to the View, the button only displays if there’s anything in the ‘remainin’ collection. This is implemeted by the ‘v-show=”remainingItems” ‘ directive which is ‘truthy’ only if the ‘remaingItems’ computed property is true.

Next:

<ul>
<li><a href=””
@click.stop.prevent=”showAll”>All Items: {{allItems}}</a></li>
<li><a href=””
@click.stop.prevent=”showRemaining”>Remaning Items: {{remainingItems}}</a></li>
<li><a href=””
@click.stop.prevent=”showCompleted”>Completed Items: {{completedItems}}</a></li>
</ul>

The code above renders as a navigation counter which does 2 things.

  1. Renders the number of items in the various collections by being bound to the appropriate ‘computed’ property e.g. {{allItems}}.
  2. When the link is clicked e.g. ‘All Items ‘ it triggers an evnt handler on the VM which changes the value of the ‘displayList’. For instance, clicking on ‘All Items ‘ causes ‘displayList’ to be set to the ‘all’ collection and that is what is rendered. the linked codes are:
View:
<li><a href=””
@click.stop.prevent=”showAll”>All Items: {{allItems}}</a></li>
VM:
all: function () {//computed property
return this.remaining.concat(this.completed);
},
allItems: function () {//computed property
return this.all.length;
}
showAll: function () {//VM method i.e. event handler
this.listName = ‘All’;
this.displayList = this.all;
return console.log(‘show all was clicked’);
},

The preceding snippet show cases the interrelationships between the VM and View which makes a data driven View possible.

Example views are:

I trust this article was useful. Thank you for reading, please leave comments below.

--

--