Build a Chat Application with Angular Material and Vonage
Published on November 6, 2020

In this tutorial, we'll enable chat in an Angular web application using the JavaScript SDK and the In-App API so that users can communicate in our application. If you'd like to check out the source code, it lives on our community GitHub page.

This is what we're trying to build:

Gif showing the end goal of this chat application"Gif showing the end goal of this chat application"

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Before You Begin

Before we begin you’ll need a few things:

  • A basic understanding of Angular

  • Node.js installed on your machine.

  • The middleware code from Github

  • The Nexmo CLI. Install it as follows:

npm install -g nexmo-cli@beta

Setup the CLI to use your Vonage API Key and API Secret:

nexmo setup api_key api_secret

Getting the middleware code from Github

First, we’re going to clone the middleware source code and install the dependencies for it. We're going to write a Node.js application using Express that provides a level of abstraction between the Vonage API and the Angular code:

git clone https://github.com/Nexmo/stitch-demo.git cd stitch-demo/ npm install

Running the middleware code from Github

Before we can run the code, we'll need to create an RTC application within the Vonage platform to use within this code:

nexmo app:create "My Conversation App" https://example.com/answer https://example.com/event --type=rtc --keyfile=private.key

The output of the above command will be something like this:

Application created: aaaaaaaa-bbbb-cccc-dddd-0123456789ab No existing config found. Writing to new file. Credentials written to /path/to/your/local/folder/.nexmo-app Private Key saved to: private.key

The first item is the Application ID, which you should take note of (we'll refer to this as APP_ID later). The last value is a private key location. The private key is used to generate JWTs that are used to authenticate your interactions with Vonage.

Now we'll need to make a copy of example.env and call that .env, and update the values within your Vonage API_KEY and API_SECRET. We'll also have to add the APP_ID we just generated and the path to your private key. After we've updated the values, we can run the code in debug mode with:

npm run debug

Create Users and Conversations

The app should be running on localhost:3000. Now that the app is running, we're going to go ahead and create a couple of users and a conversation, and then we'll add the users we created to the conversation.

We'll create a couple of users by running this command twice, once with the username alice and then with jamie:

curl --request POST \ --url http://localhost:3000/api/users \ --header 'content-type: application/json' \ --data '{ "username": "alice", "admin": true }'

The ouput should look similar to:

{"user":{"id":"USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab","href":"http://conversation.local/v1/users/USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab"},"user_jwt":"USER_JWT"}

We'll make a note of the user ID and refer to it later on as USER_ID. Now let's create a conversation via the demo API:

curl --request POST \ --url http://localhost:3000/api/conversations \ --header 'content-type: application/json' \ --data '{"displayName": "My Chat"}'

The ouput should look similar to:

{"id":"CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab","href":"http://conversation.local/v1/conversations/CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab"}

We'll make a note of the conversation ID and refer to it later on as CONVERSATION_ID. Now let's join the users to the conversation. We're going to run the following command twice—remember to replace the CONVERSATION_ID and USER_ID with IDs from the two previous steps every time you run this command:

curl --request PUT \ --url http://localhost:3000/api/conversations \ --header 'content-type: application/json' \ --data '{ "conversationId": "CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab", "userId": "USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab", "action": "join" }'

Generate Angular App

Now that we have our middleware up and running, it's time we created the Angular application. We are going to use the Angular CLI to generate the app, so if you don’t have it installed, you’ll need to install that first:

npm install -g @angular/cli

And then we'll use it to generate a new application with routing. It may take a while for it to generate all the files and install the dependencies:

ng new nexmo-stitch-angular --routing

Add Material Design

After the previous command finished, we'll add Angular Material and its dependencies to the project:

npm install --save @angular/material @angular/cdk @angular/animations

We'll also have to import the NgModule for each component we want to use in our application. In order to do that, we need to open src/app/app.module.ts in our editor and import the modules at the top:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import {
  MatAutocompleteModule,
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatCheckboxModule,
  MatChipsModule,
  MatDatepickerModule,
  MatDialogModule,
  MatExpansionModule,
  MatGridListModule,
  MatIconModule,
  MatInputModule,
  MatListModule,
  MatMenuModule,
  MatNativeDateModule,
  MatPaginatorModule,
  MatProgressBarModule,
  MatProgressSpinnerModule,
  MatRadioModule,
  MatRippleModule,
  MatSelectModule,
  MatSidenavModule,
  MatSliderModule,
  MatSlideToggleModule,
  MatSnackBarModule,
  MatSortModule,
  MatTableModule,
  MatTabsModule,
  MatToolbarModule,
  MatTooltipModule,
  MatStepperModule
} from '@angular/material';

We'll also need to update the @NgModules imports declaration to add the modules from above:

@NgModule({
  ...
  imports: [
    imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    FormsModule,
    HttpClientModule,
    MatTabsModule,
    MatCardModule,
    MatGridListModule,
    MatButtonModule,
    MatInputModule,
    MatListModule,
    MatIconModule,
    MatSidenavModule,
    MatProgressSpinnerModule,
    MatTooltipModule,
    MatDialogModule
  ],
  ],
  ...
})

We'll also want to add a theme to Material, so add this line to your style.css:

@import "~@angular/material/prebuilt-themes/indigo-pink.css";

If we want to use the official Material Design Icons, we'll need to load the icon font in our index.html:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Polyfill Vonage Client SDK for JavaScript

Now that we've got our Angular application generated and all set up with Material, we'll install the Vonage Client SDK for JavaScript and add it to the bottom of polyfills.ts:

npm install --save nexmo-conversation
/**********************************************************
 * APPLICATION IMPORTS
 */
import 'nexmo-conversation';

Messaging Service

We'll need to create an Angular Service to handle the data from our middleware. Let's generate it using the Angular CLI:

ng g service messaging

Two new files were generated in our src/app folder, messaging.service.spec.ts and messaging.service.spec.ts. We're going to update the messaging.service.spec.ts in order to add the ConversationClient from the Vonage In-App SDK, instantiate a client, and handle getting a user JSON Web Token from the middleware. We're going to replace the boilerplate code with:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

declare var ConversationClient: any;
const GATEWAY_URL = "http://localhost:3000/api/";

@Injectable()
export class MessagingService {


  constructor(private http: HttpClient) {

  }

  initialize() {
    this.client = new ConversationClient(
      {
        debug: false
      }
    )
  }

  public client: any
  public app: any


  public getUserJwt(username: string): Promise<any> {
    return this.http.get(GATEWAY_URL + "jwt/" + username + "?admin=true").toPromise().then((response: any) =&gt; response.user_jwt)
  }
}
</any>

We need to update app.module.ts in order to import the MessagingService and register it as a provider:

import { MessagingService } from './messaging.service';

...
    providers: [MessagingService],
...

Login Component

Let's start building some UI for our app. We'll start with a LoginComponent, and we'll generate that with the Angular CLI:

ng g component login

That will generate a login folder inside the app folder, and four files, for the HTML, CSS, and TypeScript code, as well as tests. Let's replace the code in login.component.html with a UI for logging in. I chose a <mat-grid-list> with a <mat-card> inside of it, and a form with a login button that calls onLogin() when it's submitted. The code looks like this:

<mat-grid-list cols="4" rowheight="100px">
  <mat-grid-tile colspan="1" rowspan="5"></mat-grid-tile>
  <mat-grid-tile colspan="2" rowspan="1"></mat-grid-tile>
  <mat-grid-tile colspan="1" rowspan="5"></mat-grid-tile>
  <mat-grid-tile colspan="2" rowspan="3">
    <mat-card class="mat-typography login">
      <h1>Login</h1>
      <form (ngsubmit)="onLogin()">
        <mat-form-field class="full-width">
          <input matinput="" placeholder="Username" name="username" [(ngmodel)]="username">
        </mat-form-field>
        <br>
        <button type="submit" mat-raised-button="" color="primary" (click)="showSpinner = !showSpinner">Login
          <mat-spinner color="accent" mode="indeterminate" *ngif="showSpinner"></mat-spinner>
        </button>
      </form>
    </mat-card>
  </mat-grid-tile>
</mat-grid-list>

Let's add some CSS to it for the Material spinner, in the login.component.css file:

.login {
    text-align: center;
}

.mat-spinner {
    width: 20px !important;
    height: 20px !important;
    display: inline-block;
    margin-left: 10px;
}

/deep/ .mat-spinner svg{
    width: 20px !important;
    height: 20px !important;
}

We also need to update login.component.ts in order to add the MessagingService to it and implement the onLogin() method. The method is going to take the username, make a request via the messaging service to the middleware we are running in order to get a user JWT, and then use that to authenticate via the login method of the Vonage In-App JavaScript SDK. The code looks like this:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

import { MessagingService } from '../messaging.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  username: string = ""

  constructor(private ms: MessagingService, private router: Router) { }

  ngOnInit() {
    this.ms.initialize()
  }

  onLogin() {
    this.ms.getUserJwt(this.username).then(this.authenticate.bind(this))
  }

  authenticate(userJwt: string) {
    this.ms.client.login(userJwt).then(app =&gt; {
      this.ms.app = app
      this.router.navigate(['/conversation']);
    })
  }
}

Notice that I've imported the Router from Angular, injected it into our constructor and I'm using it at the end of the authentication flow to navigate to the next page, /conversation.

Conversation Component

We haven't actually created the conversation component yet, so let's go ahead and use the Angular CLI to create that:

ng g component conversation

We're going to update the conversation.component.html file to use a material sidenav component, which lists the user conversations on the left and the conversation members on the right, leaving the middle for our main chat. We'll add a header to the chat section to list the conversation name and member count, and add an input section at the bottom. We'll leave the middle section for the actual conversation history to be displayed. We'll build an empty shell for now and add to it later as we develop the ConversationComponent. The HMTL should look like this:

<mat-sidenav-container class="container">
  <mat-sidenav mode="side" opened="">
    <mat-card>
      <mat-tab-group>
        <mat-tab>
          <ng-template mat-tab-label="">
            <mat-icon matlisticon="">forum</mat-icon>
          </ng-template>
          <mat-list class="conversations">
              ...
          </mat-list>
        </mat-tab>
      </mat-tab-group>
    </mat-card>
  </mat-sidenav>
  <mat-sidenav position="end" mode="side" opened="" *ngif="selectedConversation">
    <mat-card>
      <mat-list class="members">
          ...
      </mat-list>
    </mat-card>
  </mat-sidenav>
  <section class="empty-conversation" *ngif="!selectedConversation">
    <h1 class="mat-display-1">Select a conversation from the left to start chatting</h1>
  </section>
  <section *ngif="selectedConversation">
    <div class="mat-typography conversation-header">
        ...
    </div>
    <mat-divider></mat-divider>
    <mat-list dense="" class="conversation-history mat-typography">
      ...
    </mat-list>
    <div class="conversation-input">
      <mat-divider></mat-divider>
      <mat-form-field class="full-width">
        <input matinput="" placeholder="Start chatting..." name="text" [(ngmodel)]="text">
        <mat-icon matsuffix="" (click)="">send</mat-icon>
      </mat-form-field>
    </div>
  </section>
</mat-sidenav-container>

We're going to update the conversation.component.ts file with the necessary boilerplate for methods we're going to use later on:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';

import { MessagingService } from '../messaging.service';

@Component({
  selector: 'app-conversation',
  templateUrl: './conversation.component.html',
  styleUrls: ['./conversation.component.css']
})
export class ConversationComponent implements OnInit {

  constructor(private ms: MessagingService, private router: Router) { }

  buildConversationsArray(conversations) {
  }

  ngOnInit() {
  }

  selectConversation(conversationId: string) {
  }

  sendText(text: string) {
  }

  conversations: any
  selectedConversation: any
  text: string
  events: Array<any> = []
}
</any>

Let's start by implementing ngOnInit() so that it checks if we have app data before trying to getConversations using the In-App SDK. If there is no app data, then we'll get redirected to the login screen:

ngOnInit() {
    if (!this.ms.app) {
      this.router.navigate(['/']);
    } else {
      this.ms.app.getConversations().then(conversations =&gt; {
        this.conversations = this.buildConversationsArray(conversations)
      })
    }
  }

We need to implement a helper method for building a conversations array out of the conversations dictionary the In-App JavaScript SDK provides, so we can use it with *ngFor in the UI later on:

buildConversationsArray(conversations) {
    let array = [];

    for (let conversation in conversations) {
      array.push(conversations[conversation]);
    }

    return array
  }

Let's add a method for selecting a conversation from the list. We'll need to take the ID from the view and pass it on to the controller, and then use the Vonage SDK to get data about the conversation. We'll store this in a class property, so it's available to the view later on. We're also using Observable to create an array from the conversation.events Map, so we can recreate chat history when the user comes back to the app. We'll also add an event listener using the SDK to listen for text events and add those to the events history as well:

selectConversation(conversationId: string) {
    this.ms.app.getConversation(conversationId).then(conversation =&gt; {
      this.selectedConversation = conversation

      Observable.from(conversation.events.values()).subscribe(
        event =&gt; {
          this.events.push(event)
        }
      )

      this.selectedConversation.on("text", (sender, message) =&gt; {
        this.events.push(message)
      })

      console.log("Selected Conversation", this.selectedConversation)
    }
    )
  }

Last but not least, let's add a method that takes the input from the view and sends it to the Vonage In-App API via the SDK:

sendText(text: string) {
    this.selectedConversation.sendText(text).then(() =&gt; this.text = "")
  }

Now that we've implemented all the methods we need, we can go back and start to flesh out the view some more, to use the data models we created in the controller. First, let's update the conversations section in conversation.component.html:

...
          <mat-list class="conversations">
            <mat-list-item *ngfor="let conversation of conversations" (click)="selectConversation(conversation.id)">
              <mat-icon matlisticon="">forum</mat-icon>
              <p>{{conversation.display_name}}</p>
            </mat-list-item>
          </mat-list>
...

Now let's add the members section:

...
      <mat-list class="members">
        <mat-list-item *ngfor="let member of selectedConversation.members | keys">
          <p>{{member.value.user.name}}</p>
        </mat-list-item>
      </mat-list>
...

We're using a pipe called keys here to transform the members dictionary object we get from the SDK into an array, so we'll need to create that using the Angular CLI and update the generated keys.pipe.ts file:

ng g pipe keys
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'keys'
})
export class KeysPipe implements PipeTransform {

  transform(value, args:string[]) : any {
    let keys = [];
    for (let key in value) {
      keys.push({key: key, value: value[key]});
    }
    return keys;
  }
}

Next, we'll update the conversation-header section of the view to display the selected conversation name and member count:

...
    <div class="mat-typography conversation-header">
      <h2>
        <mat-icon>forum</mat-icon>
        {{selectedConversation.display_name}}</h2>
      <p>
        <mat-icon>account_circle</mat-icon>
        {{(selectedConversation.members | keys).length}} Members</p>
    </div>
...

We also need to update the conversation-history section in order to parse the events and recreate history in the chat. Events coming from the In-App SDK have multiple types, so we'll account for some of them, like member:joined and text:

...
    <mat-list dense="" class="conversation-history mat-typography">
      <mat-list-item *ngfor="let event of events; index as i" [dir]="event.from === selectedConversation.me.id ? 'rtl' : 'ltr'">
        <img *ngif="event.type == 'text'" matlistavatar="" mattooltip="{{selectedConversation.members[event.from].user.name}}" src="https://randomuser.me/api/portraits/thumb/lego/{{i}}.jpg">
        <p *ngif="event.type == 'text'" [dir]="'ltr'">{{event.body.text}}</p>
        <p *ngif="event.type == 'member:joined'" class="text-center">
          <b>{{selectedConversation.members[event.from].user.name}}</b> has joined the conversation</p>
      </mat-list-item>
    </mat-list>
...

We'll need to update the conversation-input part in order to be able to send messages into the conversation:

...
    <div class="conversation-input">
      <mat-divider></mat-divider>
      <mat-form-field class="full-width">
        <input matinput="" placeholder="Start chatting..." name="text" [(ngmodel)]="text">
        <mat-icon matsuffix="" (click)="sendText(text)">send</mat-icon>
      </mat-form-field>
    </div>
...

Let's add some CSS to it to make it full screen and fixed position. Add the following CSS to conversation.component.css:

.container {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
}

.mat-drawer.mat-drawer-side {
    padding: 0 5px;
}

.empty-conversation {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
}

.conversation-header h2, .conversation-header p {
    align-items: center;
    display: flex;
}

.text-center {
    text-align: center;
    width: 100%;
}

.conversation-history.mat-list {
    height: calc(100% - 180px);
    overflow-x: scroll;
    position: absolute;
    width: 100%;
}

.conversation-history.mat-list p {
    margin: 0;
}

.empty-conversation h1 {
    margin: 0;
}

.conversations .mat-list-item {
    cursor: pointer;
}

.mat-card {
    height: 100%;
    padding: 0 24px;
    overflow: scroll;
}
.conversation-input {
    position: absolute;
    bottom: 0;
    width: 100%;
    background-color: #fafafa;
}

section .mat-list .mat-list-avatar{
    width: 25px;
    height: 25px;
}

.mat-list-avatar {
    margin: 0 5px;
}

.right {
    text-align: right;
}

.full-width {
    width: 100%;
}

.full-width .mat-icon {
    cursor: pointer;
}

Last but not least, we need to update the app routing module in app-routing.module.ts in order to have the correct routes displayed:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ConversationComponent } from './conversation/conversation.component';


const routes: Routes = [
    {
        path: '',
        component: LoginComponent,
    },
    {
      path: 'conversation',
      component: ConversationComponent,
  }
];

@NgModule({
    imports: [
        RouterModule.forRoot(routes)
    ],
    exports: [
        RouterModule
    ],
    declarations: []
})
export class AppRoutingModule { }

We also need to replace the entire app.component.html with the <router-outlet> in order to display the router on the first page:

<router-outlet></router-outlet>

Run Your App!

After making the app detailed in this post, run the app to see it working:

ng serve

The app will run at "http://localhost:4200". I'd suggest opening the app in two separate tabs, logging in with both alice and jamie and start talking to each other! If you'd like to see the app in its final state, you can check out the source code for this app on our community GitHub page. If you want to see a more advanced version of this code, you can check the stitch-demo middleware code you downloaded at the beginning of the blog post. It also contains an Angular Material front-end.

What's Next?

If you'd like to continue learning how to use the Vonage Client SDK for JavaScript, check out our quickstarts where we show you how to:

If you have more questions about using In-App SDK, we encourage you to join the Vonage Developer Community Slack and check out our #nexmo-client-sdk channel or email us directly at ea-support@nexmo.com.

Alex LakatosVonage Alumni

Alex Lakatos is a JavaScript Developer Advocate for Nexmo. In his spare time he volunteers at Mozilla as a Tech Speaker and a Reps Mentor. JavaScript developer building on the open web, he has been pushing its boundaries every day. When he’s not programming in London, he likes to travel the world, so it’s likely you’ll bump into him in an airport lounge.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.

Subscribe to Our Developer Newsletter

Subscribe to our monthly newsletter to receive our latest updates on tutorials, releases, and events. No spam.