I'm trying to make a chat window, where when I send/get a message, the window scrolls to the very bottom. That's how I make it:
template:
<ul class="chat-window">
<li v-for="message in messages">
<span>{{ message.from }}</span>{{ message.message }}
</li>
</ul>
script:
const messages = ref()
socket.on('chat-message', (data) => {
messages.value.push(data)
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
})
But when coded this way, the last message is never seen (you need to scroll to it).
I found out that when I use setTimeout, like this:
setTimeout(() => {
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.offsetHeight
}, 10)
then it works fine. So I know how to make it work, but I don't know why I need to use setTimeout. Can anybody please explain? Is there a better way to do it?
The reason why it works with setTimeout
is the lifecycle of a vue component. You should read the documentation page to understand the how the lifecycle works.
So if you set a variable like you do
messages.value.push(data)
Vue needs to run a new lifecycle to update the component. If you access the DOM directly after the change of the value, Vue might not have been updated the component yet.
But, you can actively say, you want to wait for the update to be done with the nextTick
function: https://vuejs.org/api/general.html#nexttick
// import nextTick from vue
import { nextTick } from 'vue';
// callback needs to be async
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
})
// depending on the weight of your overall page,
// sometimes you also need to request the animation frame first
// this is better then using setTimeout. But you should try without first.
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
requestAnimationFrame(() => {
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
});
})
Instead of using querySelector you should use a template ref for the html element as described here: https://vuejs.org/guide/essentials/template-refs.html
<ul ref="chatWindow" class="chat-window">
<li v-for="message in messages">
<span>{{ message.from }}</span>{{ message.message }}
</li>
</ul>
// setup script
const messages = ref([]) // don’t forget the initial empty array here
const chatWindow = ref(null) // template refs are always initialized with null
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
// you need to make sure, the chatWindow is not null
if (!chatWindow.value) return;
chatWindow.value.scrollTop = chatWindow.value.scrollHeight
})