Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

BernardoBF4's avatar

Making data reactive with Vue

I have two components TheCollapsible and TheBuyProduct. The first should be a structural component, making it possible to pass some children and a slot and render multiple collapsible itens (parents), that can expand and show its children. The second (which uses the first), should be a reactive screen to make it possible for the user to add items of some product. The problem here is the quantity of the product is not updated when the add or subtract functions are called.

Notice I have made two trials on reactivity: the first was to add a new property to the prop passed for my second component, this property is my quantity, but it doesn't work; the second trial was to create an array, where the index is the product id and the value is the quantity (which starts at zero). Both fail to be reactive.

I wander if this is because of the way my slot works. In my TheCollapsible component, I need a list of objects that has a property which is a new list (parent has property containing children). But as I said this is merely structural, each children HTML is passed as a slot and every prop I need in the slot I get through the v-slot.

TheCollapsible.vue

<template>
  <div class="ui-collapsible">
    <div
      class="ui-collapsible__parent"
      :key="parent_index"
      v-for="(parent, parent_index) in parents"
    >
      <div>
        <p>{{ parent.title }}</p>
        <button @click="openChildren(parent_index)">
          <img
            alt="Abrir"
            :id="`collapsible-icon-${parent_index}`"
            src="/img/icons/chevrons-up-down.svg"
          />
        </button>
      </div>
      <ul
        class="ui-collapsible__children"
        :id="`collapsible-child-${parent_index}`"
      >
        <li :key="index" v-for="(child, index) in parent[child_key]">
          <slot
            :child="child"
            :child_index="index"
            :parent_index="parent_index"
          />
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  props: ["child_key", "parents"],
  methods: {
    openChildren(index) {
      this.changeIconForOpenOrClosed(index);
      const open_child = document.getElementsByClassName(
        "ui-collapsible__children--active"
      )[0];
      if (open_child) {
        open_child.classList?.remove("ui-collapsible__children--active");
        open_child.classList.add("ui-collapsible__children");
      }
      const child = document.getElementById(`collapsible-child-${index}`);
      if (open_child?.id != child.id) {
        child.classList.remove("ui-collapsible__children");
        child.classList.add("ui-collapsible__children--active");
      }
    },
    changeIconForOpenOrClosed(index) {
      const opened_button = document.querySelector(
        '[src$="chevrons-down-up.svg"]'
      );
      if (opened_button?.src) {
        opened_button.src = "/img/icons/chevrons-up-down.svg";
      }

      const click_button = document.querySelector(`#collapsible-icon-${index}`);
      if (click_button?.id != opened_button?.id) {
        click_button.src = "/img/icons/chevrons-down-up.svg";
      }
    },
  },
};
</script>

TheBuyProduct.vue

<template>
  <div class="row">
    <div class="col-6">
      <img :alt="product.title" class="product__image" :src="product.image" />
      <h1>{{ product.title }}</h1>
      <div class="product__lead" v-html="product.lead"></div>
    </div>
    <div class="col-6">
      <h1>Acompanhamentos</h1>
      <the-collapsible
        child_key="items"
        :parents="product.accompaniment_categories"
        v-slot="{ child, child_index, parent_index }"
      >
        <div class="row product__accompaniment">
          <img :alt="child.title" class="col-2" :src="child.image" />
          <span class="col-4">{{ child.title }}</span>
          <div class="col-4 product__quantity">
            <button @click="subtract(child_index, parent_index, child.id)">
              <i class="fa fa-minus"></i>
            </button>
            <span>
              {{
                product.accompaniment_categories[parent_index].items[
                  child_index
                ].quantity
              }}
              -
              {{ accompaniments[child.id] }}
            </span>
            <button @click="add(child_index, parent_index, child.id)">
              <i class="fa fa-plus"></i>
            </button>
          </div>
          <span class="col-2">R${{ child.price }}</span>
        </div>
      </the-collapsible>
      <button class="ui-button product__button">Enviar pedido</button>
    </div>
  </div>
</template>

<script>
export default {
  props: ["product"],
  data() {
    return {
      accompaniments: [],
    };
  },
  created() {
    this.addPropToAccompaniments();
  },
  methods: {
    addPropToAccompaniments() {
      for (let i = 0; i < this.product.accompaniment_categories.length; i++) {
        const category_items = this.product.accompaniment_categories[i].items;
        for (let j = 0; j < category_items.length; j++) {
          category_items[j].quantity = 0;
          this.accompaniments[category_items[j].id] = 0;
        }
      }
    },
    add(i, j, id) {
      this.product.accompaniment_categories[i].items[j].quantity += 1;
      console.log("add:", this.accompaniments[id]);
      this.accompaniments[id] += 1;
    },
    subtract(i, j, id) {
      this.product.accompaniment_categories[i].items[j].quantity -= 1;
      console.log("sub:", this.accompaniments[id]);
      this.accompaniments[id] -= 1;
    },
  },
};
</script>

0 likes
1 reply
azimidev's avatar

It might be because you're mutating the quantity directly in the subtract and add methods, instead of using Vue.set or the Vue.set-like method provided by the Vue 3 Composition API (reactive). When you directly mutate the quantity, it's not triggering a re-render of the component, which is why the changes aren't being reflected in the UI.

you can wrap the quantity property in a reactive object using reactive from the Vue 3 Composition API. Then, use the Vue.set-like method provided by reactive to update the quantity, so that it triggers a re-render.

<template>
  <div class="row">
    <div class="col-6">
      <img :alt="product.title" class="product__image" :src="product.image" />
      <h1>{{ product.title }}</h1>
      <div class="product__lead" v-html="product.lead"></div>
    </div>
    <div class="col-6">
      <h1>Acompanhamentos</h1>
      <the-collapsible
        child_key="items"
        :parents="product.accompaniment_categories"
        v-slot="{ child, child_index, parent_index }"
      >
        <div class="row product__accompaniment">
          <img :alt="child.title" class="col-2" :src="child.image" />
          <span class="col-4">{{ child.title }}</span>
          <div class="col-4 product__quantity">
            <button @click="subtract(child_index, parent_index, child.id)">
              <i class="fas fa-minus"></i>
            </button>
            <span>{{ quantity[child.id] }}</span>
            <button @click="add(child_index, parent_index, child.id)">
              <i class="fas fa-plus"></i>
            </button>
          </div>
        </div>
      </the-collapsible>
    </div>
  </div>
</template>

<script>
import { reactive } from '@vue/composition-api';

export default {
  props: ['product'],
  setup() {
    const quantity = reactive({});

    function add(child_index, parent_index, id) {
      quantity[id] = (quantity[id] || 0) + 1;
    }

    function subtract(child_index, parent_index, id) {
      if (quantity[id] > 0) {
        quantity[id] -= 1;
      }
    }

    return {
      quantity,
      add,
      subtract,
    };
  },
};
</script>
1 like

Please or to participate in this conversation.