Vue.js

Building A Real-Time Chat Application With Vue.js and Firebase – Part 2


Subscribe On YouTube

DemoCode

Part 1: Setting Up The Project
Part 2: Implementing The Chat Logic
Part 3: Firebase Cloud Functions

In the first part of the Building A Real-Time Chat Application With Vue.js and Firebase tutorial series we’ve set up the Vue.js project, installed needed dependencies like the Bootstrap framework, established the connection to Firebase and started with the implementation.

In this second part we’re going to further complete the implementation of the Vue application and we’re going to implement the Chat and CreateMessage component.


If you like CodingTheSmartWay, then consider supporting us via Patreon. With your help we’re able to release developer tutorial more often. Thanks a lot!


Implementing Chat Component

Implementing The Template

Let’s create a new file src/views/Chat.vue and start inserting the following template code first:

<template>
    <div class="chat container">
        <h2 class="text-primary text-center">Real-Time Chat</h2>
        <h5 class="text-secondary text-center">Powered by Vue.js & Firebase</h5>
        <div class="card">
            <div class="card-body">
                <p class="nomessages text-secondary" v-if="messages.length == 0">
                    [No messages yet!]
                </p>
                <div class="messages" v-chat-scroll="{always: false, smooth: true}">
                    <div v-for="message in messages" :key="message.id">
                        <span class="text-info">[{{ message.name }}]: </span>
                        <span>{{message.message}}</span>
                        <span class="text-secondary time">{{message.timestamp}}</span>
                    </div>
                </div>
            </div>
            <div class="card-action">
                <CreateMessage :name="name"/>
            </div>
        </div>
    </div>
</template>

This codes makes use of Bootstrap’s CSS classes. The output is depending on whether the messages array is filled with messages or not.

If no messages are available the text [No messages yet!] is presented to the user. If messages are available the chat messages are printed out by iterating over the messages array using the v-for directive. For each message the user name, the message text and the message timestamp are printed out.

To ensure that the user can always see the latest message we’re using the v-chat-scroll plugin for the div chat div container. To make this plugin available add the package by executing the following command within the project directory:

$ npm install vue-chat-scroll

and make sure that the plugin is activated for our application by adding the following two lines of code in main.js:

import VueChatScroll from 'vue-chat-scroll'

Vue.use(VueChatScroll)

Adding The Component’s JavaScript Code

To further complete the implementation of Chat.vue add the JavaScript code embedded in a <script>-element next:

<script>
    import CreateMessage from '@/components/CreateMessage';
    import fb from '@/firebase/init';
    import moment from 'moment';

    export default {
        name: 'Chat',
        props: ['name'],
        components: {
            CreateMessage
        },
        data() {
            return{
                messages: []
            }
        },
        created() {
            let ref = fb.collection('messages').orderBy('timestamp');

            ref.onSnapshot(snapshot => {
                snapshot.docChanges().forEach(change => {
                    if (change.type == 'added') {
                        let doc = change.doc;
                        this.messages.push({
                            id: doc.id,
                            name: doc.data().name,
                            message: doc.data().message,
                            timestamp: moment(doc.data().timestamp).format('LTS')
                        });
                    }
                });
            });
        }
    }
</script>

A few things to note here: First of all three import statement have been added.

  • The CreateMessage component (which is not implemented yet) is already imported because Chat component is using CreateMessage as a child component.
  • The messages array is declared as a data property of the component.
  • The created() method is a lifecycle hook and called after a component is created but before the component output is added to the DOM. In our case the created hook is used to access the Firestore messages collection, read out the data and make sure that all available messages are inserted into the component’s messages property, so that it can be accessed in the template code.

To access Firestore we’re making sure that we’re adding the following import statement on top:

import fb from '@/firebase/init';

As we’re exporting the Firestore reference in init.js we’re able to access Firestore via fb in Login component. A reference to the collection is created inside the created method by using:

let ref = fb.collection('messages').orderBy('timestamp');

To retrieve the data of that collection we need to use the onSnapshot method. By using that method you can listen to a document with the onSnapshot() method. An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot.

Inside the callback function we’re checking for updated documents inside the messages collection by using snapshot.docChanges(). Iterating through the list of changed documents is done by using method forEach. Here we need to pass in another callback function which is invoked for every document inside that list of changed document.

Inside that second callback function we’re checking if the value of change.type is added. This is needed because the following logic should only be executed for new messages being added to the collection in Firestore. If this is the case we’re added this new message document to the component’s messages array, so that it is displayed to the user instantly.

Adding Styles

Finally, let’s add the styles section to Chat.vue:

<style>
.chat h2{
    font-size: 2.6em;
    margin-bottom: 0px;
}

.chat h5{
    margin-top: 0px;
    margin-bottom: 40px;
}

.chat span{
    font-size: 1.2em;
}

.chat .time{
    display: block;
    font-size: 0.7em;
}

.messages{
    max-height: 300px;
    overflow: auto;
}
</style>

Implementing CreateMessage Component

In the template code of Chat component we’ve already included the CreateMessage component by using the following line of code:

<CreateMessage :name="name"/>

Herewith we’re making sure that the value of the Chat component’s name property (which is received from Login Component) is passed to CreateMessage component (again as name property) and that the output of CreateMessage component is embedded in the output of Chat component.

To make that code work we need to add the implementation of CreateMessage component to the Vue.js project next.

Implementing The Template

Let’s add a new file src/components/CreateMessage.vue to the project and insert the following template code first:

<template>
    <div class="container" style="margin-bottom: 30px">
        <form @submit.prevent="createMessage">
            <div class="form-group">
                <input type="text" name="message" class="form-control" placeholder="Enter message ..." v-model="newMessage">
                <p class="text-danger" v-if="errorText">{{ errorText }}</p>
            </div>

            <button class="btn btn-primary" type="submit" name="action">Submit</button>
        </form>
    </div>
</template>

This template is containing the HTML code which is needed to output a simple form consisting of one input field. This field is of type text and bound to the newMessage property of the component. The submit event of this form is bound to the createMessage event handler method which we’re going to implement in the next step.

Let’s now add the following JavaScript code (embedded in a <script>-tag) in CreateMessage.vue:

<script>
    import fb from '@/firebase/init';

    export default {
        name: 'CreateMessage',
        props: ['name'],
        data(){
            return {
                newMessage: null,
                errorText: null
            }
        },
        methods: {
            createMessage () {
                if (this.newMessage) {
                    fb.collection('messages').add({
                        message: this.newMessage,
                        name: this.name,
                        timestamp: Date.now()
                    }).catch(err => {
                        console.log(err);
                    });
                    this.newMessage = null;
                    this.errorText = null;
                } else {
                    this.errorText = "A message must be entered!"
                }
            }
        }
    }
</script>

First of all we’re making sure that we have access to the Firestore reference by including the following import statement on top:

import fb from '@/firebase/init';

Two data properties are defined (newMessage and errorText) by using the data function. Furthermore adding the name property to the array which is assigned to the component’s props property. This is needed to be able to receive name as an input property.

Inside the methods object the createMessage method is implemented. Inside this method we’re checking first if a message has been entered (if this.newMessage has a value). If this is the case the new message object is inserted into the Firestore messages collection by calling fb.collection(‘messages’).add.

If no message value is available we’re setting errorText to the value “A message must be entered!”, so that this error message is displayed to the user.

Completing The Router Configuration

Finally we need to complete the router configuration in src/router.js:

import Vue from 'vue'
import Router from 'vue-router'
import Login from './views/Login.vue'
import Chat from '@/views/Chat.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'Login',
      component: Login
    },
    {
      path: '/chat',
      name: 'Chat',
      component: Chat,
      props: true,
      beforeEnter: (to, from, next) => {
        if (to.params.name) {
          next();
        } else {
          next({name: 'Login'});
        }
      }
    }
  ]
})

The configuration for the /chat path is added and connected to the newly created Chat component. In this route configuration the props property is set to true. This is needed because we want to pass the name as a router property.

Furthermore we’re attaching a route guard function to the beforeEnter property. This function is executed before the route is accessed. The function is used to first check if the user has already performed the login process (a login name is available via to.params.name. If this is the case the next() function is called to conclude the routing process to /chat. In any other case the next method is used to route back to Login component.

What’s Next

In this second part of the Building A Real-Time Chat Application With Vue.js and Firebase tutorial series we’ve further completed the implementation of the Vue.js front-end application.

In the next part we’ll be implementing the Firebase Cloud Functions which is automatically cleans up the Firestore database if more then ten messages have been inserted.

ONLINE COURSE: Vue.js 2 - The Complete Guide

Check out the great Vue.js online course by Maximilian Schwarzmüller with thousands of students already enrolled:

Vue JS 2 – The Complete Guide 

  • Build amazing Vue.js Applications – all the Way from Small and Simple Ones up to Large Enterprise-level Ones
  • Leverage Vue.js in both Multi- and Single-Page-Applications (MPAs and SPAs)
  • Understand the Theory behind Vue.js and use it in Real Projects

Go To Course


Using and writing about best practices and latest technologies in web design & development is my passion.