February 16, 2019
This article presents a Vue.js application backed by a RESTful web service. The application manages a Todo list. The user is able to add and delete items from the list, to mark items as done, and to change the text of an item at any point. Additionally, the application supplements Vue
To see the app in action, watch this video which shows the operations and verifies the backend using a testing tool called ReadyAPI.
The Vue.js application is packaged alongside a JavaEE RESTful web service. This UML diagram shows the classes in the design. The classes in blue are belong to the Vue.js application and the tan classes are the JavaEE backend. All the artifacts -- Vue.js HTML and Javascript plus Java code -- are packaged as a single WAR file and deployed to the WildFly app server.
This article focuses on the Vue.js code. For details on the JavaEE part, read this article.
The UI is divided into two section. The top section is the list of Todos. Each item in the list contains form controls for updates - through a "done" checkbox and a text field - and deletes. The lower section is a form that adds items.
The list of items will only be rendered if the Todos array in the Vuex store contains items. Otherwise, a <p> element will be rendered with a "No items added yet" message. This is implemented with a v-if
directive. If the array length is greater than zero, only the unordered list is rendered. If the array is empty, only the paragraph is rendered. The v-if logic is based on a Vuex getter rather than a direct array access.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Vue.js RESTful Demo</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<h1>Vue.js RESTful Demo</h1>
<div id="app">
<h2>Todo List</h2>
<ul v-if="this.$store.getters.todos.length > 0">
<li v-for="todo in this.$store.getters.todos">
<input type="checkbox" :checked="todo.done" @change="setDone(this.event, todo.id)">
<input type="text" :value="todo.text" @change="setText(this.event, todo.id)">
<button @click="deleteTodo(todo)">Delete</button>
</li>
</ul>
<p v-if="this.$store.getters.todos.length == 0">No items added yet</p>
<hr>
<label for="newTodo">New Todo</label>
<input type="text" id="newTodo" v-model="newTodo">
<button @click="addTodo">Add</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="main.js"></script>
</body>
</html>
The list of items is built from a v-for
loop of the Todo array. Each iteration will produce a new <li> element. Inside of the list items, there are form controls for the "done" checkbox, a text field, and a button. Checking the checkbox will toggle the "done" value of the Todo item and trigger a RESTful update of the database record. Editing the text will change the text of the item (and trigger a RESTful update). The delete button initiates a database record deletion through REST.
Because I'm using Vuex, I don't bind directly to Vue properties. This may seem like a step back given how easy form binding is without Vuex. However, the value of Vuex becomes apparent as the app grows and data is handled consistently. This will result in fewer bugs, though admittedly that's not clear from this simple article.
Instead, the form controls invoke Vue methods and use the loop object todo
to parameterize those methods. For setDone() and setText(), I pass in a Javascript event which will give me access to the changed value (the un/checked done or modified text). I also pass in the id of the todo object. This does not change with the manipulation of the control, so I'm safe to use the loop's value. deleteTodo() is also parameterized. I pass in the whole object but only need the id to make the RESTful delete call.
In the lower section, I invoke the addTodo() method from the button press. I also use v-model
binding with newTodo. newTodo is not part of the Vuex store since it is tied to this specific form. I don't anticipate other parts of the application needed access to it. Consequently, I can use the straightforward form binding with v-model. My addTodo() method will pick up the bound value later in the Vue instance code.
The remainder of the HTML5 is uninteresting for a RESTful interaction standpoint but note that I'm using the minified version of Vue. For some reason, I get an error about the this
pointer in Edge if I run the un-minified version.
Vuex manages an array of Todos for the application. The store provides a getter to retrieve the array and four actions for modifying the array: fetchTodos(), addTodo(), updateTodo(), deleteTodo(). fetchTodos() issues an HTTP GET on the /todos URL and retrieves text payload of JSON. The payload converted automatically to a Javascript array of objects and added to the state.todos
property managed by the store. addTodo() will issue an HTTP POST, adding the new item to the backend, and add the item to the todos state variable. deleteTodo() issues an HTTP DELETE, removing the item from the backend, and deletes the item from the array (actually retains all items besides this one). The updateTodo() method issues an HTTP PUT and then replaces the item in the store with the passed-in value.
const store = new Vuex.Store({
strict: true,
state: {
todos: []
},
mutations: {
ADD_TODO(state, todo) {
state.todos.push( todo )
},
DELETE_TODO(state, todo) {
state.todos = state.todos.filter( t => t.id != todo.id )
},
SET_TODOS(state, todos) {
state.todos = todos
},
UPDATE_TODO(state, todo) {
const index = state.todos.findIndex( t => t.id === todo.id )
if( index != -1 ) {
state.todos.splice(index, 1, todo)
} else {
console.log("can't find id=" + todo.id + " to update in vuex")
}
}
},
actions: {
fetchTodos(context) {
axios.get('/vuejs-demo/api/todos').then(response => {
context.commit('SET_TODOS', response.data)
})
},
addTodo(context, todo) {
axios
.post('/vuejs-demo/api/todos', todo)
.then( function(response) {
context.commit('ADD_TODO', response.data)
} )
},
deleteTodo(context, todo) {
axios
.delete('/vuejs-demo/api/todos/' + todo.id)
.then( function(response) {
context.commit('DELETE_TODO', todo)
})
},
updateTodo(context, todo) {
axios
.put('/vuejs-demo/api/todos/' + todo.id, todo)
.then( function(response) {
context.commit('UPDATE_TODO', todo)
})
}
},
getters: {
todos(state) { return state.todos }
}
})
This approach is optimistic, expecting the RESTful service call to do its job and updating the store array regardless. A real implementation would need error handling around the RESTful calls. The calls could easily fail because of a network error, access control, or bad data. In this case, we might conditionally commit the results after notifying the user of the error.
Loading the app calls the created() method as part of the Vue lifecycle. That function calls the store's fetchTodos() action. As presented earlier, that action results in a RESTful call to the backend to retrieve the data. Through binding, the UI elements like the v-for loop are updated and the UI reflects the current state of the database.
The Vue instance manages a single property, newTodo. This is bound via v-model to the lower section of the UI for adding new items. When the user modifies the text field, the newTodo variable is modified. On the button press, addTodo() is invoked which retrieves the current value of this.newTodo
. From that value and a default "done=false", the app forms a new Todo object and submits it to the Vuex store. As a convenience, the newTodo variable is cleared out allowing the user to make another entry without deleting the previous one.
The deleteTodo() method delegates to the Vuex store. Only the id of the todo object is needed to execute a delete.
const app = new Vue({
el: '#app',
store,
data: {
newTodo: null
},
created() {
this.$store.dispatch('fetchTodos')
},
methods : {
addTodo() {
var t = {
"text": this.newTodo,
"done": false
}
this.$store.dispatch('addTodo', t)
this.newTodo = null // clear for next entry
},
deleteTodo(todo) {
this.$store.dispatch('deleteTodo', todo)
},
setDone(event, id) { this.updateTodo( id, event.target.checked, null ) },
setText(event, id) { this.updateTodo( id, null, event.target.value ) },
updateTodo(id, done, text) {
const fromVuex = this.$store.getters.todos.filter( t => t.id === id )
if( fromVuex != null ) {
var d = (done!=null) ? done : fromVuex[0].done
var tx = (text!=null) ? text : fromVuex[0].text
const t = { "id": id, "done": d, "text": tx }
this.$store.dispatch('updateTodo', t)
} else {
console.log("can't retrieve id=" + id + " from vuex store for update")
}
}
}
})
There are a pair of update methods: setDone() and setText(). Both of these result in a general updateTodo() action on the store. They differ in how they pull together the arguments. In the case of setDone(), an event is used to get the modified value. Remember, this is not a v-model bound object so the todo referenced in the :checked retains the old value. Similarly, setText() uses an event to get the modified value. Using the shared updateTodo() method, the current record is retrieved from the store and the changed value -- either the done or the text -- is overlayed on top of the current record, and re-submitted to the store.
By Carl Walker
President and Principal Consultant of Bekwam, Inc