This article and code is for Vue2, if you are migrating to or using Vue 3, please read about how set() isn't needed in Vue3 and how you can upgrade your code or do the same without needing the set() method respectively.
That said, this article still has some useful concepts discussed around the use of objects, negating the value of a property, and using built-in methods. So if you are new to JavaScript and coding, It may still be worth the read.
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.
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.
Understanding why you may need to use the $set() method.
Please note that when we created the tasks, we didn't add the checked property. Our objects looked like this.
tasks: [ {index: 'eat sushi 🍣',todo: 'eat sush 🍣'}, {index: 'study Vue.js', todo: 'study 💚 Vue.js'} ],
See how each object only has an
index
and atodo
property? Because of this, we can't usetask.checked = !task.checked
on our click function. We need to use set() to add the new property and value on the task.You might ask why not add the checked property on the task object in the
addTask()
method, like this.- this.tasks.push({index: this.newTask, todo: this.newTask}) + this.tasks.push({index: this.newTask, todo: this.newTask, checked: false})
That is one possibility, and as an after-the-fact solution that is probably the best way. That said, let's still look at the set() method, as it is a help in our JavaScript and programming journey.
The Vue 2 set() method
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 more about using Vue.js and design, UX/UI, and front-end development, be sure to sign up below for the weekly newsletter.