How to Create a Chat App with Angular

Ayooluwa I.
Ayooluwa I.
Published February 28, 2020 Updated February 18, 2022

💡 There’s a newer version of this tutorial! Stream now offers a dedicated Angular Chat SDK, paired with a new official Angular Chat App Tutorial.

The new SDK drastically simplifies the development process described below. You can still skim this post for inspiration, but please refer to the new resources linked above for consistently up-to-date information on developing in-app messaging with Angular.

In this tutorial, I’ll take you through building a live chat application with Angular 9 and Stream Chat. I’ll demonstrate how to work with channels and how to send messages in real-time between users. In addition, you’ll see how to keep track of the number of channels that a user belongs to, and also how to retrieve the message history for a channel.

Here’s how the final messaging application will function:

Simple Real Time Live Chat Example App

Prerequisites

Before you continue on with this tutorial, make sure you have Node.js and npm installed. You also need to have installed the Angular CLI package. If you’ve already installed it, make sure you update to the latest version:

$ npm install -g @angular/cli

Signing Up for Stream

Create a free Stream account or sign in to your existing account. Once you’re logged in, create a new application and grab your app access keys, which we’ll be making use of shortly:

Bootstrapping the Angular App

Run the command below to create a new Angular app with Angular CLI. When prompted to add Angular routing, hit N, and choose CSS as the preferred stylesheet format.

$ ng new chatroom && cd chatroom

Next, install the additional dependencies we’ll be making use of:

$ npm install express cors dotenv body-parser stream-chat axios @types/node

The code>@types/node package is necessary to provide "type" definitions for

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": ["node"]
  },
  "files": ["src/main.ts", "src/polyfills.ts"],
  "include": ["src/**/*.ts"],
  "exclude": ["src/test.ts", "src/**/*.spec.ts"]
}

At this point, you can run ng serve to start the development server. Open http://localhost:4200 in your browser to view the running application.

Setting Up the Server

Create a new server.js file in your project directory and populate it with the following code:

require('dotenv').config();

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const { StreamChat } = require('stream-chat');

const app = express();

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// initialize Stream Chat SDK

const serverSideClient = new StreamChat(
  process.env.STREAM_API_KEY,
  process.env.STREAM_APP_SECRET
);

app.post('/join', async (req, res) => {
  const { username } = req.body;
  const token = serverSideClient.createToken(username);
  try {
    await serverSideClient.updateUser(
      {
        id: username,
        name: username,
      },
      token
    );
  } catch (err) {
    console.log(err);
  }

  const admin = { id: 'admin' };
  const channel = serverSideClient.channel('team', 'talkshop', {
    name: 'Talk Shop',
    created_by: admin,
  });

  try {
    await channel.create();
    await channel.addMembers([username, 'admin']);
  } catch (err) {
    console.log(err);
  }

  return res
    .status(200)
    .json({ user: { username }, token, api_key: process.env.STREAM_API_KEY });
});

const server = app.listen(process.env.PORT || 5500, () => {
  const { port } = server.address();
  console.log(`Server running on PORT ${port}`);
});

Our server contains only one route (/join), which expects a username to be included in the request body. When this happens, an authentication token is generated for the user who will be created on the chat instance (or updated if the user already exists). By default, user tokens are valid indefinitely; you can set an expiration on a token by passing the number of seconds till expiration as the second parameter.

After this, the user is added to a channel of the team type whose ID is set to talkshop and the generated token is sent back to the client to enable user authentication on the frontend. You can learn more about channel types here, including how to create your own, if the defaults don’t work for you.

We need to set up some environmental variables before we can start the server. Create a new .env file in your project root and paste in your Stream credentials, as shown below:

PORT=5500
STREAM_API_KEY=
STREAM_APP_SECRET=

Now, go ahead and start your server by running node server.js to make it available on PORT 5500.

Building the Chat Interface

Let’s create the HTML template and styles for the application. Open up src/app/app.component.html in your text editor and change it to look like this:

<link
  rel="stylesheet"
  href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
  integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
  crossorigin="anonymous"
/>
<link
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"
  type="text/css"
  rel="stylesheet"
/>

<!-- Toolbar -->
<div class="content" role="main">
  <div *ngIf="!channel" class="login">
    <h2 class="title">Login to Chat</h2>
    <form id="login-form" (ngSubmit)="joinChat()">
      <div class="form-group">
        <label for="username">Username</label>
        <input
          type="text"
          class="form-control"
          id="text"
          name="username"
          placeholder="Username"
          [(ngModel)]="username"
        />
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>

  <div *ngIf="channel" class="container">
    <h3 class=" text-center">Stream Messaging</h3>
    <div class="messaging">
      <div class="inbox_msg">
        <div class="inbox_people">
          <div class="headind_srch">
            <div class="channel_heading">
              <h4>Channels</h4>
            </div>
          </div>
          <div class="inbox_chat">
            <div class="channels" *ngFor="let channel of channelList">
              <div class="chat_list">
                <div class="chat_people">
                  <div class="chat_ib">
                    <h5>
                      {{ channel.data.name }}
                    </h5>
                    <p>
                      {{
                        channel.state.messages[
                          channel.state.messages.length - 1
                        ].text
                      }}
                    </p>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="mesgs">
          <div class="msg_history">
            <li class="message" *ngFor="let message of messages">
              <div
                *ngIf="
                  message.user.id !== currentUser.me.id;
                  then incoming_msg;
                  else outgoing_msg
                "
              ></div>
              <ng-template #incoming_msg>
                <div class="incoming_msg">
                  <div class="incoming_msg_img">
                    <img
                      src="https://i.imgur.com/k2PZLZa.png"
                      alt="User avatar"
                    />
                  </div>
                  <div class="received_msg">
                    <div class="received_withd_msg">
                      <p>{{ message.text }}</p>
                    </div>
                  </div>
                </div>
              </ng-template>
              <ng-template #outgoing_msg>
                <div class="outgoing_msg">
                  <div class="sent_msg">
                    <p>{{ message.text }}</p>
                  </div>
                </div>
              </ng-template>
            </li>
          </div>
          <div class="type_msg">
            <form class="input_msg_write" (ngSubmit)="sendMessage()">
              <input
                type="text"
                class="write_msg"
                placeholder="Type a message"
                name="newMessage"
                [(ngModel)]="newMessage"
              />
              <button class="msg_send_btn" type="button">
                <i class="fa fa-paper-plane-o" aria-hidden="true"></i>
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Next, add the styles for the app to src/app/app.component.css:

.login {
	position: absolute;
	width: 100%;
	max-width: 600px;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
}

.container {
	max-width: 1170px;
	margin: auto;
}

img {
	max-width: 100%;
}

.inbox_people {
	background: #f8f8f8 none repeat scroll 0 0;
	float: left;
	overflow: hidden;
	width: 40%;
	border-right: 1px solid #c4c4c4;
}

.inbox_msg {
	border: 1px solid #c4c4c4;
	clear: both;
	overflow: hidden;
}

.top_spac {
	margin: 20px 0 0;
}

.channel_heading {
	float: left;
	width: 40%;
}

.srch_bar {
	display: inline-block;
	text-align: right;
	width: 60%;
}

.headind_srch {
	padding: 10px 29px 10px 20px;
	overflow: hidden;
	border-bottom: 1px solid #c4c4c4;
}

.channel_heading h4 {
	color: #05728f;
	font-size: 21px;
	margin: auto;
}

.srch_bar input {
	border: 1px solid #cdcdcd;
	border-width: 0 0 1px 0;
	width: 80%;
	padding: 2px 0 4px 6px;
	background: none;
}

.srch_bar .input-group-addon button {
	background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
	border: medium none;
	padding: 0;
	color: #707070;
	font-size: 18px;
}

.srch_bar .input-group-addon {
	margin: 0 0 0 -27px;
}

.chat_ib h5 {
	font-size: 15px;
	color: #464646;
	margin: 0 0 8px 0;
}

.chat_ib h5 span {
	font-size: 13px;
	float: right;
}

.chat_ib p {
	font-size: 14px;
	color: #989898;
	margin: auto
}

.chat_img {
	float: left;
	width: 11%;
}

.chat_ib {
	float: left;
	padding: 0 0 0 15px;
	width: 88%;
}

.chat_people {
	overflow: hidden;
	clear: both;
}

.chat_list {
	border-bottom: 1px solid #c4c4c4;
	margin: 0;
	padding: 18px 16px 10px;
}

.inbox_chat {
	height: 550px;
	overflow-y: scroll;
}

.active_chat {
	background: #ebebeb;
}

.incoming_msg_img {
	display: inline-block;
	width: 6%;
}

.received_msg {
	display: inline-block;
	padding: 0 0 0 10px;
	vertical-align: top;
	width: 92%;
}

.received_withd_msg p {
	background: #ebebeb none repeat scroll 0 0;
	border-radius: 3px;
	color: #646464;
	font-size: 14px;
	margin: 0;
	padding: 5px 10px 5px 12px;
	width: 100%;
}

.time_date {
	color: #747474;
	display: block;
	font-size: 12px;
	margin: 8px 0 0;
}

.received_withd_msg {
	width: 57%;
}

.mesgs {
	float: left;
	padding: 30px 15px 0 25px;
	width: 60%;
}

.sent_msg p {
	background: #05728f none repeat scroll 0 0;
	border-radius: 3px;
	font-size: 14px;
	margin: 0;
	color: #fff;
	padding: 5px 10px 5px 12px;
	width: 100%;
}

.outgoing_msg {
	overflow: hidden;
	margin: 26px 0 26px;
}

.sent_msg {
	float: right;
	width: 46%;
}

.input_msg_write input {
	background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
	border: medium none;
	color: #4c4c4c;
	font-size: 15px;
	min-height: 48px;
	width: 100%;
}

.type_msg {
	border-top: 1px solid #c4c4c4;
	position: relative;
}

.msg_send_btn {
	background: #05728f none repeat scroll 0 0;
	border: medium none;
	border-radius: 50%;
	color: #fff;
	cursor: pointer;
	font-size: 17px;
	height: 33px;
	position: absolute;
	right: 0;
	top: 11px;
	width: 33px;
}

.messaging {
	padding: 0 0 50px 0;
}

.msg_history {
	height: 516px;
	overflow-y: auto;
}

.message {
	list-style-type: none;
}

Now, we can go ahead and write the logic for the application. Locate app.module.ts and import FormsModule which exports the required providers and directives for template-driven forms and makes them available in our AppComponent:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Finally, update your app.component.ts file as shown below:

import { Component } from '@angular/core';
import { StreamChat, ChannelData, Message, User } from 'stream-chat';
import axios from 'axios';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  title = 'angular-chat';
  channel: ChannelData;
  username = '';
  messages: Message[] = [];
  newMessage = '';
  channelList: ChannelData[];
  chatClient: any;
  currentUser: User;

  async joinChat() {
    const { username } = this;
    try {
      const response = await axios.post('http://localhost:5500/join', {
        username,
      });
      const { token } = response.data;
      const apiKey = response.data.api_key;

      this.chatClient = new StreamChat(apiKey);

      this.currentUser = await this.chatClient.setUser(
        {
          id: username,
          name: username,
        },
        token
      );

      const channel = this.chatClient.channel('team', 'talkshop');
      await channel.watch();
      this.channel = channel;
      this.messages = channel.state.messages;
      this.channel.on('message.new', event => {
        this.messages = [...this.messages, event.message];
      });

      const filter = {
        type: 'team',
        members: { $in: [`${this.currentUser.me.id}`] },
      };
      const sort = { last_message_at: -1 };

      this.channelList = await this.chatClient.queryChannels(filter, sort, {
        watch: true,
        state: true,
      });
    } catch (err) {
      console.log(err);
      return;
    }
  }

  async sendMessage() {
    if (this.newMessage.trim() === '') {
      return;
    }

    try {
      await this.channel.sendMessage({
        text: this.newMessage,
      });
      this.newMessage = '';
    } catch (err) {
      console.log(err);
    }
  }
}

At this point, a login form should be rendered on the page. It only contains a "username" field, which is enough to demonstrate how the app works, although you’d have a proper authentication flow in a real application.

Once the form is submitted, the joinChat() method is called, which submits the username to the /join route that we set up earlier, and receives a token from the server, which is subsequently used to set the current user.

After this, we connect to the talkshop channel, and listen for new messages on the channel, thanks to Stream’s event capabilities. This allows us to update the UI whenever a new message is sent to the channel. We do this by appending the new message to the messages array, so that the new message is displayed in the chat window.

To render the list of channels that a user belongs to on the sidebar, we use the queryChannels() method, which is documented here. It accepts query filters, which you can use on any of the built-in channel fields or any custom ones you may have defined.

New messages are sent to the room by submitting the form at the bottom of the chat window according to the code in sendMessage(). The text input is immediately cleared by setting this.newMessage to an empty string.

To test the application, open it in separate tabs and login using different usernames. Send a few messages using each user:

Example real-time chat app built with Angular 9 and Stream components

Final thoughts

That concludes this tutorial on how to build a real-time chat application in Angular!

You can now build on the knowledge gained here to create a live chatting solution that solves a real-world messaging problem. You can check out other things Stream Chat can do by viewing its extensive documentation. The complete code for this tutorial can be found in the GitHub repository.

Thanks for reading, and happy coding!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->