newbiedev
newbiedev

Reputation: 3566

Vuejs - add different classes in elements generated with v-for

I need to add a class to a list group create using bootstrap 5 in my vuejs app. I know about class binding but in my case I'm not sure how to proceed. I want that when the user click on an item inside the list, the clicked item get the disabled active class and the other elements gets only the disabled class. At the moment I have this code in my template

 <ul class="list-group list-group-flush">
   <li class="list-group-item list-group-item-action" v-for="(choice, index) in item.choices" :key="index">
     <small class="" @click.prevent="checkAnswer(item.questionIndex, index)">{{ index }}) {{ choice }}</small>
   </li>
 </ul>

The v-for loop will generate the elements and when an element is clicked a method is called to check the user choice. In my app script I have this code

export default {
  name: 'Survey',
  data() {
    return {
      n: 0,
      answeredQuestions: [],
    }
  },
  mounted() {

  },
  computed: {
    questions() {
      return this.$store.getters.survey;
    },
  },
  methods: {
    showNext() {
      if( this.n < this.questions.length ){
        this.n++
      }
    },
    isAnswered(index) {
      return this.n !== index ? 'hide' : '';
    },
    checkAnswer(questionIndex, choice) {
      this.answeredQuestions.push(true);
      this.showNext();
      ...
    }
  }
}

What's the best way to implement the needed class binding?

Upvotes: 0

Views: 2067

Answers (3)

Peter Pointer
Peter Pointer

Reputation: 4162

You search the internet for vue class binding and it's the first result that pops up:
https://v2.vuejs.org/v2/guide/class-and-style.html

You can use an plain object, object from your data, a function returning an object or simply a string. You can make any attribute dynamic with v-bind:, or simply :.
Your checkAnswer() function can cause a change in classes by manipulating something in data, for example.

See tutorial above for example code. Keep in mind v-bind:class is the same as :class.

The "best way" changes like every week in Vue, just find a way to do it and learn its advantages and disadvantages.

An example would be:

template: let a function generate the classes

<small
  :class="getChoiceClasses(item, choice, index)"
  @click.prevent="checkAnswer(item.questionIndex, index)"
>{{ index }}) {{ choice }}</small>

script: add method

getChoiceClasses(item, choice, index) {
  let classes = {
    active: choice == 1, // for example
    disabled: false, // default
    even: index % 2 == 0
  };

  if (whateverYouNeedToCheck) {
    classes.disabled = true;
  }
  
  return classes;
}

A method is a little slower than a value from data, but it's very minor and only becomes a problem when you have 100s of calls.

Upvotes: 1

Sarah Gro&#223;
Sarah Gro&#223;

Reputation: 10879

There's a lot of unknowns about the rest of your code (how the questions are handled and switched through, etc.), but here's a working example for a single question. So you'll have to adapt this for having multiple questions in your app, but it should push you in the right direction. I used an inline :style attribute in addition to the static styles already present on the <li>, but you could move that to a function as suggeted in Peter's answer, if you prefer.

const app = {
  name: 'Survey',
  data() {
    return {
      n: 0,
      questions: [],
      answeredQuestions: [],
      item: {
        questionIndex: 1,
        choices: ['Lorem', 'Ipsum']
      },
      selectedChoice: null
    }
  },
  mounted() {

  },
  computed: {
    questions() {
      return this.$store.getters.survey;
    },
  },
  methods: {
    showNext() {
      if (this.n < this.questions.length) {
        this.n++
      }
    },
    isAnswered(index) {
      return this.n !== index ? 'hide' : '';
    },
    checkAnswer(questionIndex, choice) {
      this.answeredQuestions.push(choice);
      this.showNext();
    }
  }
};
Vue.createApp(app).mount('#app');
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script>
<div id="app">
  <ul class="list-group list-group-flush">
    <li class="list-group-item list-group-item-action" :class="{disabled: answeredQuestions.length, active: answeredQuestions.includes(index)}" v-for="(choice, index) in item.choices" :key="index" @click.prevent="checkAnswer(item.questionIndex, index)">
      <small class="">{{ index }}) {{ choice }}</small>
    </li>
  </ul>
</div>

Upvotes: 1

palmaone
palmaone

Reputation: 168

If I understand correctly your situation and what you intend to do here I would suggest using the item in the checkAnswer method so that an identifier is used to set a computed property to the current item.questionIndex.

Then you bind the class of each element with a ternary operator condition to check the questionIndex and return the proper classes string: <small :class="questionIndex == item.questionIndex ? 'disabled active':'disabled'" ...

Upvotes: 1

Related Questions