Skip to content
Advertisement

How to scroll to bottom of div when new elements are added

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.

User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement