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>
Advertisement
Answer
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.