how-to-build-a-chat-app-with-angularjs

How to Build an AngularJS Chat App

Let’s build a fully-functioning chat app for web and mobile web using AngularJS and ChatEngine. If you’re unfamiliar with ChatEngine, it offers the infrastructure and a ton of APIs for building chat apps. That includes a number of plugins, a bunch that we’ll implement today to build out the core features of any chat app.

The full GitHub repo for this project is available here.

Step 1: Setup

Before getting started, you’ll have to setup ChatEngine via our ChatEngine Quick Start. This enables you to use your own PubNub application keyset and automagically sets up your account to use ChatEngine. It’ll take a couple minutes at most, then you can come back here and continue on.

With that done, let’s set up the environment.

mkdir chatengine-simple-demo

cd chatengine-simple-demo

npm init

npm install chat-engine --save

npm install chat-engine-random-username --save

npm install chat-engine-online-user-search --save

npm install chat-engine-typing-indicator --save

We’ll have to create a new HTML file which called index.html. This file will reference the following:

  1. Boostrap
  2. AngularJS
  3. ChatEngine
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>ChatEngine Simple Example</title>

        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css"
          integrity="sha384-AysaV+vQoT3kOAXZkl02PThvDr8HYKPZhNT5h/CXfBThSRXQ6jW5DO2ekP5ViFdi" crossorigin="anonymous">

        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>

        <script src="node_modules/chat-engine/dist/chat-engine.js"></script>

    </head>
    <body>
    </body>
</html>

Step 2: Bind ChatEngine to AngularJS

At this point, ChatEngine asks AngularJS for an update to the UI when something happens. To do this, add a script file called plugin.js.

angular.module('open-chat-framework', [])
  .service('ngChatEngine', ['$timeout', function($timeout) {

    this.bind = function(ChatEngine) {
      // updates angular when anything changes
      ChatEngine.onAny(function(event, payload) {
        $timeout(function() {});
      });
    }
  }]);

Now, inject this plugin to AngularJS and initialize ChatEngine.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {
    $rootScope.ChatEngine = ChatEngineCore.create({
      publishKey: 'pub-c-d8599c43-cecf-42ba-a72f-aa3b24653c2b',
      subscribeKey: 'sub-c-6c6c021c-c4e2-11e7-9628-f616d8b03518'
    }, {
      debug: true,
      globalChannel: 'chat-engine-angular-simple'
    });

    // bind open chat framework angular plugin
    ngChatEngine.bind($rootScope.ChatEngine);
  }]);

Having these files inside the root, we have to reference them to index.html and add the name main module.

<!DOCTYPE html>
<html ng-app="chatApp">
    <head>
        <meta charset="UTF-8">
        <title>ChatEngine Simple Example</title>

        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css"
          integrity="sha384-AysaV+vQoT3kOAXZkl02PThvDr8HYKPZhNT5h/CXfBThSRXQ6jW5DO2ekP5ViFdi" crossorigin="anonymous">

        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>

        <script src="node_modules/chat-engine/dist/chat-engine.js" type="text/javascript"></script>

        <script src="plugin.js"></script>
        <script src="chatApp.js"></script>
    </head>
    <body>
    </body>
</html>

Step 3: Connect ChatEngine

Now that ChatEngine is injected, we can access this from the parameter $scope of any controller. Then we add a controller called chatAppController which will be in charge of connecting ChatEngine as soon as AngularJS has been loaded and it will set the field me which represent our user.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {
    $rootScope.ChatEngine = ChatEngineCore.create({
      publishKey: 'pub-c-d8599c43-cecf-42ba-a72f-aa3b24653c2b',
      subscribeKey: 'sub-c-6c6c021c-c4e2-11e7-9628-f616d8b03518'
    }, {
      debug: true,
      globalChannel: 'chat-engine-angular-simple'
    });

    // bind open chat framework angular plugin
    ngChatEngine.bind($rootScope.ChatEngine);
  }])
  .controller('chatAppController', function($scope) {
    $scope.ChatEngine.connect(new Date().getTime(), {}, 'auth-key');

    $scope.ChatEngine.on('$.ready', (data) => {
      $scope.me = data.me;
    });
  });

The body of your HTML executes the controller when this will be rendered in the browser.

<body ng-app="chatApp">
<div class="container-fluid" ng-controller="chatAppController">
    <div class="row">
        <div class="col-md-6">
            <div class="card">
                <div class="card-block">
                    <h4 class="card-title">ChatEngine</h4>
                    <p class="card-text">Your are connect with uuid {{me.uuid}}</p>
                </div>
            </div>
        </div>
    </div>
</div>
</body>

Step 4: Live-Updating List of Online Users

Inside of the event $.ready which is execute as soon as the ChatEngine is connected and ready to use; we will bind the global channel which was set when we setup the ChatEngine to the $scope with field chat.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ....

  }])
  .controller('chatAppController', function($scope) {
    $scope.ChatEngine.connect(new Date().getTime(), {}, 'auth-key');

    $scope.ChatEngine.on('$.ready', (data) => {
      $scope.me = data.me;

      // bind chat to updates
      $scope.chat = $scope.ChatEngine.global;
    });
  });

Next, add the HTML elements to display the list of users connected.

<div class="card">
    <div class="card-block">
        <h4 class="card-title">ChatEngine</h4>
        <p class="card-text">Your are connect with uuid {{me.uuid}}</p>
    </div>
    <ul id="online-list" class="list-group list-group-flush">
        <li class="list-group-item" ng-repeat="(uuid, user) in chat.users">
            <a href="">{{uuid}}</a>
        </li>
    </ul>
</div>

But you’ll notice that mere identifiers of each user doesn’t look so good. Luckily, ChatEngine provides a plugin that generates a random username and assigns it to each user (chat-engine-random-username). For this, you’ll have to add the reference of the plugin after the reference of ChatEngine inside the head of your HTML file.

<script src="node_modules/chat-engine-random-username/dist/chat-engine-random-username.js" type="text/javascript"></script>

After adding the reference, you have to load the plugin to object me and slightly change your UI.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function($scope) {
    $scope.ChatEngine.connect(new Date().getTime(), {}, 'auth-key');

    $scope.ChatEngine.on('$.ready', (data) => {
      $scope.me = data.me;

      $scope.me.plugin(ChatEngineCore.plugin['chat-engine-random-username']($scope.ChatEngine.global));

      // bind chat to updates
      $scope.chat = $scope.ChatEngine.global;
    });
  });
<div class="card">
    <div class="card-block">
        <h4 class="card-title">ChatEngine</h4>
        <p class="card-text">Your are {{me.state.username}} with uuid {{me.uuid}}</p>
    </div>
    <ul id="online-list" class="list-group list-group-flush">
        <li class="list-group-item" ng-repeat="(uuid, user) in chat.users">
            <a href="">{{user.state.username}}</a>
        </li>
    </ul>
</div>

Like the previous step, we’re going to use a plugin that makes adding the ability to search for online users super easy to implement.

<script src="node_modules/chat-engine-online-user-search/dist/chat-engine-online-user-search.js" type="text/javascript"></script>

Add the reference, and load the plugin, in this case to global chat.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function($scope) {
    $scope.ChatEngine.connect(new Date().getTime(), {}, 'auth-key');

    $scope.ChatEngine.on('$.ready', (data) => {
      $scope.me = data.me;

      $scope.me.plugin(ChatEngineCore.plugin['chat-engine-random-username']($scope.ChatEngine.global));

      // bind chat to updates
      $scope.chat = $scope.ChatEngine.global;
      $scope.chat.plugin(ChatEngineCore.plugin['chat-engine-online-user-search']({ prop: 'state.username' }));
    });
  });

Having the plugin loaded, we have to introduce the function search to our controller. Inside of this, we have the logic to hide the users which do not match with the query.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function($scope) {
    $scope.ChatEngine.connect(new Date().getTime(), {}, 'auth-key');

    $scope.ChatEngine.on('$.ready', (data) => {
      $scope.me = data.me;

      $scope.me.plugin(ChatEngineCore.plugin['chat-engine-random-username']($scope.ChatEngine.global));

      // bind chat to updates
      $scope.chat = $scope.ChatEngine.global;
      $scope.chat.plugin(ChatEngineCore.plugin['chat-engine-online-user-search']({ prop: 'state.username' }));
    });

    $scope.search = function () {
      let found = $scope.chat.onlineUserSearch.search($scope.mySearch);

      // hide every user
      for(let uuid in $scope.chat.users) {
        $scope.chat.users[uuid].hideWhileSearch = true;
      }

      // show all found users
      for(let i in found) {
        $scope.chat.users[found[i].uuid].hideWhileSearch = false;
      }
    }
  });

Now we’ll add some small changes to the HTML and add search capability to our UI.

<div class="card">
    <div class="card-block">
        <h4 class="card-title">ChatEngine</h4>
        <p class="card-text">Your are {{me.state.username}} with uuid {{me.uuid}}</p>
    </div>
    <ul id="online-list" class="list-group list-group-flush">
        <li class="list-group-item" ng-repeat="(uuid, user) in chat.users" ng-hide="user.hideWhileSearch">
            <a href="">{{user.state.username}}</a>
        </li>
    </ul>
    <div class="card-block">
        <form class="send-message" ng-submit="search()">
            <div class="input-group">
                <input id="usernameSearch" type="text" class="form-control message"
                       placeholder="Search for Username" ng-change="search()" ng-model="mySearch">
                <span class="input-group-btn">
                    <button class="btn btn-primary" type="submit">Search</button>
                </span>
            </div>
        </form>
    </div>
</div>

Step 6: Inviting Users to a Chatroom

To create a chatroom for one or more users, the ChatEngine approach is similar to a traditional phone call. A user initiates a call, and someone else answers the call to establish the channel of conversation.

In this step, we’ll focus on the first part – inviting someone to join a conversation. We have to add the field called chats (an array to the $rootScope) and a new function called newChat() into the controller. This will receive a parameter which is the data that represents a user.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {
    $rootScope.ChatEngine = ChatEngineCore.create({
      publishKey: 'PUB_KEYS_HERE',
      subscribeKey: 'SUB_KEYS_HERE'
    }, {
      debug: true,
      globalChannel: 'chat-engine-angular-simple'
    });

    // bind open chat framework angular plugin
    ngChatEngine.bind($rootScope.ChatEngine);

    // set a global array of chatrooms
    $rootScope.chats = [];
  }])
  .controller('chatAppController', function($scope) {

    ...

  });
angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function($scope) {
    $scope.ChatEngine.connect(new Date().getTime(), {}, 'auth-key');

    ...

    // create a new chat
    $scope.newChat = function(user) {
      // define a channel
      let chat = new Date().getTime();
      // create a new chat with that channel
      let newChat = new $scope.ChatEngine.Chat(chat);
      // we need to auth ourselves before we can invite others
      newChat.on('$.connected', () => {
        // this fires a private invite to the user
        newChat.invite(user);
        // add the chat to the list
        $scope.chats.push(newChat);
      });
    };
  });

Once you’ve done these two things, we’ll attach the function newChat to our list of online users.

<div class="card">
    <div class="card-block">
        <h4 class="card-title">ChatEngine</h4>
        <p class="card-text">Your are {{me.state.username}} with uuid {{me.uuid}}</p>
    </div>
    <ul id="online-list" class="list-group list-group-flush">
        <li class="list-group-item" ng-repeat="(uuid, user) in chat.users" ng-hide="user.hideWhileSearch">
            <a href="" ng-click="newChat(user)">{{user.state.username}}</a>
        </li>
    </ul>
    <div class="card-block">
        <form class="send-message" ng-submit="search()">
            <div class="input-group">
                <input id="usernameSearch" type="text" class="form-control message"
                       placeholder="Search for Username" ng-change="search()" ng-model="mySearch">
                <span class="input-group-btn">
                    <button class="btn btn-primary" type="submit">Search</button>
                </span>
            </div>
        </form>
    </div>
</div>

Step 7: Accepting a Chat Invite

Now we have to allow users to receive and accept a chat invitation, to establish our channel of conversation.

We do this by adding an event handler to the object which represents me inside of the ChatEngine.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...r

  }])
  .controller('chatAppController', function($scope) {
    $scope.ChatEngine.connect(new Date().getTime(), {}, 'auth-key');

    $scope.ChatEngine.on('$.ready', (data) => {
      $scope.me = data.me;
      $scope.me.plugin(ChatEngineCore.plugin['chat-engine-random-username']($scope.ChatEngine.global));

      // when I get a private invit
      $scope.me.direct.on('$.invite', (payload) => {
        let chat = new $scope.ChatEngine.Chat(payload.data.channel);
        chat.onAny((a,b) => {
          console.log(a);
        });
        // create a new chat and render it in DOM
        $scope.chats.push(chat);
      });

      // bind chat to updates
      ...
    });

      ...
  });

Notice that when an invitation is received, we add the new chat to the array of chats introduced in the previous step. This is how we can access the controller from the UI and render it to the HTML.

<div class="row">
    <div class="col-md-6">
        <div class="card">
            <div class="card-block">
                <h4 class="card-title">ChatEngine</h4>
                <p class="card-text">Your are {{me.state.username}} with uuid {{me.uuid}}</p>
            </div>

            ...

        </div>
    </div>
    <div class="col-md-6">
        <div id="chats" class="row" ng-repeat="chat in chats">
            <div class="chat col-xs-12">
                <div class="card">
                    <div class="card-header">
                        <div class="col-sm-6">
                            {{chat.channel}}
                        </div>
                        <div class="col-sm-6 text-right">
                            <a href="" class="close">x</a>
                        </div>
                    </div>
                    <ul class="list-group list-group-flush online-list-sub">
                        <li class="list-group-item" ng-repeat="(uuid, user) in chat.users"
                            ng-hide="user.hideWhileSearch">
                            {{user.state.username}}
                        </li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>

Step 8: Send and Receive Messages

Now let’s add the core functionality of any chat application – sending and receiving chat messages in realtime. We need to add an additional controller assigned to each chatroom that is in charge of sending, receiving, and storing a list of messages.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function($scope) {

    ...

  })
  .controller('chat', function($scope) {
    // every chat has a list of messages
    $scope.messages = [];

    // send a message using the messageDraft input
    $scope.sendMessage = function () {
      $scope.chat.emit('message', { text: $scope.newMessage });
      $scope.newMessage = '';
    };

    // when this chat gets a message
    $scope.chat.on('message', function(payload) {
      // if the last message was sent from the same user
      payload.sameUser = $scope.messages.length > 0 && payload.sender.uuid == $scope.messages[$scope.messages.length - 1].sender.uuid;

      // if this message was sent by this client
      payload.isSelf = payload.sender.uuid == $scope.me.uuid;

      // add the message to the array
      $scope.messages.push(payload);
    });
  });
<div class="col-md-6">
    <div id="chats" class="row" ng-repeat="chat in chats" ng-controller="chat">
        <div class="chat col-xs-12">
            <div class="card">
                <div class="card-header">
                    <div class="col-sm-6">
                        {{chat.channel}}
                    </div>
                    <div class="col-sm-6 text-right">
                        <a href="" class="close">x</a>
                    </div>
                </div>
                <ul class="list-group list-group-flush online-list-sub">
                    <li class="list-group-item" ng-repeat="(uuid, user) in chat.users"
                        ng-hide="user.hideWhileSearch">
                        {{user.state.username}}
                    </li>
                </ul>
                <div class="card-block">
                    <div>
                        <div ng-repeat="message in messages" ng-class="{'hide-username': message.sameUser, 'text-xs-right': !message.isSelf}">
                            <p class="text-muted username">{{message.sender.state.username}}</p>
                            <p>{{message.data.text}}</p>
                        </div>
                    </div>
                    <p class="typing text-muted"></p>
                    <form class="send-message" ng-submit="sendMessage(chat)">
                        <div class="input-group">
                            <input id="usernameMessage"  ng-model="newMessage" type="text" class="form-control message" placeholder="Your Message...">
                            <span class="input-group-btn">
                                <button class="btn btn-primary" type="submit">Send</button>
                            </span>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

Step 9: Leave a Chat Room

Now let’s allow users to leave a chatroom and remove it from the list of chats. We add a function to the controller chat, and the chatroom will disappear from our UI.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function ($scope) {

    ...

  })
  .controller('chat', function ($scope) {

    ...

    // leave a chatroom and remove from global chat list
    $scope.leave = function (index) {
      $scope.chat.leave();
      $scope.chats.splice(index, 1);
    };
  });

To finish this step, we bind this function to the HTML in order to be invoked when the user wants to close the chatroom.

<div class="col-sm-6 text-right">
    <a href="" ng-click="leave($index)" class="close">x</a>
</div>

Step 10: Add Additional Users to the Chatroom

You may want to allow users to have a multi-user group chat. In this step, we’ll add some code to add this functionality.

We’ll add two additional functions to the controller. The first one searches from the global list, and the second one invites the users to join them to the chatroom.

angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function ($scope) {

    ...

  })
  .controller('chat', function ($scope) {

    ...

    // find an user name from global list
    $scope.searchFromGlobal = function () {
      if($scope.mySearchFromGlobal) {
        $scope.users = $scope.ChatEngine.global.onlineUserSearch.search($scope.mySearchFromGlobal);
      } else {
        $scope.users = [];
      }
    };

    // invite to user to join
    $scope.invite = function (user) {
      $scope.chat.invite(user);
    };
  });

To finish this step we have to bind the two functions to the HTML elements which are in charge of invoking these functions.

<div class="chat col-xs-12">
    <div class="card">
        ...
        <hr />
        <div class="card-block">
            <h6>Add a user to this chat</h6>
            <form ng-submit="searchFromGlobal()">
                <div class="input-group">
                    <input name="usernameSearchGlobal" type="text" class="form-control message"
                           placeholder="Add User" ng-change="searchFromGlobal()"
                           ng-model="mySearchFromGlobal">
                </div>
            </form>
            <ul class="list-group list-group-flush online-list-sub">
                <li class="list-group-item" ng-repeat="(uuid, user) in users">
                    <a href="#" ng-click="invite(user)"> {{user.state.username}}</a>
                </li>
            </ul>
        </div>
    </div>
</div>

Step 11: Typing Indicators

Last but not least, typing indicators, the feature that shows when a user is actively typing a message. We have a ChatEngine plugin that makes this functionality really easy to implement.

<script src="node_modules/chat-engine-typing-indicator/dist/chat-engine-typing-indicator.js" type="text/javascript"></script>
angular.module('chatApp', ['open-chat-framework'])
  .run(['$rootScope', 'ngChatEngine', function($rootScope, ngChatEngine) {

    ...

  }])
  .controller('chatAppController', function ($scope) {

    ...

  })
  .controller('chat', function ($scope) {
    $scope.chat.plugin(ChatEngineCore.plugin['chat-engine-typing-indicator']({ timeout: 5000 }));

    ...

    // when we get notified of a user typing
    $scope.chat.on('$typingIndicator.startTyping', (event) => {
      event.sender.isTyping = true;
    });

    // when we get notified a user stops typing
    $scope.chat.on('$typingIndicator.stopTyping', (event) => {
      event.sender.isTyping = false;
    });
  });

We’ve now loaded the plugin inside the controller and mounted the events that trigger when the user starts and stops typing. The plugin then sends that status to all connected users.

<form class="send-message" ng-submit="sendMessage(chat)">
    <div class="input-group">
        <input id="usernameMessage"  ng-model="newMessage" type="text" class="form-control message"
               placeholder="Your Message..." ng-change="chat.typingIndicator.startTyping()">
        <span class="input-group-btn">
            <button class="btn btn-primary" type="submit">Send</button>
        </span>
    </div>
</form>
<ul class="list-group list-group-flush online-list-sub">
    <li class="list-group-item" ng-repeat="(uuid, user) in chat.users"
        ng-hide="user.hideWhileSearch">
        {{user.state.username}}
        <span ng-show="user.isTyping">is typing...</span>
    </li>
</ul>

Next Steps

You now have a basic but powerful chat app rich with features! There’s more you can do. Check out our GitHub Repo to see more plugins, supported languages, and other programmatic features powered by ChatEngine.

Language:

Use Cases:

Try PubNub Today