Vue Chat - Frontend

joan41868

Yoan Sredkov

Posted on March 2, 2021

Vue Chat - Frontend

Hello fellow DEVs,

In my previous post, we implemented 80% of our chat backend.
This is the link to the post, in case you missed:
https://dev.to/joan41868/java-chat-app-backend-53k3

Today, we'll continue with implementing basic frontend, as well as enhancing our backend to remember our users. Let's GO!

Prerequisite - Node.js 10+ installed, with NPM/yarn

Install vue-cli with:

 npm i -g @vue/cli # you may need sudo under linux 
Enter fullscreen mode Exit fullscreen mode

OR

yarn global add @vue/cli 
Enter fullscreen mode Exit fullscreen mode

Then, go on and create a new vue project with:

vue create {project_name} 
Enter fullscreen mode Exit fullscreen mode

You will be prompted to select dependencies manually/automatically. It's up to you which dependencies you choose to have. I've chosen:

  • Babel
  • Vue Router
  • Vuex

You should see this as project structure:
Alt Text

Open VSCode in the project folder, and type in the terminal:

npm run serve

This will start your frontend application on port 8080. (Please note that Spring's default port is 8080 too, so your vue app may start at port 8081).

Lets add our dependencies:

npm i axios bootstrap bootstrap-vue 
npm i sockjs-client webstomp-client
Enter fullscreen mode Exit fullscreen mode

We need to define several components - our router and our chat page. We will start with the chat page, as it's more complex in nature.
Before anything else, this is the main.js file. Nothing complex, just vue+bootstrap setup.


import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'

import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'

// Import Bootstrap an BootstrapVue CSS files (order is important)
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

// Make BootstrapVue available throughout your project
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')


Enter fullscreen mode Exit fullscreen mode

We will start off by defining the methods in the chat page, as follows:


methods: {
            goBack(){
                this.$router.push({name: 'Home'});
            },

            addMessage(message) {

                if (this.messages.length > 5) {
                    this.messages.shift();
                }
                this.messages.push(message);
                this.scrollOnNewMessage();
            },

            async sendInitialConnectionRequest() {
                const ax = axios.default;
                const ipifyUrl = 'https://api.ipify.org?format=json';
                const ipData = (await ax.get(ipifyUrl)).data;
                const {
                    ip
                } = ipData;
                const checkUrl = this.beUrl + '/check/' + ip;
                // data should contain IP
                const rsp = await ax.get(checkUrl);

                const {
                    data
                } = rsp;

                if (data.ipAddressHash && data.username) {
                    this.username = data.username;
                } else {
                    this.username = prompt("Enter a username");
                    if (this.username) {
                        const registerUrl = this.beUrl + "/create";
                        const requestData = {
                            ipAddress: ip,
                            username: this.username
                        };
                        await ax.post(registerUrl, requestData);
                    } else {
                        await this.sendInitialConnectionRequest();
                    }
                }

            },

            subscribeToChat() {
                if (this.username && this.username !== '') {
                    this.stompClient.subscribe("/topic/messages/" + this.username, (data) => {
                        const {
                            senderUsername,
                            recipientUsername,
                            content
                        } = JSON.parse(data.body);

                        const message = {
                            senderUsername,
                            recipientUsername,
                            content,
                        };

                        this.addMessage(message);
                    });
                }
            },
            connectToChatServer() {
                this.connectedToServer = false;
                this.socket = new SockJs(this.beUrl + "/chat-app");
                this.stompClient = StompClient.over(this.socket, {
                    debug: false
                });

                this.stompClient.connect({}, (frame) => {
                    this.connectedToServer = true;
                    this.subscribeToChat();
                });
            },

            sendMessage(e) {
                e.preventDefault(); // prevent default form submission
                const message = {
                    senderUsername: this.username,
                    recipientUsername: this.recipientUsername,
                    content: this.messageContent,
                };
                if (message.content.length > 0) {
                    this.stompClient.send("/topic/messages/" + message.recipientUsername, JSON.stringify(message), {});
                    this.addMessage(message); // add message to currently shown messages
                    this.messageContent = "";
                }
            },
            scrollOnNewMessage(){
                const msgContainerHtmlNode = document.getElementById("message-container"); 
                msgContainerHtmlNode.scrollTop = msgContainerHtmlNode.scrollHeight;
            }
        }

Enter fullscreen mode Exit fullscreen mode

And here are the missing variables you probably noticed:


data() {
            return {
                connectedToServer: false,
                messages: [],
                showInfo: Boolean,
                username: String,
                                messageContent: "",
                                recipientUsername: "",
                beUrl: 'http://localhost:8080' //'https://java-chat-backend.herokuapp.com'
            }
        },

Enter fullscreen mode Exit fullscreen mode

This is our chat page markup:


<template>
    <div class="home bg-dark">

        <show-info-button></show-info-button>
        <go-home-button></go-home-button>


        <p style="color: white; font-size: 22px; color: #b491c8;">Your are: {{username}}</p>

        <b-form @submit="sendMessage"
            style="display: flex; flex-direction: column; justify-content: space-between; margin: auto; width: 75%;">
            <b-form-input id="recipient" v-model="recipientUsername" type="text" placeholder="recipient..."
                class="bg-dark" style="margin-top: 5px; margin-bottom: 5px; color: #b491c8;" />

            <div id="message-container" class="chatmessagecontainer" v-show="messages.length > 0">

                <span v-for="message in messages" v-bind:key="message.content + Math.random().toString()">
                    <message :content="message.content" :senderUsername="message.senderUsername"></message>
                </span>

            </div>

            <div class="chat-controller">

                <b-form-input id="messageContent" v-model="messageContent" type="text" placeholder="Your message.."
                    class="bg-dark"  style="margin-top: 5px; margin-bottom: 10px; color: #b491c8;" />

                <span
                    style="display: flex; flex-direction: row; justify-content: space-between; margin-top: 5px; margin: auto;">
                    <b-button type="submit" variant="outline-success" @click="sendMessage">
                        <b-icon icon="chat-fill"></b-icon> Send
                    </b-button>
                    <b-button variant="outline-danger" @click="()=>{messages=[]}">
                        <b-icon icon="trash"></b-icon> Clear
                    </b-button>
                </span>

            </div>


        </b-form>


    </div>
</template>

Enter fullscreen mode Exit fullscreen mode

And the scoped style:


    .chatmessagecontainer {
        display: flex;
        flex-direction: column;
        justify-content: space-evenly;
        max-height: 320px;
        overflow-x: none;
        overflow-y: scroll;
        border: 1px solid black;
        border-radius: 8px;
        margin: auto;
        margin-top: 10px;
        width: 100%;
        padding: 4px 4px 4px 4px;
    }

    .chat-controller {
        margin: auto;
        width: 75%;
        position: absolute;
        left: 0;
        right: 0;
        bottom: 5px;

    }

    button {
        font-size: 15px;
    }

    input {
        font-size: 16px;
        color: #b491c8;
    }

Enter fullscreen mode Exit fullscreen mode

And the router file:


import Vue from 'vue'
import VueRouter from 'vue-router'
import  Chat from '../views/Chat.vue'
Vue.use(VueRouter)

const routes = [
  {
    path: '/chat',
    name: 'Chat',
    component: Chat
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

Enter fullscreen mode Exit fullscreen mode

And finally, the three missing components you definetely noticed:

Message:


<template>
  <div class="chatmessage">
                    <p class="chat-message-username"><b>{{senderUsername}}</b></p>
                    <p class="chat-message-content">{{content}}</p>
  </div>
</template>

<script>
export default {
    props:{
        content: String,
        senderUsername: String,
        createdAt: Date
    }
}
</script>

<style >
.chatmessage{
  background-color: #212121;
  color: white;
  border-radius: 9px;
  max-width: 250px;
  margin: auto;
  font-size: 15px;
  max-height: 60px;
}

.chat-message-content{
  font-size: 14px;
  margin-top: 0;
  padding: 0;

}

.chat-message-username{
  font-size: 15px;
  font-family: monospace;
}

</style>



Enter fullscreen mode Exit fullscreen mode

GoHomeButton


<template>
    <b-icon icon="arrow-left" style="position: absolute; top: 5px; left: 5px; font-size: 33px;" class="h4 text-danger"
            @click="goBack()"></b-icon>
</template>

<script>
export default {
    methods:{
        goBack(){
            this.$router.push("/");
        }
    }
}
</script>

<style>

</style>

Enter fullscreen mode Exit fullscreen mode

ShowInfoButton


<template>
    <div>
        <b-icon icon="question" style="position: absolute; top: 5px; right: 5px; font-size: 33px;"
            class="h4 text-success" v-b-modal.modal-1></b-icon>


        <b-modal id="modal-1" title="Safemessage" body-bg-variant="dark" header-bg-variant="dark"
            footer-bg-variant="dark" body-text-variant="light" header-text-variant="light" :cancel-disabled="true">
            <p>A secure chat aimed at maximum privacy.</p>
            <p>No cookies.</p>
            <p>No trackers.</p>
            <p>No database for messages.</p>
            <router-link to="/donate">Support the author :)</router-link>
        </b-modal>

    </div>

</template>


Enter fullscreen mode Exit fullscreen mode

This is all for this post, as it got pretty long. In the next one, the 3rd part, we will enhance our backend so it can accept the initialConnectionRequest sent from our frontend.

Thank you for dropping by, and have a nice day :)

💖 💪 🙅 🚩
joan41868
Yoan Sredkov

Posted on March 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related