Vue add class on clicked item in loop and remove from other items

Viewed 1179

I have a loop where I loop over an Array.

for each item in this array I render a new component. Now when a user clicks on a certain component I only want to add a class to that component to highlight it and remove it from others that have it. Think of it as a menu active item.

<step-icon
  v-for="(step, currentStep) in steps"
/>

data() {
  return {
    steps: [{foo: 'bar'}, {foo2: 'bar2'}]
  }
}

my step-icon.vue:

<template>
  <div :class="{'selected': selected}" @click="clickStep()">
    hello
  </div>
</template>

data() {
  return {
    selected: false
  }
},
methods: {
  clickStep() {
      this.selected = true;
  }
}

This works only 1 way, I can only add the selected class but never remove it.

2 Answers

I created a simple example illustrating your use case since you didn't provided enough detail to go with. Below you can find the items selected and unselected. Firstly, we added a key isSelected and set it to false as default. This will act as a status for all items.

steps: [
  {key:"0", tec:"foo", isSelected:false},
  {key:"1", tec:"bar", isSelected:false},
  {key:"2", tec:"foo2", isSelected:false},
  {key:"3", tec:"bar2", isSelected:false},
]

Next, we looped over the array and displayed all the items.

  <ul>
    <li 
        v-for="l in steps" 
        id="l.key"
        @click="select(l.key, l.isSelected)"
        v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
        > {{ l.tec }} </li>
   <ul>

Here you can se we have set our status property isSelected on v-bind directive which will add or remove the class based on the value of isSelected.

Next, once the item is clicked we will trigger select method.

  methods: {
    select(key) {
      for (let i = 0; i < this.steps.length; i++) {
        if (this.steps[i].key !== key) {
          this.steps[i].isSelected = false
        }
      }

      this.toggleSelection(key)
    },
    toggleSelection(key) {
      const stepsItem = this.steps.find(item => item.key === key)
      if (stepsItem) {
        stepsItem.isSelected = !stepsItem.isSelected
      }
    }
  }

The select method will firstly unselect all those except the one which is selected and then call toggleSelection which will set the selected Item to true or false.

Complete Code:

new Vue({
  el: '#app',
  data: {
    steps: [
      {key:"0", tec:"foo", isSelected:false},
      {key:"1", tec:"bar", isSelected:false},
      {key:"2", tec:"foo2", isSelected:false},
      {key:"3", tec:"bar2", isSelected:false},
    ]
  },
  methods: {
    select(key) {
      for (let i = 0; i < this.steps.length; i++) {
        if (this.steps[i].key !== key) {
          this.steps[i].isSelected = false
        }
      }

      this.toggleSelection(key)
    },
    toggleSelection(key) {
      const stepsItem = this.steps.find(item => item.key === key)
      if (stepsItem) {
        stepsItem.isSelected = !stepsItem.isSelected
      }
    }
  }
})
.selected {
  background: grey;
}
.notselected {
  background:transparent;
}
<script src="https://unpkg.com/vue@2.6.10"></script>

<div id="app">
  <ul>
    <li 
        v-for="l in steps" 
        id="l.key"
        @click="select(l.key, l.isSelected)"
        v-bind:class="{ selected : l.isSelected, notselected : !l.isSelected }"
        > {{ l.tec }} </li>
   <ul>
</div>

You can keep all the step state in the parent component.

Now the parent component can listen for toggle_selected event from the nested one, and call toggle_selected(step) with the current step as a param.

Toggle_selected method should deselect all steps except the current one, and for the current one just toggle the selected prop.

If You would like to modify more props of the step in the nested component You could use .sync modifier (:step.sync="step") and then this.@emit('update:step', newStepState) in the nested component.

I've also made a snippet (my first). In this example I omitted clickStep and just put @click="$emit('toggle_selected') in the step-icon component.

new Vue({
  el: '#app',
  // for this example only defined component here
  components: {
    'step-icon': {
      props: { step: Object }
    }
  },
  data: {
    steps: [
      { name: 'Alfa', selected: false},
      { name: 'Beta', selected: false},
      { name: 'Gamma', selected: false},
    ]
  },
  methods: {
    toggle_selected(step) {
      this.steps.filter(s => s != step).forEach(s => s.selected = false);
      step.selected = true;
    }
  }
})
#app {
  padding: 2rem;
  font-family: sans-serif;
}
.step-icon {
  border: 1px solid #ddd;
  margin-bottom: -1px;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
}
.step-icon.selected {
  background: #07c;
  color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<!-- Ive used inline-template for this example only -->
<div id="app"> 
  <step-icon v-for="(step, currentStep) in steps" :key="currentStep"
      :step.sync="step" 
      @toggle_selected="toggle_selected(step)" inline-template>
    <div class="step-icon" 
         :class="{selected: step.selected}"
         @click="$emit('toggle_selected')">
      {{ step.name }}
    </div>
  </step-icon>


</div>

Related