Back to search

Create a live chat widget with JavaScript

  • Alex Booker
  • Neo Ighodaro
March 8th, 2018
To follow this tutorial, it will be helpful to have a basic knowledge of HTML, JavaScript (jQuery) and Node.js. Familiarity with a command line interface will also be useful.

In many cases, customers are disconnected from someone who can answer their questions in real time. Rather than watch potential customers click away, live chat support can provide the convenient answers that customers want, and keep them engaged. Unsurprisingly, talking to customers can help you understand their pain points so you can address them and crush your competition.

In this tutorial, we will explore how you can create a custom live chat system for your web application using Chatkit.

This tutorial will cover how to build a chat widget the customer will interact with, and how to create an admin panel where you and your team can talk to your customers.

When we’re done, our entire application will look and function like this:

Preview of finished widget

What you need

To follow along, it would be helpful to have:

  • A basic knowledge of HTML
  • A basic knowledge of JavaScript (in this tutorial, we’ll be using Node and jQuery)
  • Basic knowledge of the command-line

You’ll also need to sign up to Chatkit and create a Chatkit instance.

Create your Chatkit instance

You’re probably wondering “what is Chatkit?” Great question!

Chatkit is a tool that makes it easy to add real-time chat features to your application.

With just a few lines of code, you can add real-time messaging and with just a few more, you can enable advanced features like:

  • Read receipts
  • Typing indicators
  • “Who’s online” presence
  • Roles and permissions

To use Chatkit, you’ll first need to sign up then create a new Chatkit instance:

New ChatKit instance

In the Inspector, create a new user called chatkit-dashboard (remember to hit “Save”):

New ChatKit dashboard

Setup the client environment

To get started, let’s create a new directory. This will be the directory where our entire project will live.

In your terminal, create a new directory for your application, we will name ours acme:

mkdir acme
cd acme 

In this directory, create a new package.json file. This file is where we will add all our Node dependencies:

{
    "main": "index.js",
    "dependencies": {
      "body-parser": "^1.16.0",
      "express": "^4.16.2",
      "pusher-chatkit-server": "^0.5.0"
    }
}

In the above file, we define a couple of dependencies that will be required to run our Node application.

Install them by running npm install:

npm install

The next thing to do will be to create our Express application. If you’re unfamiliar with Express, you can learn more here.

Setup the server environment

In the root directory of your application, create a new file called index.js. In this file, we will define all the routes the application requires to function.

In the index.js file, we will start by importing all the necessary Node modules we added to the package.json file:

const path       = require('path')
const express    = require('express')
const bodyParser = require('body-parser')
const Chatkit    = require('pusher-chatkit-server')

The next thing we need to do is initialise Express and Chatkit.

In the same file (index.js), paste the following:

const app = express()
const chatkit = new Chatkit.default(require({
    instanceLocator: "PUSHER_CHATKIT_INSTANCE_LOCATOR",
    key: "PUSHER_CHATKIT_KEY"
}))

⚠️ Remember to replace the placeholder instanceLocator and key values with your actual credentials from the Chatkit dashbaord

Next, we need to register some Express middleware so that we can accept data from the client as well as serve static files. This will be quite familiar to you if you’re used to Express:

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(express.static(path.join(__dirname, 'assets')))

The next thing to create will be the routes. We will create routes for the API side of things and then to display HTML.

In index.js, paste the code below at the bottom of the file:

app.get('/', (req, res) => {
  res.sendFile('index.html', {root: __dirname + '/views'})
})

Simply, this route will render the homepage view (index.html) once we create it.

We’ll create the view in a second but first, let’s create some other endpoints that we'll need to communicate with Chatkit.

In index.js, add this new route before the one we added above:

app.post('/session/auth', (req, res) => {
    res.json(chatkit.authenticate(req.body, req.query.user_id))
})

Chatkit authentication is powerful and flexible, but we won’t dig too deeply in this tutorial. For the purposes of this tutorial, everyone has equal permissions.

This route will use the authenticate method on the chatkit instance to create a token for the user_id. You can read more about Chatkit tokens in our documentation.

The next endpoint we want to create is /session/load. After the one we just added, paste the code below:

app.post('/session/load', (req, res, next) => {
    // Attempt to create a new user with the email serving as the ID of the user.
    // If there is no user matching the ID, we create one but if there is one we skip
    // creating and go straight into fetching the chat room for that user
    chatkit.createUser(req.body.email, req.body.name)
        .then(() => getUserRoom(req, res, next))
        .catch(err => {
            (err.error_type === 'services/chatkit/user/user_already_exists')
                ? getUserRoom(req, res, next)
                : next(err)
        })

    function getUserRoom(req, res, next) {
        const name  = req.body.name
        const email = req.body.email

        // Get the list of rooms the user belongs to. Check within that room 
        // list for one whos name matches the users ID. If we find one, we 
        // return that as the response, else we create the room and return 
        // it as the response.
        chatkit.apiRequest({method: 'GET', 'path': `/users/${email}/rooms`})
            .then(rooms => {
                let clientRoom = false

                // Loop through user rooms to see if there is already a room for 
                // the client
                rooms.forEach(room => {
                    return room.name === email ? (clientRoom = room) : false
                })

                if (clientRoom && clientRoom.id) {
                    return res.json(clientRoom)
                }

                const createRoomRequest = {
                    method: 'POST',
                    path: '/rooms',
                    jwt: chatkit.generateAccessToken({userId: email}).token,
                    body: { name: email, private: false, user_ids: ['adminuser'] },
                };

                // Since we can't find a client room, we will create one and return 
                // that.
                chatkit.apiRequest(createRoomRequest)
                       .then(room => res.json(room))
                       .catch(err => next(
                           new Error(`${err.error_type} - ${err.error_description}`)
                       ))
            })
            .catch(err => next(
                new Error(`${err.error_type} - ${err.error_description}`)
            ))
    }
})

In the route above we are doing a couple of things.

First, we take the user’s name and email, and use them to create a new Chatkit user using createUser.

Once the user exists, we call getUserRoom to fetch a Chatkit room for the user and agent. A Chatkit room can contain between 1 and 100 users. If you want to build a one-to-one chat like we do here, we can create a room with just two people in it.

In the getUserRoom function, we make a request to get a list of all the available rooms on the Chatkit application. If there is a room that matches the user’s email address then we return that room. If that room doesn’t exist then we create a new room with the user’s email address as the name of the room then return that room.

Add the chat UI

Earlier in the tutorial, we defined a route for the homepage. Now let’s write the HTML in a file called index.html in the views directory:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Acme Agency</title>
    <link href="//getbootstrap.com/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="chat.css">
  </head>
  <body>
    <div class="site-wrapper">
      <div class="site-wrapper-inner">
        <div class="cover-container">
          <header class="masthead clearfix">
            <div class="inner">
              <h3 class="masthead-brand">ACME.</h3>
              <nav class="nav nav-masthead">
                <a class="nav-link active" href="#">Home</a>
                <a class="nav-link" href="#">Features</a>
                <a class="nav-link" href="#">Contact</a>
              </nav>
            </div>
          </header>
          <main role="main" class="inner cover">
            <h1 class="cover-heading">ACME.</h1>
            <p class="lead">ACME is a San Francisco based design agency. We build amazing web experiences.</p>
            <p class="lead">
              <a href="#" class="btn btn-lg btn-secondary">Learn more</a>
            </p>
          </main>
          <footer class="mastfoot">
            <div class="inner">
              <p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>. Photo by <a href="https://unsplash.com/photos/UxtIESWxLh8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Jean Gerber</a></p>
            </div>
          </footer>
        </div>
      </div>
    </div>
    <div class="chatbubble">
        <div class="unexpanded">
            <div class="title">Chat with Support</div>
        </div>
        <div class="expanded chat-window">
          <div class="login-screen container">
            <form id="loginScreenForm">
              <div class="form-group">
                <input type="text" class="form-control" id="fullname" placeholder="Name*" required>
              </div>
              <div class="form-group">
                <input type="email" class="form-control" id="email" placeholder="Email Address*" required>
              </div>
              <button type="submit" class="btn btn-block btn-primary">Start Chat</button>
            </form>
          </div>
          <div class="chats">
            <div class="loader-wrapper">
              <div class="loader">
                <span>{</span><span>}</span>
              </div>
            </div>
            <ul class="messages clearfix">
            </ul>
            <div class="input">
              <form class="form-inline" id="messageSupport">
                <div class="form-group">
                  <input type="text" autocomplete="off" class="form-control" id="newMessage" placeholder="Enter Message">
                </div>
                <button type="submit" class="btn btn-primary">Send</button>
              </form>
            </div>
          </div>
        </div>
    </div>
    <script src="//code.jquery.com/jquery-3.2.1.slim.min.js"></script>
    <script src="//cdn.jsdelivr.net/npm/axios@0.17.0/dist/axios.min.js"></script>
    <script src="//unpkg.com/pusher-chatkit-client"></script>
    <script src="chat.js"></script>
  </body>
</html>

In the HTML above, we import a custom css file, chat.css, which you can download here.

Next, we use a Bootstrap template to create a make-believe landing page for our make-believe agency.

We have a .chatbubble div that contains all the code for our customer support chat room. We also have a loginScreenForm that displays the login screen with a Name and Email Address field.

When the start button is clicked, we show a loader animation while the data is sent to the server.

In the backend, we will create a new Chatkit session and a chatroom using the email address of the customer. When all that is complete, it loads up the chat room’s messages if any have been left there before.

The next thing we want to load is some custom JavaScript for the front-end from a file called chat.js.

Rather than include the complete code here, you’ll need to download chat.js (link above) and save it in the assets directory.

Here, we will break down the important parts:

const PUSHER_INSTANCE_LOCATOR = "PUSHER_CHATKIT_INSTANCE_LOCATOR"

let chat = {
    messages: [],
    room:  undefined,
    userId: undefined,
    currentUser: undefined,
}

const chatPage   = $(document)
const chatWindow = $('.chatbubble')
const chatHeader = chatWindow.find('.unexpanded')
const chatBody   = chatWindow.find('.chat-window')

const helpers = {}

chatPage.ready(helpers.ShowAppropriateChatDisplay)
chatHeader.on('click', helpers.ToggleChatWindow)
chatBody.find('#loginScreenForm').on('submit', helpers.LogIntoChatSession)
chatBody.find('#messageSupport').on('submit', helpers.SendMessageToSupport)

⚠️ Remember to replace the placeholder PUSHER_INSTANCE_LOCATOR value with your actual instance locator from the Chatkit dashboard

Above, we just define some variables we will be needing. The chat variable will be the main object to hold our messages, the current room object etc.

Next, we define a helpers object. We will be adding several methods to this JavaScript object. After the helpers definition, we start registering event handlers to various events from the page.

Let’s start adding methods to the helpers object. The first one will be ToggleChatWindow. This is a function that toggles the display of the chat window. It either minimizes it or maximises it depending on the current state. Here is the function:

ToggleChatWindow: function () {
    chatWindow.toggleClass('opened')
    chatHeader.find('.title').text(
        chatWindow.hasClass('opened') ? 'Minimize Chat Window' : 'Chat with Support'
    )
},

The next one will be ShowAppropriateChatDisplay. In this function, we will show either the login screen or the chat screen. It decides which to show dependent on the chat.room object being set. If there is a room loaded then it shows the messages in the chat screen.

ShowAppropriateChatDisplay: function () {
    if (chat.room && chat.room.id) {
        helpers.ShowChatRoomDisplay()
    } else {
        helpers.ShowChatInitiationDisplay()
    }
},

The next method will be ShowInitiationDisplay. This function just makes sure the login screen is the one showing:

ShowChatInitiationDisplay: function () {
    chatBody.find('.chats').removeClass('active')
    chatBody.find('.login-screen').addClass('active')
},

The next function to add to the helpers object is the ShowChatRoomDisplay function:

ShowChatRoomDisplay: function () {
    chatBody.find('.chats').addClass('active')
    chatBody.find('.login-screen').removeClass('active')

    // Create a token provider to retrieve the token from our Node server
    const tokenProvider = new Chatkit.TokenProvider({
        userId: chat.userId, 
        url: "/session/auth",
    })

    // Create an instance of the chatkit manager
    const chatManager = new Chatkit.ChatManager({
        tokenProvider,
        instanceLocator: PUSHER_INSTANCE_LOCATOR,
    });

    // Connect to chatkit
    chatManager.connect({
        // Successful connection
        onSuccess: currentUser => {
            chat.currentUser = currentUser

            // Fetch ,essages and add them to the UI
            currentUser.fetchMessagesFromRoom(chat.room, {}, messages => {
                // Hide the loading screen
                chatBody.find('.loader-wrapper').hide()
                chatBody.find('.input, .messages').show()

                // Add messages to the UI
                messages.forEach(message => helpers.NewChatMessage(message))

                // Subscribe to the room and add a listener for new messages
                currentUser.subscribeToRoom(chat.room, {
                    newMessage: message => helpers.NewChatMessage(message)
                })
            }, err => {
                console.error(err)
            })
        }
    });
},

In the function above, we created a tokenProvider instance. This token provider sends a request to /session/auth with the user_id. The endpoint returns a JWT that the chat app can then use to make requests on behalf of the user.

After that, we connected to chatkit using the token provided. On successful connection, we fetch the messages from the room and then add the messages to the UI.

Lastly, we subscribe to the room so we can add listeners. The only listener we implement is newMessage. When a new message is received, this function is called and the message is rendered. There are other listeners you can implement though, take a look at the docs for some more.

Next, let’s add the NewChatMessage function to the helpers object. In the function we will take a message object and add it to the UI. We will also save it to chat.messages. Here is the code:

NewChatMessage: function (message) {
    if (chat.messages[message.id] === undefined) {
        const messageClass = message.sender.id !== chat.userId ? 'support' : 'user'

        chatBody.find('ul.messages').append(
            `<li class="clearfix message ${messageClass}">
                <div class="sender">${message.sender.name}</div>
                <div class="message">${message.text}</div>
            </li>`
        )

        chat.messages[message.id] = message
        chatBody.scrollTop(chatBody[0].scrollHeight)
    }
},

The next function to add will be SendMessageToSupport. This function simply posts a message directly to the room:

SendMessageToSupport: function (evt) {
    evt.preventDefault()
    const message = $('#newMessage').val().trim()

    chat.currentUser.addMessage(message, chat.room)

    $('#newMessage').val('')
},

The final method we will add to the helpers object is LogIntoChatSession. This method will allow us to log in to a chat session. It sends the user’s name and email address and then receives a room object in response. We then call ShowAppropriateChatDisplay after we have gotten the room:

LogIntoChatSession: function (evt) {
    const name  = $('#fullname').val().trim()
    const email = $('#email').val().trim().toLowerCase()

    // Disable the form
    chatBody.find('#loginScreenForm input, #loginScreenForm button').attr('disabled', true)

    if ((name !== '' && name.length >= 3) && (email !== '' && email.length >= 5)) {
        axios.post('/session/load', {name, email}).then(response => {
            chat.userId = email
            chat.room   = response.data
            helpers.ShowAppropriateChatDisplay()
        })
    } else {
        alert('Enter a valid name and email.')
    }

    evt.preventDefault()
}

That’s all for the chat.js file. Again if you need the full version, it’s available here.

Test it out

If you followed the tutorial so far you should have been able to create the agency website with the chat widget at the bottom right of the screen.

Run the command below to start a Node server:

node index.js

When you visit http://localhost:3000 you should see something like this:

Chat widget screenshot

Now when you log in and send messages, they will be sent to Chatkit and you can see all the messages posted to the room.

In the rest of this tutorial, we’ll look at how to build an admin interface so a support agent can join the conversation.

Render the admin view

Next, we will focus on the admin-side of our chat. In this backend, we will be able to communicate with users who contact us using the chat widget we built in the previous part.

In index.js, add the following route:

app.get('/admin', (req, res) => {
    res.sendFile('admin.html', {root: __dirname + '/views'})
})

Simply, this route will render the admin page (admin.html) once we create it.

To keep this tutorial simple, we will leave the admin page public. In a real world scenario, you would want to ensure the user is authenticated and authorised before accessing this route.

Add the admin UI

Let’s build up the frontend for our admin panel. When we are done we will have something similar to this:

Chat widget admin panel

On the left we have the rooms that are available and on the right we have the chat window. When a chat room is selected, the chat will pop up on the right.

Create a new file views/admin.html and add the following:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Admin</title>
    <link href="//getbootstrap.com/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="admin.css" rel="stylesheet">
  </head>
  <body>
    <header>
        <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
            <a class="navbar-brand" href="#">Dashboard</a>
        </nav>
    </header>
    <div class="container-fluid">
        <div class="row" id="mainrow">
            <nav class="col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar">
                <ul class="nav nav-pills flex-column" id="rooms">
                </ul>
            </nav>
            <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" id="main">
                <h1>Chats</h1>
                <p>👈 Select a chat to load the messages</p>
                <p>&nbsp;</p>
                <div class="chat" style="margin-bottom:150px">
                    <h5 id="room-title"></h5>
                    <p>&nbsp;</p>
                    <div class="response">
                        <form id="replyMessage">
                            <div class="form-group">
                                <input type="text" placeholder="Enter Message" class="form-control" name="message" />
                            </div>
                        </form>
                    </div>
                    <div class="table-responsive">
                      <table class="table table-striped">
                        <tbody id="chat-msgs">
                        </tbody>
                    </table>
                </div>
            </main>
        </div>
    </div>
    <script src="//code.jquery.com/jquery-3.2.1.slim.min.js"></script>
    <script src="//cdn.jsdelivr.net/npm/axios@0.17.0/dist/axios.min.js"></script>
    <script src="//unpkg.com/pusher-chatkit-client"></script>
    <script src="admin.js"></script>
  </body>
</html>

As you can see, both the conversation list and chat containers are empty. We’ll populate them dynamically later with the data we get from Chatkit 👍🏻

Style the admin page

In the above HTML, we reference a CSS file called assets/admin.css, which you can download here.

I won’t explain the CSS in this tutorial as we want to focus on building live chat.

Create the admin panel JavaScript

In the same HTML file, we reference a script called assets/admin.js, which you can download here

I haven’t included the entire code (copy and paste it from the above link) but we will break it down here:

const PUSHER_INSTANCE_LOCATOR = "PUSHER_CHATKIT_INSTANCE_LOCATOR"

let chat = {
    rooms: [],
    messages: [],
    currentUser: false,
}

const chatBody = $(document)
const chatRoomsList = $('#rooms')
const chatReplyMessage = $('#replyMessage')

const helpers = {}

chatBody.ready(helpers.loadChatManager)
chatReplyMessage.on('submit', helpers.replyMessage)
chatRoomsList.on('click', 'li', helpers.loadChatRoom)

⚠️ Remember to replace the placeholder PUSHER_INSTANCE_LOCATOR value with your actual credentials from the Chatkit dashbaord

Above we define some initial variables we’ll use later.

The chat variable will be the main object to hold our messages, the list of rooms, and the current user.

We also defined the helpers object. In here we will add some helper methods. Let’s start defining them.

The first one is the clearChatMessages function which is fairly straightforward:

clearChatMessages: () => {
    $('#chat-msgs').html('')
},

The next helper method is the displayChatMessage method which displays a single chat message by adding it to the UI:

displayChatMessage: (message) => {
    if (chat.messages[message.id] === undefined) {
        chat.messages[message.id] = message

        $('#chat-msgs').prepend(
            `<tr>
                <td>
                    <div class="sender">
                        ${message.sender.name} @ 
                        <span class="date">${message.createdAt}</span>
                    </div>
                    <div class="message">${message.text}</div>
                </td>
            </tr>`
        )
    }
},

Here, we use jQuery but you could just as well use React, Vue, vanilla JavaScript etc.

The next method to add to the helpers object is the loadChatRoom method:

loadChatRoom: (evt) => {
    chat.currentRoom = chat.rooms[$(evt.target).data('room-id')]

    if (chat.currentRoom !== undefined) {
        $('.response').show()
        $('#room-title').text(chat.currentRoom.name)

        chat.currentUser.fetchMessagesFromRoom(chat.currentRoom, {}, msgs => {
            msgs.forEach(message => helpers.displayChatMessage(message))

            chat.currentUser.subscribeToRoom(chat.currentRoom, {
                newMessage: message => helpers.displayChatMessage(message)
            })
        })
    }

    evt.preventDefault()

    helpers.clearChatMessages()
},

In this function, we try to load a chat room by fetching messages belonging to the room. Then, we add the messages to the UI and add a newMessage listener to catch new messages sent by Chatkit.

The next method is replyMessage. In this method we post a new message to the chat room:

replyMessage: (evt) => {
    evt.preventDefault()

    const message = $('#replyMessage input').val().trim()

    chat.currentUser.addMessage(
        message, 
        chat.currentRoom, 
        messageId => {}, 
        error => {}
    )

    $('#replyMessage input').val('')
},

The final method we have to add is loadChatManager:

loadChatManager: () => {
    const tokenProvider = new Chatkit.TokenProvider({
        userId: 'chatkit-dashbaord',
        url: "/session/auth", 
    })

    const chatManager = new Chatkit.ChatManager({
        tokenProvider,
        instanceLocator: PUSHER_INSTANCE_LOCATOR,
    });

    chatManager.connect({
        onSuccess: user => {
            chat.currentUser = user

            // Get all joinable rooms and join them...
            user.getJoinableRooms(rooms => {
                rooms.forEach(room => user.joinRoom(room.id))
            })

            // Get all rooms and put a link on the sidebar...
            user.getAllRooms(
                allRooms => {
                    allRooms.forEach(room => {
                        if ( ! chat.rooms[room.id]) {
                            chat.rooms[room.id] = room

                            $('#rooms').append(
                                `<li class="nav-item"><a data-room-id="${room.id}" class="nav-link" href="#">${room.name}</a></li>`
                            )
                        }
                    })
                }
            )
        }
    })
}

In the method above, we instantiate a Chatkit.TokenProvider and tell it to fetch a token from the /session/auth endpoint we created earlier in the tutorial.

We specify that the userId is chatkit-dashboard, which the server uses to generate the token that will be used to connect to Chatkit.

When the connection is successful, we get all the joinable rooms for the current user and join them. If there are no joinable rooms, that endpoint will return an empty array and so we are also calling the getAllRooms method which will list all the available rooms which we add to the sidebar.

That’s all for the admin.js file. Again, if you need the full version, it’s available here.

With those new files, we should have an admin section ready to be used in conjunction with the chat widget we created in the first part of the article.

Here is the preview again of how it will look:

Preview of finished widget

Conclusion

In this tutorial, you learned how to build a customer support widget and administrator interface using JavaScript and Chatkit. Thanks to Chatkit, we didn’t have to write much server code at all, Chatkit does all the heavy lifting. For free, we get message retention, stability, and with a few adaptions, we could easily add read receipts, typing indicators, and more. Read more about Chatkit in our documentation.

The source code to the article is available on GitHub.

  • Chatkit

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.