2

I want to create a simple accordion like structure but somehow I can't manage to toggle individual elements:

<div v-for="qa, j in group.questions_answers" :key="j">
  <div class="question" @click="toggle()" > <!-- use index here? -->
    <span v-if="itemOpen" class="font-iconmoon icon-accordion-up"><span/>
    <span v-else class="font-iconmoon icon-accordion-down"><span/>
    <span class="q-text" v-html="qa.question"><span/>
  </div>
  <div v-if="itemOpen" class="answer" v-html="qa.answer"></div>
</div>

How would I go about toggling indiviual question/answer blocks? Do I need to use refs?

Currently I can toggle all elements ...

export default {
  data() {
    return {
      itemOpen: true
    }
  },
  methods: {
    toggle() { // index
      this.itemOpen = !this.itemOpen
    }
  }
}

I could draft up a Q/A component and then do toggle inside of each Component but I believe that is an overkill for this.

UPDATE I am dealing with following data structure:

{
    "intro_text": "intro text",
    "questions_groups": [
        {
            "group_heading": "heading",
            "questions_answers": [
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {...}
            ]
        },
        {
            "group_heading": "heading 1",
            "questions_answers": [
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {...}
        },
        {
            "group_heading": "heading 2",
            "questions_answers": [
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                },
                {
                    "question": "Question",
                    "answer": "Answer"
                }
                {...}
            ]
        }
    ]
}
mahatmanich
  • 10,791
  • 5
  • 63
  • 82
  • I guess I could do something similar to this using index: https://stackoverflow.com/questions/60902395/vue-js-toggle-clicked-icon-in-v-for-generated-list – mahatmanich Nov 09 '21 at 16:05

2 Answers2

1

This is how to properly mutate a property of an object into an array that you are looping on.

<template>
  <!-- eslint-disable vue/no-v-html -->
  <article>
    <div
      v-for="todo in todoList"
      :key="todo.id"
      class="question"
      @click="toggleMyTodo(todo)"
    >
      <span
        v-if="isTodoDone(todo)"
        class="font-iconmoon icon-accordion-up"
      ></span>
      <span v-else class="font-iconmoon icon-accordion-down"></span>
      <span class="q-text" v-html="todo.question"></span>
      <div v-if="isTodoDone(todo)" class="answer" v-html="todo.answer"></div>
    </div>
  </article>
</template>

<script>
export default {
  data() {
    return {
      todoList: [
        {
          id: 1,
          task: 'Cook',
          done: false,
          question: 'duh?',
          answer: 'ahh okay!',
        },
        {
          id: 2,
          task: 'Hover',
          done: false,
          question: 'duh?',
          answer: 'ahh okay!',
        },
        {
          id: 3,
          task: 'Washing machine',
          done: false,
          question: 'duh?',
          answer: 'ahh okay!',
        },
      ],
    }
  },
  methods: {
    toggleMyTodo({ id }) {
      const currentTodoIndexToToggle = this.todoList.findIndex(
        (todo) => todo.id === id
      )
      // $set is used because of this https://v2.vuejs.org/v2/guide/reactivity.html#For-Arrays
      this.$set(this.todoList, currentTodoIndexToToggle, {
        ...this.todoList[currentTodoIndexToToggle],
        done: !this.todoList[currentTodoIndexToToggle].done,
      })
    },
    isTodoDone({ id }) {
      // I'm using `?.` just to be sure https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
      return this.todoList.find((todo) => todo.id === id)?.done
    },
  },
}
</script>

Btw, please do not use v-html as is but pass it through a sanitizer.


PS: we're using this.$set because of some caveats in Vue2, more details here.

kissu
  • 40,416
  • 14
  • 65
  • 133
  • I don't have an `id`, neither do I have a `done` flag for each element. For `id` I guess I can use `index` here. I am using v-html without sanitization because I know the source and it is not a user input. html comes from a legacy cms ... – mahatmanich Nov 10 '21 at 07:20
  • @mahatmanich nope, don't use the index here, it is totally counter productive and doing the opposite of what it's supposed to do. Also, if you don't have an ID and a status check, loop on your array with a `.map` and add those! – kissu Nov 10 '21 at 07:35
  • Such a simple task so much overhead. jQuery used to be very handy in these tasks ... it might be easier to introduce another component level and have at it on the individual component rather? – mahatmanich Nov 10 '21 at 07:58
  • @mahatmanich I mean, 5 lines of code for something declarative with reactive benefits out of the box afterwards, is still totally fine in terms of overhead IMO. Add 2 more if your dataset doesn't have an ID (+ boolean flag), meanwhile I don't really know any backend that doesn't send any ID out of the box when implemented as a regular REST API. As if you need another component, I don't think that it is anyhow needed here, it all depends of the actual complexity of your app. From what you've told so far here, it is not worth it, simply `.map` and your dataset and add the missing properties! – kissu Nov 10 '21 at 08:28
  • You are probably right. I am new to js frameworks and did a lot of lifting with jquery so I am still trying to get to the nuts and bolts. It is just a much different way of getting things done. Mostly intuitve, but sometimes not so much ... here I thought it might be easier. – mahatmanich Nov 10 '21 at 09:04
  • @mahatmanich Vue is still one of the easiest yep powerful and flexible that you can find out here. – kissu Nov 10 '21 at 09:07
0

Since I did not have an id and done field on the individual question/answer object and I would have needed to loop through several q/a groups, It was easier and more performant to create a sub component in that case to target the individual q/a rows for toggle:

<template>
  <div>
    <slot-main-heading>{{$t('faq.h1')}}</slot-main-heading>
    <div v-html="introText"></div>
    <div v-for="(group, i) in questionsGroups" :key="i">
      <slot-htwo-heading>{{group.group_heading}}</slot-htwo-heading>
      <template v-for="(qa, j) in group.questions_answers">
        <cms-accordion-row :qaData="qa" :key="j"/>
      </template>
    </div>
  </div>
</template>

cmsAccordionRow.vue

<template>
<div>
  <div class="question" @click="toggle()">
      <span v-if="itemOpen" class="font-iconmoon icon-accordion-up" ></span>
      <span v-else class="font-iconmoon icon-accordion-down" ></span>
      <span class="q-text" v-html="qaData.question" ></span>
  </div>
  <div v-if="itemOpen" class="answer" v-html="qaData.answer"></div>
</div>
</template>

<script>
export default {
  props: {
    qaData: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      itemOpen: true
    }
  },
  methods: {
    toggle() {
      this.itemOpen = !this.itemOpen
    }
  }
}
</script>

mahatmanich
  • 10,791
  • 5
  • 63
  • 82
  • I'm not really sure of what is happening here neither of the format of your actual data. Why cannot have some decent dataset here and need to use some finicky tricks? I cannot really help without visuals neither so yeah, stick with the working solution I guess. You will not have access to the dataset later on with a fine-grain control tho. – kissu Nov 10 '21 at 16:19
  • @kissu I updated the data structure in the question. That is what I got to work with ... – mahatmanich Nov 10 '21 at 16:27
  • What does that mean? Where does it come from? Why would you not massage it to your liking? Sometimes the issue is not where it's expected to be. – kissu Nov 10 '21 at 16:28
  • data structure comes from a legacy cms ... sometimes you are stuck with what you get... – mahatmanich Nov 10 '21 at 21:02
  • Still, you can massage the payload or even make it on some backend if you wish, before receiving it back. It's not because something is old that you should not try to keep the code clean and organized. Especially here, it's not that big of a deal. – kissu Nov 10 '21 at 21:10
  • Why is my solution not clean, and organized? I receive the data for one measly page request and I guess there is no easier way to to toggle individual items if you don have an id. I think the solution is quite clean, plus it avoids adding data and running through nested loops twice. Big O does not like that ... – mahatmanich Nov 10 '21 at 21:38
  • 1
    Having some tracking on the actual thing you're looping and having a specific state tied to the element is always nice. As I told you, if you're fine with your solution, keep it like that! – kissu Nov 10 '21 at 21:55