user3541631
user3541631

Reputation: 4008

Vuejs - Accordion

I'm trying to create an accordion using vuejs.

I found some examples online, but what I want is different. For SEO purpose I use "is" and "inline-template", so the accordion is kind of static not fully created in Vuejs.

I have 2 problems/questions:

1) I need to add a class "is-active" on the component based on user interaction(clicks), because of this I receive the following error.

Property or method "contentVisible" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.

This probable because I need to set it at instance level. But "contentVisible" have a value (true or false) different for each component.

So I thought using at instance level an array of "contentVisible" and a props (pass thru instance) and custom events on child to update the instance values.

2) Could work but it is a static array. How can I make a dynamic array (not knowing the number of item components) ?

<div class="accordion">
    <div>
        <div class="accordion-item" is="item"  inline-template :class="{ 'is-active':  contentVisible}" >
            <div>
                <a @click="toggle" class="accordion-title"> Title A1</a>
                <div v-show="contentVisible" class="accordion-content">albatros</div>
            </div>
        </div>
        <div class="accordion-item" is="item"  inline-template :class="{ 'is-active': contentVisible}" >
            <div>
                <a @click="toggle" class="accordion-title"> Title A2</a>
                <div v-show="contentVisible" class="accordion-content">lorem ipsum</div>
            </div>
        </div>

    </div>

var item = {
  data: function() {
      return {
          contentVisible: true
      }
  },

  methods: {
      toggle: function(){
          this.contentVisible = !this.contentVisible
      }
  }
}

new Vue({
    el:'.accordion',
    components: {
        'item': item
    }
})

Update I create the following code but the custom event to send the modification from component to instance is not working, tabsactive is not changing

var item = {
  props: ['active'],
  data: function() {
      return {
          contentVisible: false
      }
  },
  methods: {
      toggle: function(index){
          this.contentVisible = !this.contentVisible;
          this.active[index] = this.contentVisible;
          **this.$emit('tabisactive', this.active);**
          console.log(this.active);
      }
  }
}

new Vue({
    el:'.accordion',
    data: {
      tabsactive: [false, false]
    },
    components: {
        'item': item
    }
})

<div class="accordion" **@tabisactive="tabsactive = $event"**>
        <div class="accordion-item" is="item"  inline-template :active="tabsactive" :class="{'is-active': tabsactive[0]}">
            <div>
                <a @click="toggle(0)" class="accordion-title"> Title A1</a>
                <div v-show="contentVisible" class="accordion-content">albatros</div>
            </div>
        </div>
        <div class="accordion-item" is="item"  inline-template :active="tabsactive" :class="{'is-active': tabsactive[1]}">
            <div>
                <a @click="toggle(1)" class="accordion-title" > Title A2</a>
                <div v-show="contentVisible" class="accordion-content">lorem ipsum</div>
            </div>
        </div>
</div>

Upvotes: 7

Views: 16272

Answers (4)

Shuvo
Shuvo

Reputation: 177

You can try this way.

<template>
    <div role="button" class="group py-10" @click="toggleAccordion">
        <div class="flex items-center justify-between cursor-pointer">
            <span class="font-medium font-butler text-3xl text-blue-700">
                <slot name="title" />
            </span>
            <span>
                <svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <line :class="{ 'opacity-0 w-0': isOpen }" x1="13.9" y1="28" x2="13.9" y2="0" stroke="#835F41"
                        stroke-width="1.2" class="transition-all duration-200 ease-linear" />
                    <line x1="0.5" y1="13.4" x2="28.5" y2="13.4" stroke="#835F41" stroke-width="1.2" />
                </svg>
            </span>
        </div>
        <div ref="contentRef" :style="contentStyles"
            class="overflow-hidden transition-all duration-200 ease-linear font-light font-archivo text-base text-black">
            <slot name="content" />
        </div>
    </div>
</template>

<script setup>
import { ref, computed, nextTick } from 'vue';

// refs
const isOpen = ref(false);
const contentRef = ref(null);

// computed
const contentStyles = computed(() => ({
    maxHeight: isOpen.value ? `${contentRef.value.scrollHeight}px` : '0px',
    opacity: isOpen.value ? 1 : 0,
    marginTop: isOpen.value ? '1.25rem' : '0', // equivalent to 'mt-5' in Tailwind
}));

// functions
async function toggleAccordion() {
    isOpen.value = !isOpen.value;
    // Force a reflow to ensure the transition happens smoothly
    await nextTick();
    if (isOpen.value) {
        contentRef.value.style.maxHeight = `${contentRef.value.scrollHeight}px`;
    } else {
        contentRef.value.style.maxHeight = '0px';
    }
}

</script>


<style scoped>
.content-wrapper {
    transition-property: max-height, opacity, margin-top;
}
</style>

Upvotes: 0

Saurabh
Saurabh

Reputation: 73589

On point 1:

You have to define contentVisible as a vue instance variable, as you have accessed it with vue directive v-show, it searches this in vue data, watchers, methods, etc, and if it does not find any reference, it throws this error.

As your accordion element is associated with the parent component, you may have to add contentVisible data there, like following:

new Vue({
    el:'.accordion',
    data: {
       contentVisible: true
    }
    components: {
        'item': item
    }
})

If you have multiple items, you may use some other technique to show one of them, like have a data variable visibleItemIndex which can change from 1 to n-1, where n is number of items.

In that case, you will have v-show="visibleItemIndex == currentIndex" in the HTML.

You can as well have hash for saving which index are to de displayed and which to be collapsed.

On point 2:

You can use v-for if you have dynamic arrays. you can see the documentation here.

Upvotes: 2

Ismael Soschinski
Ismael Soschinski

Reputation: 475

This works for me:

<template>
    <div>
        <ul>
            <li v-for="index in list" :key="index._id">

                <button @click="contentVisible === index._id ? contentVisible = false : contentVisible = index._id">{{ index.title }}</button>

                <p v-if='contentVisible === index._id'>{{ index.item }}</p>

            </li>
        </ul>
    </div>
</template>

<script>
    export default {
        name: "sameName",
        data() {
            return {
                contentVisible: false,
                list: [
                    {
                    _id: id1,
                    title: title1,
                    item: item1
                    },
                    {
                    _id: id2,
                    title: title2,
                    item: item2
                    }
                ]
            };
        },
    };
</script>

Upvotes: 3

Alex Sakharov
Alex Sakharov

Reputation: 566

I'm having a real hard time understanding what exactly it is you want or why you would want it, but I think this does it?

Vue.component('accordion-item', {
  template: '#accordion-item',
  methods: {
    toggle() {
      if(this.contentVisible){
      	return
      }
      if(this.$parent.activeTab.length >= 2){
      	this.$parent.activeTab.shift()
      }
      this.$parent.activeTab.push(this)
    }
  },
  computed: {
    contentVisible() {
      return this.$parent.activeTab.some(c => c === this)
    }
  }
})

const Accordion = Vue.extend({
  data() {
    return {
      activeTab: []
    }
  },
  methods: {
    handleToggle($event) {
      this.activeTab = []
    }
  }
})

document.querySelectorAll('.accordion').forEach(el => new Accordion().$mount(el))
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>

<template id="accordion-item">
  <div class="accordion-item" :class="{ 'is-active':  contentVisible}">
      <a href="#" @click="toggle" class="accordion-title"><slot name="title"></slot></a>
      <div v-show="contentVisible" class="accordion-content" @click="$emit('toggle', $event)">
        <slot name="content"></slot>
      </div>
  </div>
</template>

  <div class="accordion">
    <accordion-item @toggle="handleToggle">
      <p slot="title">a title</p>
      <p slot="content">there are words here</p>
    </accordion-item>
    <accordion-item @toggle="handleToggle">
      <p slot="title">titles are for clicking</p>
      <p slot="content">you can also click on the words</p>
    </accordion-item>
    <accordion-item @toggle="handleToggle">
      <p slot="title">and another</p>
      <p slot="content">only two open at a time!</p>
    </accordion-item>
    <accordion-item @toggle="handleToggle">
      <p slot="title">and #4</p>
      <p slot="content">amazing</p>
    </accordion-item>
  </div>

Upvotes: -1

Related Questions