javascriptvue.jsvue-componentvuejs3vue-events

Vue / Nuxt 3 Collapse component function from parent


I've created a reusable Collapse component using vue 3. It works as expected. The problem I have is I need to control 2 instances of the collapse from the parent / page. I was able to have the parent toggle a specific collapse using props but if I toggle from the parent and then toggle the collapse on itself the parent is now taking two clicks to then toggle again. I'm assuming the data is actually not updating? Not sure if there is a better way to do this without getting into a state management solution.

Collapse.vue

<script setup>
  const props = defineProps({
    title: {
      type: String,
      required: true,
    },
    collapsed: {
      type: Boolean,
      default: false,
    },
  });

  const isOpen = ref(props.collapsed);

  watch(
    () => props.collapsed,
    (newValue) => {
      isOpen.value = newValue;
    }
  );

  const toggleCollapse = () => {
    isOpen.value = !isOpen.value;
  };
</script>

<template>
  <div class="flex flex-col ml-[20px] mb-2">
    <button
      @click="toggleCollapse"
      class="text-left flex items-center relative font-source text-[19px] sm:text-[16px] lg:text-[19px] mb-2 font-semibold"
    >
      <IconCSS
        :class="{ 'rotate-90': isOpen }"
        class="text-3xl text-green absolute -left-7"
        name="mdi:chevron-right"
      />

      {{ title }}
    </button>
    <div v-if="isOpen">
      <slot name="collapseContent"></slot>
    </div>
  </div>
</template>

Parent

<script setup>
  const collapsePanels = ref([
    { id: 0, isVisible: false },
    { id: 1, isVisible: false },
    { id: 2, isVisible: false }
  ]);

  const openCollapse = (id) => {
    collapsePanels.value.forEach((el, index) => {
      if (id === index) {
        el.isVisible = !el.isVisible;
      }
    });
  };
</script>

<template>
  <button @click="openCollapse(0)">Toggle Panel One</button>
  <button @click="openCollapse(2)">Toggle Panel Three</button>

  <Collapse :collapsed="collapsePanels[0].isVisible" title="Collapse One">
    <template v-slot:collapseContent>
      <p>Collpase One Content</p>
    </template>
  </Collapse>

  <Collapse title="Collapse Two">
    <template v-slot:collapseContent>
      <p>Collpase Two Content</p>
    </template>
  </Collapse>

  <Collapse :collapsed="collapsePanels[2].isVisible" title="Collapse Three">
    <template v-slot:collapseContent>
      <p>Collpase Three Content</p>
    </template>
  </Collapse>

Solution

  • You should store and control the state in one place and use events to communicate between components.

    Add id prop

     const props = defineProps({
        id: {
          type: Number,
          required: true,
        },
        ...
    

    And an toggle event in the Collapse.vue

     const emit = defineEmits(['toggle'])
      const toggleCollapse = () => {
        emit('toggle', props.id)
      };
    

    Then use the event in the Parent App with @toggle="toggleCollapse"

     <Collapse :id="0" @toggle="toggleCollapse"...
    

    and in <setup>

     const toggleCollapse = (id) => {
        openCollapse(id)
      }
    

    Here is the working SFC Playground