0

I have a v-for list of items that are based on list stored in Vuex. I want it so that the list automatically scrolls to the bottom every time a new item is added. I figured the scrolling part out using this: Scroll to bottom of div?

The pseudocode is the following:

addItem(){
    dispatch vuex event to add item
    scroll to bottom of list
}

The issue I'm running into is that Vue seems to be updating the v-for list after the list is scrolled to the bottom, meaning that the list is always scrolled to the second to last item, instead of the last.

I fixed it by using a setTimout() to wait 10ms, but it feels a little hacky. Is there a better way to trigger the scrollToBottom() code on v-for list update?

zachThePerson
  • 632
  • 5
  • 25
  • 2
    `this.$nextTick(() => { /* scroll to bottom */ });` is the normal way to do it in Vue ([docs](https://vuejs.org/v2/api/#Vue-nextTick)) – blex Sep 12 '20 at 19:27
  • That's the answer! Thanks a ton! – zachThePerson Sep 12 '20 at 19:30
  • You have to separate the concerns. The update function simply updates the collection in the store. And inside a watch on the store getter (or mapState, whatever you have), you scroll the list to bottom. Also note that if you used commit instead of dispatch it would work, because dispatch is async while commit is sync. Another option would be to: `dispatch().then(() => scroll )`. -- dispatch returns a promise. – tao Sep 12 '20 at 19:31
  • @tao, waiting for the dispatch to change the state is not enough. You need to wait for the next render. Example using `then` not working: https://jsfiddle.net/gLnqhvoe/1/ | Example using `$nextTick` working: https://jsfiddle.net/gLnqhvoe/ – blex Sep 12 '20 at 20:24
  • @blex can you submit your `$nexTick()` answer as a response so I can mark this as Solved? – zachThePerson Sep 12 '20 at 22:43

2 Answers2

1

The normal way to wait for the next render is to use this.$nextTick(() => /* ... */). Here is a demo:

new Vue({
  el: '#app',
  'store': new Vuex.Store({
    state: {
      items: ["a", "b", "c", "d"],
      index: 100
    },
    mutations: {
      itemAdded(state) {
        state.items.push(String.fromCharCode(++state.index));
      }
    },
    actions: {
      addItem({ commit }) {
        commit('itemAdded');
      }
    }
  }),
  computed: {
    ...Vuex.mapState(['items'])
  },
  mounted() { this.scrollToBottom(); },
  methods: {
    addItem() {
      this.$store.dispatch('addItem');
      this.$nextTick(() => this.scrollToBottom()); // <-------------------------
    },
    scrollToBottom() {
      this.$refs.list.scrollTop = this.$refs.list.scrollHeight;
    }
  }
});
ul { border: 1px solid black; height: 4em; overflow-y: scroll; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js"></script>

<div id="app">
  <div>
    <button @click="addItem">Add item</button>
    <ul ref="list">
      <li v-for="(label, i) in items" :key="i">{{label}}</li>
    </ul>
  </div>
</div>
tony19
  • 125,647
  • 18
  • 229
  • 307
blex
  • 24,941
  • 5
  • 39
  • 72
0

you need async / await dispatch;

async addItem(){
   await dispatch()
   scroll to bottom of list
}
Adem yalçın
  • 176
  • 1
  • 6
  • No, waiting for the dispatch to change the state is not enough. You need to wait for the next render. Example using `async` not working: https://jsfiddle.net/gLnqhvoe/3/ | Example using `$nextTick` working: https://jsfiddle.net/gLnqhvoe/ – blex Sep 12 '20 at 20:23
  • @blex exactly why I think using `$nextTick()` is still the best answer. It's not posted as an actual response though, so I can't change this to solved – zachThePerson Sep 12 '20 at 22:42
  • sorry, I wrong understood your question. you need catch after dom updated you can do nextTick or updated method. and Best solution as you said $nextTick() // Life cycle method 'updated' https://vuejs.org/v2/api/#updated //$nextTick method https://vuejs.org/v2/api/#vm-nextTick – Adem yalçın Sep 13 '20 at 00:03