Creating a web chat app with Twilio Conversations and Vue.js (Part 2)

September 08, 2021
Written by
Reviewed by

Creating a web chat app with Twilio Conversations and Vue.js (Part 2)

This tutorial is divided into 2 parts: part 1 covers the project setup and back end development, while part 2 covers the front end development and testing of the app.

Part 1: Creating a web chat app with Twilio Conversations and Vue.js (Part 1) 

 

In part 1, we set up the project and  built the back end of the below chat app. Let’s continue and build the front end.

Expected user interface of the completed app

Building the front end

The front end of the chat app is built using Vue components. The components used in this chat app with a .vue file extension are called single file components. Single file components combine HTML templates, JavaScript logic, and CSS styling into a single file.

Let’s create single file components for the chat app. Open a new window in the terminal.

Delete the HelloWorld.vue component that the Vue CLI created by default, and create files named Chat.vue and Conversation.vue under the src/components directory.

Build the chat component

First, we will build Chat.vue, a component for the entire chat screen. Single file components are divided into three parts: <template>, <script>, and <style>. We’ll first define the HTML for the chat screen in <template>.

Open Chat.vue in a text editor and paste this code.

<template>
  <div id="chat">
    <h1>Welcome to the Vue chat app<span v-if="nameRegistered">, {{ name }}</span>!</h1>
    <p>{{ statusString }}</p>
    <div v-if="!nameRegistered">
      <input @keyup.enter="registerName" v-model="name" placeholder="Enter your name">
      <button @click="registerName">Register name</button>
    </div>
    <div v-if="nameRegistered && !activeConversation && isConnected">
      <button @click="createConversation">Join chat</button>
    </div>
    <Conversation v-if="activeConversation" :active-conversation="activeConversation" :name="name" />
  </div>
</template>

We will then add CSS styles to the HTML using <style>. Add this code after the <template> block:

<style scoped>
ul {
 list-style-type: none;
 padding: 0;
}
 
li {
 display: inline-block;
 margin: 0 10px;
}
 
a {
 color: #42b983;
}
</style>

The scoped attribute in <style scoped> is used to apply CSS only to the elements in the current component.

Next, we’ll add the JavaScript logic using <script>. Add this code between the <template> block and the <style> block:

<script>
import {Client as ConversationsClient} from "@twilio/conversations"
import Conversation from "./Conversation"

export default {
    components: { Conversation },
    data() {
        return {
            statusString: "",
            activeConversation: null,
            name: "",
            nameRegistered: false,
            isConnected: false
        }
    },
}
</script>

This code imports the Client class from @twilio/conversations, which is the starting point for accessing the functionality in Twilio Conversations, and a Conversation component, which is a child component of the Chat component that we are yet to write.

The data method is used to register data in the application as an object, so that Vue can detect and re-render it whenever a change occurs.

We define functions to manipulate these data elements in the methods object. methods is a set of functions that process events that occur in the DOM, such as user actions.

We will create an initConversationsClient method to initialize the Client object. initConversationsClient will be executed when the username is entered. Paste this code inside the export default block and after the data() method:

methods: {
    initConversationsClient: async function() {
        window.conversationsClient = ConversationsClient
        const token = await this.getToken(this.name)
        this.conversationsClient = await ConversationsClient.create(token)
        this.statusString = "Connecting to Twilio…"
        this.conversationsClient.on("connectionStateChanged", (state) => {
            switch (state) {
            case "connected":
                this.statusString = "You are connected."
                this.isConnected = true
                break
            case "disconnecting":
                this.statusString = "Disconnecting from Twilio…"
                break
            case "disconnected":
                this.statusString = "Disconnected."
                break
            case "denied":
                this.statusString = "Failed to connect."
                break
            }
        })
    },
}

The first user to enter a user name is treated as the “main user”. The client is assigned to this user with ConversationsClient.create(token).

To create a Conversations client, you will need an access token. The initConversationsClient method is defined as an asynchronous function using async/await, as the client needs to be created after the getToken() method to get the access token is complete.

We’re also using the conversationsClient.on event listener to change the message displayed in the UI every time the user’s connection status to the chat changes.

Next, we’ll create a getToken method to send a GET request to the /auth/user/:user endpoint configured on the server side to get the access token for each user.

Add this code under the initConversationsClient method:

getToken: async function(identity) {
    const response = await fetch(`http://localhost:5000/auth/user/${identity}`)
    const responseJson = await response.json()
    return responseJson.token
},

In this code, we’re using async/await again to wait for a response from the /auth/user/:user endpoint.

Next, we’ll create a registerName method to register the user name. Add this code under the getToken method:

registerName: async function() {
    this.nameRegistered = true
    await this.initConversationsClient()
},

This method updates the nameRegistered property to indicate that the user has entered a username.

Next, we’ll create a createConversation method to create a new conversation. Add this code under the registerName method:

createConversation: async function() {
    // Ensure User1 and User2 have an open client session
    try {
        await this.conversationsClient.getUser("User1")
        await this.conversationsClient.getUser("User2")
    } catch {
        console.error("Waiting for User1 and User2 client sessions")
        return
    }
    // Try to create a new conversation and add User1 and User2
    // If it already exists, join instead
    try {
        const newConversation = await this.conversationsClient.createConversation({uniqueName: "chat"})
        const joinedConversation = await newConversation.join().catch(err => console.log(err))
        await joinedConversation.add("User1").catch(err => console.log("error: ", err))
        await joinedConversation.add("User2").catch(err => console.log("error: ", err))
        this.activeConversation = joinedConversation
    } catch {
        this.activeConversation = await (this.conversationsClient.getConversationByUniqueName("chat"))
    }
}

In this method, we use try...catch to check whether “User1” and “User2” hold a client. If they don’t hold it, it will output a Waiting for User1 and User2 client sessions error message.

We then create a new conversation with an asynchronous process using newConversation.join() and add “User1” and “User2” to the conversation using joinedConversation.add. If the conversation already exists, we set activeConversation to the existing conversation obtained with getConversationByUniqueName.

This method of joining a conversation and adding users is simplified for demonstration purposes and is inherently insecure. In a production application, consider how you will verify the user’s identity, what privileges they will have, and how you will secure the application.

Now we have a Chat.vue component. Let’s edit the App.vue root component so that the Chat.vue component will display correctly.

Open the App.vue component with a text editor and change the content to this code:

<template>
  <Chat />
</template>

<script>
import Chat from "./components/Chat.vue"
import "@twilio/conversations"
export default {
    name: "App",
    components: {
        Chat
    }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
#app input {
  padding: 12px 20px;
  margin: 8px 0;
  display: inline-block;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  margin-right: 5px;
  width: 300px;
}
#app button {
  background-color: #21cfbc;
  color: white;
  padding: 14px 20px;
  margin: 8px 0;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

Let’s check the display of Chat.vue. In the second window of the terminal, execute this command:

npm run serve

Make sure that the server is still running in the first terminal window, and your Vue CLI is running in your second terminal window. Open http://localhost:8080/ in your browser. You will see a screen like this:

Chat app welcome page with name registration form

Try typing your name and click “Register name”. The screen will then update as shown below.

Chat app showing that the user is connected

Now the process for users to register a username is ready. Next, let’s add some code to Conversation.vue to build the conversation section of the app.

Build the conversation component

Open Conversation.vue in a text editor. First, we’ll add the HTML template, like we did for Chat.vue. Paste this code in Conversation.vue:

<template>
  <div id="conversation">
    <div class="conversation-container">
      <div 
        v-for="message in messages" :key="message.index"
        class="bubble-container"
        :class="{ myMessage: message?.state?.author === name }"
      >
        <div class="bubble">
          <div class="name">{{ message?.state?.author }}:</div>
          <div class="message">{{ message?.state?.body }}</div>
        </div>
      </div>
    </div>
    <div class="input-container">
      <input @keyup.enter="sendMessage" v-model="messageText" placeholder="Enter your message">
      <button @click="sendMessage">Send message</button>
    </div>
  </div>
</template>

Next, we’ll add the CSS. Paste this code under the <template> block.

<style scoped>
.conversation-container {
 margin: 0 auto;
 max-width: 400px;
 height: 600px;
 padding: 0 20px;
 border: 3px solid #f1f1f1;
 overflow: scroll;
}
 
.bubble-container {
 text-align: left;
}
 
.bubble {
 border: 2px solid #f1f1f1;
 background-color: #fdfbfa;
 border-radius: 5px;
 padding: 10px;
 margin: 10px 0;
 width: 230px;
 float: right;
}
 
.myMessage .bubble {
 background-color: #abf1ea;
 border: 2px solid #87E0D7;
 float: left;
}
 
.name {
 padding-right: 8px;
 font-size: 11px;
}
 
::-webkit-scrollbar {
 width: 10px;
}
 
::-webkit-scrollbar-track {
 background: #f1f1f1;
}
 
::-webkit-scrollbar-thumb {
 background: #888;
}
 
::-webkit-scrollbar-thumb:hover {
 background: #555;
}
</style>

Next, we’ll add the JavaScript logic. Add this code between the <template> block and the <style> block:

<script>
export default {
    props: ["activeConversation", "name"],
    data() {
        return {
            messages: [],
            messageText: "",
            isSignedInUser: false
        }
    },
}
</script>

In this code, we are importing props. Props are used to pass data from the parent component to the child components. In this chat app, Chat.vue is the parent component and Conversations.vue is the child component. In Chat.vue, <template> passes activeConversation, which is the chat that the user is currently in, as well as the username name as follows:

 <Conversation :active-conversation="activeConversation" :name="name" />

Next, we will add a process to display messages received in the past and set up an event listener for newly received messages. Paste this code inside export default, under the data() method:

mounted() {
    this.activeConversation.getMessages()
        .then((newMessages) => {
            this.messages = [...this.messages, ...newMessages.items]
        })
    this.activeConversation.on("messageAdded", (message) => {
        this.messages = [...this.messages, message]
    })
},

In this code, we are using mounted(). mounted() is one of the Vue lifecycle hooks, and is called when a component is added to the DOM. When a user enters the chat and the chat screen is added to the DOM, the messages sent so far are retrieved and displayed on the screen. We also set up the activeConversation.on event listener to prepare it to receive messages that are about to be sent. Newly sent messages will be added to the messages array using a destructuring assignment.

Next, we’ll define what to do when a user sends a message. Paste this code under mounted().

 

methods: {
    sendMessage: function() {
        this.activeConversation.sendMessage(this.messageText)
            .then(() => {
                this.messageText = ""
            })
    }
}

Here we defined the sendMessage method in methods. We also set messageText to an empty string to clear the text area to enter a message once the message is sent.

Congratulations! That completes the configuration of the chat app! Now let’s test the app we made.

Test the chat app

Make sure that the server is still running in the first window in the terminal and that Vue CLI is running in the second window.

In addition to the browser window you already have open, open two more windows and access http://localhost:8080/ in both windows.

We registered a user with your name earlier. We’ll now register 2 additional users.

Create a user “User1” in the first newly opened window and a user “User2” in the second window. 

 

 

 

In this tutorial, dummy users User1 and User2 are added directly in the program for the purpose of simplifying the demonstration. Entering a user name other than “User1” and “User2” in this step will result in an error.

In a production app, it is recommended to add logic to handle conversation creation, assignment, and user addition based on the user’s behavior.

Registering User1 in the first newly opened window
Registering User2 in the second newly opened window

Next, click “Join chat” in the main user, User1, User2's windows.

A chat screen like this will appear.

Main user connected to a chat

If nothing happens when you click “Join chat”, try opening the browser’s developer console in the main user window. If you get a “Waiting for User1 and User2 client sessions” message when you click “Join chat”, it means that there are no clients for User1 and User2. Open two windows, register User1 and User2, and then try “Join chat” again.

Now let’s send a message. From the main user screen, enter your message in the form and click the “Send message” button.

Message from main user displayed in a chat

Open User1’s screen to see the message from the main user.

Message from first user showing in User1s window

Let’s send a message from “User1” as well. Enter your message in the form and click the “Send message” button.

Open the “User2” screen, you’ll see the messages from the main user and “User1”.


Messages from main user and User1 showing in User2s window

Next steps

Congratulations, you have completed your chat app! If you want to develop the app further, you can add Twilio Sync to share online status, or use Typing Indicator to display a status like “User1 is typing” while the user is typing.

All of the code we have used so far can be found in the Github repository. If you want to learn more about Vue.js, see the official Vue.js documentation.

Feel free to reach out to me at snakajima[at]twilio.com and see what I’m building on Github at smwilk.