0

Solved -- solution at bottom of post

I'm experiencing a problem in Vue2 where I'm trying to control a v-if & v-else based on an @click event inside a v-for loop, but the v-if is not changing dynamically each click like I would expect.

Here is roughly the code that is failing (note: people comes from a store file and is fully loaded when used):

<div v-for="(person, index) in people" :key="index">
  <div @click="detailsOpened(index)">
    <PersonIcon v-if="showDetails(index)" />
    <PersonIcon2 v-else />
  </div>
</div>

import {
  each
} from 'lodash-es'

data(): {
  return {
    personMap: []
  }
}

beforeMount() {
  this.personMap = each(people, () => false)
}

methods: {
  showDetails(index: number): boolean {
    return this.personMap[index] === true
  }

  detailsOpened(index: number): boolean {
    return this.personMap[index] = !this.personMap[index]
  }
}

I've done console.log print outs and detailsOpened seems to work correctly. The boolean value keyed at each relevant index changes when the icon is clicked so that isn't the problem. It seems the v-if only runs when the page first renders and it doesn't update dynamically when the personMap values change. console.log's confirm that the showDetails method only runs at the beginning of the page loading, based on the size of the people array. I'm not sure what is causing this and would appreciate any help in the right direction!

Edit: Solution I had tried the top answer's advice but get hit with the console error, "Error: [vuex] do not mutate vuex store state outside mutation handlers.". I moved these methods and data to a Pinia store file and explictly called "Vue.set"/"this.$set" in the store's setter and still got the the same error. What ultimately solved my problem was 1) Moving data and methods to a Pinia store file. 2) Using using Vue.set on the detailsOpened method inside the store file. 3) Using lodash's cloneDeep on people when this.personMap was initialized.

Julison
  • 11
  • 5

3 Answers3

0

As you already said: "showDetails method only runs at the beginning of the page loading". Vue doesn't know when to call the showDetails method again. A method must be called and executed explicitly in order to run.

In your case, you can use a computed property to achieve what you are trying to do.

Nico
  • 423
  • 1
  • 8
  • A computed property does not work here bc it requires the function to use a parameter. I get an error in my IDE when trying to use a computed property that has a parameter like 'index', "TS2769: No overload matches this call.   The last overload gave the following error.     Type '(index: number) => boolean' is not assignable to type 'ComputedOptions | (() => any)'.       Type '(index: number) => boolean' is not assignable to type '() => any'." – Julison Feb 22 '23 at 14:14
0

Call it in computed instead of method

<template>
  <div v-for="(person, index) in people" :key="index">
    <div @click="detailsOpened(index)">
      <PersonIcon v-if="personDetailsOpen(index)" />
      <PersonIcon2 v-else />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      personMap: [],
      people: [...] // initialize with your data
    };
  },
  methods: {
    detailsOpened(index) {
      this.$set(this.personMap, index, !this.personMap[index]);
    }
  },
  computed: {
    personDetailsOpen() {
      return (index) => this.personMap[index] === true;
    }
  }
};
</script>
Sonny49
  • 425
  • 3
  • 18
  • The problem with that is I get an error in my IDE when trying to use a computed property that has a parameter like 'index', "TS2769: No overload matches this call.   The last overload gave the following error.     Type '(index: number) => boolean' is not assignable to type 'ComputedOptions | (() => any)'.       Type '(index: number) => boolean' is not assignable to type '() => any'." – Julison Feb 22 '23 at 14:13
0

Vue does not react when you directly update the item at its index. You need to use the set operator to make it reactive. Please look at this answer too for more understanding.

On another hand, here is a working demo of your use-case where I used a set to update the personMap array's item.

new Vue({
  el: "#app",
  data() {
    return {
      people: [1, 2, 3],
      personMap: []
    }
  },
  beforeMount() {
    this.personMap = this.people.map(item => item = false);
  },
  methods: {
    showDetails(index) {
      return this.personMap[index] === true
    },
    detailsOpened(index) {
      this.$set(this.personMap, index, !this.personMap[index])
    },
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <div v-for="(person, index) in people" :key="index">
    <div @click="detailsOpened(index)">
      <div v-if="showDetails(index)">Clicked</div>
      <div v-else>Click me</div>
    </div>
  </div>
</div>
Neha Soni
  • 3,935
  • 2
  • 10
  • 32
  • I had tried this previously but get hit with the console error, "Error: [vuex] do not mutate vuex store state outside mutation handlers.". I moved these methods and data to a Pinia store file and explictly called "Vue.set"/"this.$set" in the store's setter and I'm getting the same error – Julison Feb 22 '23 at 14:48