I am trying to make a chat application using Vue and Express.
At the moment, I want to make the container with the messages automatically scroll to the bottom when a new message is sent. I tried to do this by using a scrollToEnd
function that selects the div container and assigns its scrollHeight
to the scrollTop
:
scrollToEnd: function () {
var messages = this.$el.querySelector('#messages')
messages.scrollTop = messages.scrollHeight
}
This gives the following error:
TypeError: Cannot read property 'scrollHeight' of null
For some reason, using the querySelector
always returns null, also when I am testing it on other elements.
Below the full code for the component can be found.
<template>
<div id="messages">
<ul>
<li v-for="msg in messages.slice().reverse()">{{ msg.message }}</li>
</ul>
</div>
</template>
<script>
import MessageService from '@/services/MessageService'
export default {
name: 'messages',
data () {
return {
messages: []
}
},
mounted () {
this.getMessages()
this.$root.$on('newMessage', (msg) => {
this.message = msg
this.getMessages()
this.scrollToEnd()
})
},
methods: {
async getMessages () {
const response = await MessageService.fetchMessages()
this.messages = response.data.messages
},
scrollToEnd: function () {
var messages = this.$el.querySelector('#messages')
messages.scrollTop = messages.scrollHeight
}
}
}
</script>
this.$el
The root DOM element that the Vue instance is managing.
this.$el
is the #messages
div, there's no need to fetch it from the DOM.
Then, you could use this.$el.lastElementChild.offsetTop
to get the last message and scroll to its top, so if it's long, you're not scrolling past its starting point.
Here, I simplified the template a little to make it straight to the point.
<template>
<ul id="messages">
<li v-for="msg in messages.slice().reverse()">{{ msg.message }}</li>
</ul>
</template>
<script>
export default {
name: 'messages',
data() {
return { messages: [] };
},
mounted() {
this.getMessages();
},
updated() {
// whenever data changes and the component re-renders, this is called.
this.$nextTick(() => this.scrollToEnd());
},
methods: {
async getMessages () {
// ...snip...
},
scrollToEnd: function () {
// scroll to the start of the last message
this.$el.scrollTop = this.$el.lastElementChild.offsetTop;
}
}
}
</script>
If you really want to keep the <div>
container, you could use a ref
.
<template>
<div id="messages">
<ul ref="list">
<li v-for="msg in messages.slice().reverse()">{{ msg.message }}</li>
</ul>
</div>
</template>
Then in the component, you can refer to it with this.$refs.list
.
ref
is used to register a reference to an element or a child component. The reference will be registered under the parent component’s$refs
object. If used on a plain DOM element, the reference will be that element; if used on a child component, the reference will be component instance.
While Vue examples often use the native DOM API to get around, using ref
in this instance is way easier.