How to update DOM when changes occurs within an object rendered by v-for?
Asked Answered
F

2

6

I am using v-for to render a list based on an array of objects.

<ul>
  <li v-for="category, i in categories"
  :class="{active: category.active}"
  @click="changeCategory(i)">
    <a>{{ category.service_type_name }}</a>
  </li>
</ul>

When you click the list item changeCategory(index) runs:

changeCategory(index) {
    this.categories.forEach((c, i) => {
        c.active = (i != index)? false : true
    })
}

So the clicked-on list items active attribute gets set to true and all others set to false, and the list item gets an .active class added to it (because of the :class="{active: category.active}" line in the template.

However, the DOM is not updated to show this change.

Is there anyway to update the DOM automatically when this change occurs, without using this.$forceUpdate() in the changeCategory(index) function?

edit: changed map to forEach, DOM still does not update.

edit: I am using vue-class-components with typescript

import Vue from 'app/vueExt'
import { Component } from 'vue-property-decorator'
import * as Template from './services.vue'

@Component({
    mixins: [Template]
})
export default class Services extends Vue {
    categories = []

    created() {
        this.api.getServiceSetupCatagories().then((res) => {
            this.categories = res.categories
            this.categories.forEach((c, i) => {
                // adding active property to the object
                // active = true if i = 0, false otherwise
                c.active = (i)? false : true
            })
        })
    }

    changeCategory(index) {
        this.categories.forEach((c, i) => {
            c.active = (i != index)? false : true
        })
    }
}
Flourish answered 1/5, 2017 at 15:34 Comment(9)
map doesn't modify your array, it returns a new array. Therefore vue doesn't detect any change and your dom isn't updated. Try using forEach instead.Tuscan
@EricGuan that's true, good point. I tried forEach and event went to the extent of using a for loop to change active property, but it had no effect. When I console.log the item it shows the correct active value, but the DOM doesn't get updated.Flourish
Seems to work for me. codepen.io/Kradek/pen/MmmZLNKabyle
@BertEvans Yeah, I am not sure what's going on. I tried your code with map function instead of forEach and that works too as intended. I am not sure why the DOM isn't updating in my case.Flourish
Technically, yes, map will work in this case too, you're just kind of mis-using it. It's intended with map that you return a new array, not just iterate the array. Does every category have an active property beforehand?Kabyle
If the active property does not exist on category until the changeCategory method is called, then you are falling into a caveat in Vue and should use Vue.set(c, "active", i != index ? false : true). vuejs.org/v2/guide/reactivity.html#Change-Detection-CaveatsKabyle
@BertEvans It didn't before. But now I am adding the active property to the object before rendering (in created() hook). Still no effect.Flourish
As @RoyJ mentions, we need to see more code to help then.Kabyle
Let us continue this discussion in chat.Flourish
K
2

After discussing the issue I suggested the following alternative method.

Add activeIndex to data and change the template to the following.

<ul>
  <li v-for="category, i in categories"
      :class="{active: activeIndex === i}"
      @click="activeIndex = i">
    <a>{{ category.service_type_name }}</a>
  </li>
</ul>

This appears to work for @wrahim.

I believe the underlying issue with the original code was that the active property was added to the category falling into one of Vue's change detection caveats.

Kabyle answered 1/5, 2017 at 18:15 Comment(0)
U
1

This works as expected. Your problem is not in the code you posted here. (Even using map: within map, objects are references, so modifying their members modifies the original object.)

new Vue({
  el: '#app',
  data: {
    categories: [{
        service_type_name: 'one',
        active: false
      },
      {
        service_type_name: 'two',
        active: false
      }
    ]
  },
  methods: {
    changeCategory(index) {
      this.categories.map((c, i) => {
        c.active = (i != index) ? false : true
      })
    }
  }
});
.active {
  background-color: lightgray;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.min.js"></script>
<ul id="app">
  <li v-for="category, i in categories" :class="{active: category.active}" @click="changeCategory(i)">
    <a>{{ category.service_type_name }}</a>
  </li>
</ul>
Ununa answered 1/5, 2017 at 16:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.