DEV Community

Cover image for Modifying classes in mapped list items in Vue3
Murat Can Yüksel
Murat Can Yüksel

Posted on

Modifying classes in mapped list items in Vue3

In this post, I'll show a way of manipulating UI list elements on click. I didn't know how to do it, I never had to before. I had troubles formulating what I need to do, and finding a onto-the-point example to it. So, I'll try to provide what I so direly needed at one point. I'm sure there are more efficient ways to do it. One thing about this approach is that even though I mention VueJs in the title, you can get the same results with any other big JS framework, like ReactJS.

As it happens in the real world, I want to fetch data from somewhere and display it in the UI. But I want to do more than just to display it, I want to be able to manipulate it in the UI in this or that way. Say, I want to give each individual part displayed in the UI a different color on click, but again, as it happens, I don't have the necessary structure in the data itself. Like, say I've been given a set of names, and I was asked to arrange them as such that the user should be able to determine if one or some of them engages in an action: such as going to a party.

As I've said, the data itself doesn't have anything for me to discern who will do what, it's just a collection of names like the one I've created and put in my public folder so that I can fetch it from there. Check it out =>

{
  "items": [
    { "name": "Anakin", "surName": "Skywalker" },
    { "name": "Padmé", "surName": "Amidala" },
    { "name": "Obi-Wan", "surName": "Kenobi" }
  ]
}

Enter fullscreen mode Exit fullscreen mode

Now, what I want to do is to display these in the UI. But before actually displaying it, I want to save the data somewhere and then inject a key/value pair to each item so that I can control the element in the UI by means of these key/value pairs. In order to do so, I will create an empty array, and map the original data + injected key/value pairs into it with the map method.

So let me start with the script part. Mark that I'll use the script setup syntax of Vue3:

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";

const data = ref();
const changedData = ref();

const getData = async () => {
  try {
    const res = await axios.get("../public/data/data.json");
    console.log(res.data);
    data.value = res.data.items;
    changedData.value = res.data.items.map((item) => ({
      ...item,
      isGoing: false,
    }));
    console.log(res.data.items);
    console.log(changedData.value);
  } catch (error) {
    console.log(error);
  }
};

const handleClick = (item) => {
  item.isGoing = !item.isGoing;
  console.log(item);
};

onMounted(() => {
  getData();
});
</script>

Enter fullscreen mode Exit fullscreen mode

Now, what do I do here? First, I start by importing ref and onMounted from vue. Refs are a way to store reactive data in variables, and onMounted is a hook that let's you call a function when a component mounts the first time, i.e. like when the page loads, starts and all.

I have two reactive variables, data, and changedData. I'll save the data I've fetched in the getData async function in the data variable, and then add some new key/value pair to each and every object in it and save this new object in the changedData variable. In this way, I'll both have not disturbed the original data, and will have the desired type of data with which I can develop my application as I wish.

In getData async function I use try/catch syntax, as it is the best one I know and the simplest for my understanding. Look carefully to this snippet:

 changedData.value = res.data.items.map((item) => ({
      ...item,
      isGoing: false,
    }));


Enter fullscreen mode Exit fullscreen mode

Note: In Vue, refs are called with .value suffix. You can read the official documentation about the different use cases of ref and reactive, they are pretty much the same, but have different accepted use cases as far as I'm concerned.

Anyway, in the above snippet, I use the Javascript map function that creates a shallow copy of the target, without changing the original, iterates over each element in it, and does something with it before saving the new, modified dataset into the changedData variable.

What it does to it is, by using the Javascript spread operator, adding the isGoing:false key/value pair to each and every element in the array of objects that I've fetched.

Now I'll write the template.

template

Look at this snippet:

<template>
  <div class="app">
    <div
      v-for="(item, key) in changedData"
      :key="key"
      class="card"
      @click="handleClick(item)"
      :class="[item.isGoing ? 'going' : 'notGoing']"
    >
      <div class="para">{{ item.name }} {{ item.surName }}</div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

I have a div with a class of app that functions as the container, then I have another div with which I iterate over the items in the changedData variable. I give the index of the element as key, add a class of card in each element, specify a handleClick function that takes the individual item as a parameter, and then use the syntax for specifying dynamic classes in VueJS. Then I just display the contents of the array of objects I have in the div with para class.

There are a couple of different ways of creating dynamic classes in VueJS, but I like this array syntax, as it allows me to write an if/else statement using the ternary operator. It basically says that "if item.isGoing is true, use the class going for this element, and in the case of item.isGoing is false, use the notGoing class for it. Here is the styles I've written:

<style scoped>
.app {
  display: flex;
  flex-direction: column;
  /* justify-content: center; */
  align-items: center;
  padding-top: 5rem;
}
.card {
  margin-bottom: 1rem;
  /* border: 5px solid green; */
  text-align: center;
  width: 250px;
  height: 50px;
  border-radius: 16px;
}
.para {
  font-weight: 700;
}
.going {
  border: 5px solid green;
  text-decoration: none;
  background-color: rgb(56, 219, 56);
}
.notGoing {
  background-color: #ffe01b;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Note: scoped here is a nice feature of Vue, which makes sure that the classnames you use in this component cannot interact with other components who use the same naming for those classes and use different styling, i.e. you can have a hundred components with the class of .card and every single one of them would only target the html in their respective components, so that you don't encounter unexpected breaks in your style.

This is pretty much it, actually. Now, with the code that's written, whenever I click one of the elements displayed on the UI, they'll change color as I've specified that the class going would have a background color of green, and the class notGoing would have a background color of yellow. And if I'd click on the element again, the color would change back to its original state. This logic is made sure by the following snipped:

const handleClick = (item) => {
  item.isGoing = !item.isGoing;
  console.log(item);
};

Enter fullscreen mode Exit fullscreen mode

The handleClick function, by taking the individual item as a parameter, makes sure that with each click item.isGoing will change into its opposite.

Here is a screen shot of one of the items clicked:

Image description

That's it. I hope I helped some of you in this or that way.

Cheers and keep coding!

Discussion (1)

Collapse
ilejohn profile image
Opeyemi Ilesanmi • Edited on

Nice article. You can make your javascript code easier to read by adding 'javascript' at the end of the first three bar-ticks while editing the article.