Using Vue.js $set() method to set data object properties

Binding a class on an item and controlling it by the truthy value of a data property is a powerful feature of Vue.js. I will look at a few different ways to do this from binding a class to one item to binding a class to many items at the same time while looking at how it works under the hood. To start I will look at the problem I encountered and to finish up look at how versatile what we just learnt is.

The Problem

When I tried to add a class to a list item in Vue.js to highlight a selected item, only one item could be checked at a time and an item could not be unchecked either. See below for what I am talking about.

See the Pen Basic Vue.js Todo App by Simon (@simonramsay) on CodePen.

This was fine when adding an item or deleting an item but not the intended outcome when checking something off. Being able to have many items checked off at the same time is what was needed. In this post, I will guide you through the steps to my solution and try to explain it as clearly as possible.

Setting up our App

In the below app that I made we have a simple input that when you press enter, on key up a task is added to the tasks array in the data property of our Vue instance.

I also used the same code snippet to delete the tasks by passing the index into the method and using splice().

See the Pen Basic Vue.js Todo App by Simon (@simonramsay) on CodePen.

All so simple, and only a few lines of code

So the next job was to enable a user to check a task off as completed.

I added a v-bind directive to bind the class completed to the li items and created a data property. What this does is it binds the class if the value of the data property is true.

Check Class and Style Bindings on Vue.js official site for more information.

<li v-for="(task, index) in tasks" v-bind:class="{completed: completedClass == index }"  > 

<button @click.prevent="completedClass = index; completedClassClick()" >Completed</button>

In this case, if the completed button is clicked the data property completedClass is set to the index of the item. This makes the statement completedClass == index true and therefore the completed class is applied to highlight the item. Also, note the second methodĀ completedClassClick() is called. If you run it in the browser it will console logĀ this.completedClass with the value of the index. wow, that was easy!

..not so quick tiger!

See the Pen Basic Vue.js Todo App by Simon (@simonramsay) on CodePen.

Check it out, we can check something but

  • We can't uncheck it.
  • When we check something else it unchecks the current checked task.

Well that could be confusing, let's fix it

JavaScript Basics

What we need to do is bind the class using a value that can remain true.

In the first example below every time the completed button is clicked the completedClass property is set to a new index, the index of the task just clicked. If the completedClass value and the index of the task match then the CSS class is applied. As such this condition can only be true for one task at a time.

v-on:click.prevent="completedClass = index"

v-bind:class="{completed: completedClass == index }"

Below, I have written some basic pseudo-code to illustrate that we need a unique value.

v-on:click.prevent="set something unique to true"

v-bind:class="{completed: if something unique is true}" 

Now we know what we need, however, our data structure is not correct. The task is a single string item in a list or more technically an array.

tasks = [ task, task, task ]

What we need are our task items to be objects.
In other words, we are adding the item to our list in the array as a string which is a primitive value and as such we are unable to add a property to it, I.e. we can't say

On clicking this item.
Set this item checked.

To do this we need to have an object and then set a property on the object to true.

tasks = [ {task}, {task}, {task} ]

Below is what we need in where the task is an object and checked is a property set to true.

//tasks array from our data object with one task
tasks: [
// task object 
{ 
task: "Learn JavaScript" ,
checked : true
}
 ]

Then the class completed is bound as task.checked is true.

v-bind:class="{completed: task.checked}"
/* CSS */
.completed {
  background: pink;
}

So once again so it sticks.

If our property is true, as it is, the completed class will be bound and as a result, the completed CSS rule will be applied. Also, note that the task will have a property set to true which means it could be filtered on this value for any other reason you need.

Back to our App.

Add Items to the Array as Objects

First, we need to adjust the addTask method like below. This adds our individual tasks to the array as objects.

methods: {
        addTask() {
            if(this.newTask.length > 0) {

-                this.tasks.push(this.newTask)
-                this.newTask = ''
   
+                this.tasks.push({index: this.newTask, todo: this.newTask}) //add the new task an object: property
+                this.newTask = '';

                }
        },

This makes our added tasks objects which will allow us to modify them as separate tasks and to add a new property to the object, which is exactly what we need. Perfect.

Making the ID a unique value would be best for a large application but that would require another method to get the previous ID and increment it by one. Since it is fine as is for this example I will cover that in a future post.

More JavaScript Basics

To set a new property with a value on our objects we can use dot notation in JavaScript like the example below. This takes the day object that we just created, adds a fineWeather property, and sets it to true.

var day = {};
day.fineWeather = true;

To make it so the item can be turned on and off, we need to set the value to the opposite of what it is now.

day.fineWeather = !day.finewWeather;

If you tried that in the console and console logged the value you would see it is true.

Image
firefox console showing the former code output

If you then do it again the value would be set false. This is what we need to do and

Luckily, Vue.js has a $set() Instance Method to use on data

This method is vm.$set()
So let's use it.

Here is the Instance Method we can use on our data properties with the parameters that it takes.

vm.$set( target, propertyName/index, value )

In the case of our App we are

  • Targeting the item which is a task in our tasks. v-for = "task in tasks"
  • Setting a new property on the task checked.
  • Set the value to true or false, the opposite of the current value.
$set(task, 'checked', !task.checked)

We can use the $set() method in a v-on:click directive to set a property to a value when the element is clicked. This stores the state on the task. If we want to store the state in a separate array that is also possible but for now adding the state to the task is just fine.

What this is doing is adding a new property to the object and then negating the value, I.e if the value doesn't exist it makes it exist by creating it and thus is set true, if the property exists its value is set to the opposite of its current value.

See the Pen Vue.js Todo App with completed functionality by Simon (@simonramsay) on CodePen.

That's it, by making our tasks objects we can now set any property on our objects to any values we want, why not try to add an archived property to your objects. I'll follow up on this with how we can use it to filter our tasks In the very near future.

The main lesson learnt is to add items as objects as they are more versatile.

Thanks for reading and if you want to learn how you can edit the tasks in our todo app make sure to check out the next article about v-if and v-model. (coming soon.)