ChoreTracker UX Improvements with Vue.JS
This is a lab project for 67-272: Application Design and Development. The primary objectives of this lab are (1) to give students experience discovering pain points in existing applications and (2) to give students hands-on experience incorporating view components into an existing project using Vue.JS.
As seen in lecture, we can leverage the Vue.JS framework to allow for cool, dynamic effects on the front-end of our application. Part of the reason we may wish to use something like Vue.JS would be to improve the user experience (or UX for short) of an application. Nowadays, the demand for instantaneous results is higher than ever. We want to complete everything in a short of a time as we possibly can. Vue.JS supports this outlook onto completing tasks.
Before going any further, it is worth keeping the Vue.JS documentation open, as you may wish to refer to it to better understand the Vue.JS framework.
Part 1: Setup and Installation
For this lab, we are going to implement several features using Vue.JS into the Chore Tracker lab from before. If you would like to use your previous Chore Tracker solution, go ahead, however the code portions from the lab will be based off of the starter code here, so keep that in mind. If you don't have a strong preference for using your previous code, it is recommended to simply clone the starter code repository.
-
Clone the starter code repository. Be sure to remove any remote connections on the repository!
Now that we have our rails project, we have to do some small setup to add the Vue.JS framework to our project.
The gem we will be using is the vuejs-rails gem.
-
In your gemfile, add:
gem 'vuejs-rails'
anywhere outside of the "groups" and then bundle install
. Make sure all of the gems installed without any issues!
-
In your app/assets/javascripts/application.js
file, add the following line (after //= require_tree .
):
//= require vue
-
Run rails db:migrate
and then go to rails console
to load the testing contexts (for children, tasks, and chores) as some base data. Remember this can be done in rails console through first requiring needed modules:
require 'factory_bot_rails'
require './test/contexts'
include Contexts
and then including the contexts:
create_children
create_tasks
create_chores
You should see some records added to your database. Once you have some records, run rails server
and check to see Chore Tracker is running properly in your browser. Also check the javascript console in the browser and make sure there are no errors. Ask a TA if you are not sure how to open the javascript console in your browser.
-
If you are using Google Chrome and have not done so, install the Vue.JS DevTools, as it allows for properly checking components and their state, in real-time.
-
Create a new file in the project: app/assets/javascripts/chores.js
. Note there are several .coffee
files in here, we will not be using them.
Add and commit your work to Git if you have not done so already.
Part 2: Pain Points
Now we will spend some time re-familiarizing ourselves with the initial pain points of the Chore Tracker application. We are taking an iterative refinement approach to developing Chore Tracker: First we were concerned with simply building a working application, and now we are ready to improve it from a UX standpoint.
This is actually a really important skill to use in industry since stakeholders are looking for results, and then improvement. The sooner you can have something working (some Minimal Viable Product), the better!
Open up the lab questionaire and answer the questions while you perform the following tasks. Do NOT submit your answers until you show it to the TAs and get checked off for this section.
-
Go through and add a new chore to the chores list (you can add new tasks and children if you wish, but just go ahead and add a chore).
-
Edit the status of the chore to be incomplete if it isn't already. Submit and verify the edit was made. If the chore was already incomplete, edit it to be complete and verify the edit was made.
-
Try to delete a chore from the Chore Tracker and respond to the questions in the Google Form before part 2 step 1.
Okay so hopefully by playing around with some Chore CRUD actions on Chore Tracker you see that those actions work, but the process of managing chores is slow and tedious. In what modern application would making a new chore bring you to another page? This is pretty unusable by modern standards, and that is where Vue.JS comes to the rescue.
Stop
Make sure a TA has verified you have Vue.JS properly installed into Chore Tracker and you have had a chance to explore the pain points of the existing Chore Tracker application. After a TA has reviewed your answers, you should submit the form.
Part 3: Adding a New Chore
Now we will start using Vue.JS to manipulate the means by which we perform actions on Chore Tracker. For the sake of this lab, we will only be modifying the Chores page, but you can likely imagine extending the work we do here to the Child and Task pages as well for a similar, unified experience. If you want more practice with Vue.JS (recommeded), then practice modifying the other pages to have similar form actions with Vue.
Also, it is worth acknowledging the forms could use a facelift (from a CSS standpoint), but again we are only concerned with Vue.JS interactions at this point.
Adding the base Vue instance and verify
The first thing we need to do is add the base Vue instance to our application. This is going to handle the overall state of the application, but for now we will condense the scope of the instance to be the body of the Chores table found in chores index page.
-
Wrap a new div
with id="chores_list"
around the table in the chores index page (generally, giving everything in an app an id is good practice anyway, but don't worry about this now).
-
Now, we will go ahead and add in our Vue.JS instance to get the fun started. Open app/assets/javascripts/chores.js
. We don't need to add anything else to our application to link this javascript file, as the //= require_tree .
in application.js
does this for us. First, add some code to load Vue after all other content on the webpage:
var chores;
$(document).on('turbolinks:load', function() {
// Code we will add to this file goes here!
});
Note that all of the javascript code we will write in this file must go in the body of this $(document).on('turbolinks:load', ...
function!!
Define a new Vue instance defined as chores
by such:
//Code to instantiate a blank, new instance of Vue.JS
chores = new Vue({
// Keys, values for the instance go in here
});
Now for the keys within the object we pass as an argument when defining a Vue instance. Note that the code from steps 3-4 go in here.
-
First, the el
key refers to the portion of the app we want the instance to have control over. In this case, we want it to be our div with the id chores_list
. Add this like so:
// within the code to define the instance of Vue assigned to the variable chores
el: '#chores_list',
-
Now, we will want to define our data
field key to keep track of state variables. The only thing we will have for now is an array of chores, which we will initialize to be empty:
// within the code to define the instance of Vue assigned to the variable chores
data: {
chores: []
}
This is good for the Vue instance, for now. We will add fields and methods in a bit.
Now that we have a base Vue instance to work with, we can begin to modify our Chores view to use Vue.
Open app/views/chores/index.html.erb
and take a look at the contents. Note that it mainly contains a table being populated by iterating over a Rails variable @chores
, which is the list of chores we populate within the Chores controller. Now that we are using Vue.JS, we are going to have to modify this structure of representing iteration to allow us to use dynamically updated state variables within our Vue instance.
Basically, because Vue allows for us to automatically update views based on changing state variables, we can change our HTML to allow for dynamic front-end updates with no cheeky AJAX hide and render maneuvers. We can create a general chore-row component for each row in the table for dynamic updating.
-
To do this, lets define a new component within our app/assets/javascripts/chores.js
file above the definition of the Vue instance from before as such, with the template being a div with the id chore-row
which we will write soon:
// A component describing a row in the chores table
Vue.component('chore-row', {
// Defining where to look for the HTML template in the index view
template: '#chore-row',
// Passed elements to the component from the Vue instance
props: {
chore: Object
},
// Behaviors associated with this component
methods: {
remove_record: function(chore){
},
toggle_complete: function (chore){
},
}
})
Note we have a chore
prop, which is the Chore object which will be passed to the component. Now, we can start to write the general HTML template for the chore-row
component.
-
At the bottom of the Chore#index
view file, add the following template code (At the very bottom, not wrapped in any tags!):
<!-- Defining Vue templates to work with components -->
<script type="text/x-template" id="chore-row">
<tr>
<td>{{ chore.child_id }}</td>
<td>{{ chore.task_id }}</td>
<td>{{ chore.due_on }}</td>
<td>{{ chore.completed }}</td>
<td v-on:click="toggle_complete(chore)">Check</td>
<td v-on:click="switch_edit_modal(chore)">Edit</td>
<td v-on:click="remove_record(chore)">Delete</td>
</tr>
</script>
Take a minute to look at the template code. The first four columns in the table are various fields of the Chore object. The last three are actions which will be instantiated through a click, however the methods toggle_complete
, switch_edit_modal
, and remove_record
have not been filled in yet in the chore-row
component! We will come back to these later.
Now that we have our template, we need to actually create HTML elements based on it.
-
Replace the lines looping over @chores
populating the table (from the @chores.each
all the way to the corresponding end
tag), with the following:
<!--
Template for creating a table row display
v-for iterates over values in the Vue instance
v-bind binds props to be sent to components
-->
<tr is='chore-row'
v-for='chore in chores'
v-bind:chore='chore'>
</tr>
This will loop over the chores
variable defined in our Vue instance and create chore-row
elements for each individual chore based on the template we added right before.
There is only one issue, the chores
variable in the Vue instance is always empty! We need to populate it. Here, we will use AJAX to perform a GET
request to get all the chores.
-
Add the following method at the top of chores.js
right under the line that says var chores;
since we need the page to load prior to running our javascript code:
function run_ajax(method, data, link, callback=function(res){chores.get_chores()}){
$.ajax({
method: method,
data: data,
url: link,
success: function(res) {
callback(res);
},
error: function(res) {
console.log("error");
// Will update with an error handling function later
}
})
}
This code will perform a general AJAX request, so long as we pass the HTML verb, data, and route needed. We also allow for a custom success callback function to handle getting data back. Note this is using the jQuery ajax
function, which you can read more about here.
-
Now we need to call it, let's add the methods
property to the Vue instance and the method to make an AJAX call to get chores following code inside (Be sure to add a comma after the data {} in the instance!):
methods: {
get_chores: function(){
run_ajax('GET', {}, '/chores.json', function(res){chores.chores = res});
}
},
-
Then, add the mounted
property to the Vue instance (mounted: function(){}
) and within add the following line:
mounted: function(){
this.get_chores();
}
Note that this
refers to the Vue instance and we want to call the get_chores
method in the instance now.
Now we are getting the chores from our API and updating the instance variable accordingly. Check the page to see that the table is being populated as expected. Try using the Vue DevTools to see the components as well! You should be able to see the various ChoreRow
components with their Chore objects.
If the page is not rendering the components, try restarting your server!
-
The only problem all of this is that we are looking at a set of ID numbers, which to most users is completely useless. This is a challenge, though, since we are not iterating over an ActiveRecord collection, we must perform a join operation ourselves using javascript for child and task to display properly. To do this we must add the following to methods
within the Vue instance (Be sure to add a comma after the get_chores
function here!):
find_child_name: function(chore){
var desired_id = chore.child_id;
for (var child = 0; child < this.children.length; child += 1){
if (this.children[child]['id'] == desired_id){
return this.children[child]['first_name'].concat(' ', this.children[child]['last_name']);
}
}
return "No name"
},
find_task_name: function(chore){
var desired_id = chore.task_id;
for (var task = 0; task < this.tasks.length; task += 1){
if (this.tasks[task]['id'] == desired_id){
return this.tasks[task]['name'];
}
}
return "No task"
},
and then replace the looping <tr>
HTML in your index page with:
<tr is='chore-row'
v-for="chore in chores"
v-bind:chore="chore"
v-bind:name="find_child_name(chore)"
v-bind:task='find_task_name(chore)'>
</tr>
We also need to pull tasks and children from the backend, so in chores.js
add to the instance's data:
tasks: [],
children: [],
add to props
in the ChoreRow component:
name: String,
task: String
add to the instance's methods:
get_tasks: function(){
run_ajax('GET', {}, '/tasks.json', function(res){chores.tasks = res;});
},
get_children: function(){
run_ajax('GET', {}, '/children.json', function(res){chores.children = res;});
},
add to mounted
in the instance:
this.get_tasks();
this.get_children();
and then update the template at the bottom of your index page code to just call name
and task
!
For reference, the chores.js
file at this point should look like this file.
Before moving on, if your site does not look like this image, ask a TA for help!
Adding a new chore form
Now, we need to modify the form for adding a new chore so it shows up dynamically at the bottom of the page without any kind of redirect.
- The existing button links to the
chores#new
page, so we should get rid of that button from the chores#index
page. Replace it with this new button within the chores_list
div:
<!-- v-on:click binds a function to a clicking action -->
<button v-on:click="switch_modal()">Add Chore</button>
This button will simply toggle whether or not we show the new Chore form underneath the table.
- Let's add this corresponding method to the Vue instance in
methods
:
switch_modal: function(event){
this.modal_open = !(this.modal_open);
},
this.modal_open
refers to a data variable for the instance which will track whether or not we show the new form, which we will add next.
-
Add in the modal_open
variable to the data
property of the Vue instance, and initialize it to be false
.
-
Now, let's set up some logic in the chores#index
page for showing the new form conditionally. Under the table (before closing the #chores_list
div), add the following code:
<div v-if="modal_open">
<h3>New Chore</h3>
<new-chore-form></new-chore-form>
</div>
This will conditionally render a component we are about to define called new-chore-form
.
- Let's write the template for this form at the bottom of
chores#index
(At the very bottom, not wrapped in any tags!):
<!-- Component to add a new form -->
<script type="text/x-template" id="new-chore-form">
<div>
<% @chore = Chore.new() %>
<%= render 'vue_form' %>
</div>
</script>
and then add the complete new component in chores.js
:
// A component for adding a new chore
var new_form = Vue.component('new-chore-form', {
template: '#new-chore-form',
data: function () {
return {
child_id: 0,
task_id: 0,
due_on: '',
completed: false
}
},
methods: {
submitForm: function (x) {
new_post = {
child_id: this.child_id,
task_id: this.task_id,
due_on: this.due_on,
completed: this.completed
}
run_ajax('POST', {chore: new_post}, '/chores.json');
}
},
})
Note how we have data variables for the main fields of Chore, and combine them all into one object when we run our POST request to add a new Chore.
- Now, let's create a new file to manage our Vue.JS new form:
app/views/chores/_vue_form.html.erb
. Within this file, add the following partial code to create our form:
<!-- Form template using Vue for adding and editing chores -->
<%= form_for(@chore) do |f| %>
<% if @chore.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@chore.errors.count, "error") %> prohibited this chore from being saved:</h2>
<ul>
<% @chore.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :child_id %><br />
<%= f.collection_select :child_id, Child.alphabetical.active.all, :id, :name, {:prompt => "Select child..."}, "v-model.number" => "child_id" %>
</div>
<div class="field">
<%= f.label :task_id %><br />
<%= f.collection_select :task_id, Task.alphabetical.active.all, :id, :name, {:prompt => "Select task..."}, "v-model.number" => "task_id" %>
</div>
<p>Date: <input type="date" id=""></p>
<div class="field">
<%= f.label :completed %><br />
<%= f.check_box :completed, "v-model.completed" => "completed" %>
</div>
<% end %>
<button v-on:click="submitForm()">Submit</button>
(Note that for child_id
and task_id
, the .number
in v-model.number
specifies the datatype must be a number, a good security practice!).
Now, after we restart our server, we should have a form for adding new Chores!
-
Wait, something isn't right. We are able to view the new form, but no matter what we select for the date, the date isn't being saved properly. If we look in the console we see our "error" message being printed so we can see something is wrong in the post. Use the Vue.JS DevTools extension to see if the NewChoreForm due_on
date value is being updated properly (also look at the server output).
Oh, it isn't! Figure out where in the form we are missing something and add it in (Hint: You just need to add v-model="<figure out what goes here>"
to the date input tag in the code above.).
Now the form should add a new row to the list of chores upon clicking Submit
. Perhaps one small UX improvement would be to hide the form upon clicking Submit
. Knowing we call the submitForm
method upon clicking Submit
, modify this to hide the New Chore Form upon submitting.
Stop
Make sure a TA has verified that you can add a new chore into Chore Tracker using a Vue.JS form (no reloads should be happening).
Part 4: Completing and Deleting a Chore
Now that we have our form setup, we can add some quick other interactions to speed up the user experience:
Toggling Completion
We already added the v-on:click="toggle_complete(chore)"
property to the template for a chore-row
, so now we just need to add the method for actually toggling the method within the chore-row
component (which will work by clicking 'check'):
toggle_complete: function (chore){
chore['completed'] = !(chore['completed']);
run_ajax('PATCH', {chore: chore}, '/chores/'.concat(chore['id'], '.json'));
},
Note here how we simply switch the completed
state and POST it. Vue takes care of re-rendering, so this is all we have to do for completion.
Deletion
Something similar to toggling completion will allow us to delete a chore quickly. Again, note we added v-on:click='remove_record(chore)'
and so clicking Delete
will do the trick once we add this method to the chore-row
component:
remove_record: function(chore){
run_ajax('DELETE', {chore: chore}, '/chores/'.concat(chore['id'], '.json'));
},
Both Completion and Deletion work instantaneously like magic because of the two-way binding relationship we set up between these reactive components!
Stop
Make sure a TA has verified that you can complete and delete chores in Chore Tracker using Vue.JS (no reloads should be happening).
Part 5 Error Handling
Now, try making a chore with no date. Hmm, nothing seems to happen!! We need to account for the errors in the form to display to the user these problems. To do this, we will make a quick component and update the js a little.
- First, let's start by adding a new error-row component in our js file:
Vue.component('error-row', {
// Defining where to look for the HTML template in the index view
template: '#error-row',
// Passed elements to the component from the Vue instance
props: {
error: String,
msg: Array
},
});
Note how simple this component is. We are going to represent the errors in a table fashion.
- Now in the
run_ajax
function, lets add the following line to our error handler (below our console.log
):
chores.errors = res.responseJSON;
the following line to the top of our success handler:
chores.errors = {};
and the following line to the data
field in our Vue instance:
errors: {},
- Now, all we have to do is add the following code within the
div
with id=chores_list
in the index html file:
<div v-if="Object.keys(errors).length > 0">
<h1>Errors</h1>
<table>
<tr>
<th width="125" align="left">Field</th>
<th width="200" align="left">Problem</th>
</tr>
<tr is='error-row'
v-for="(msg, error) in errors"
v-bind:error="error"
v-bind:msg="msg">
</tr>
</table>
</div>
and the following template code for the component (bottom of the index file)!
<script type="text/x-template" id="error-row">
<tr>
<td>{{ error }}</td>
<td>{{ msg }}</td>
</tr>
</script>
Stop
Make sure when you try to create a chore with no date, the error table appears! This will be important for phase 5.