There are many ways to build a chat application with Ionic. You could use Firebase as a realtime database, or you can use your own Node server with some Socket.io, and that’s what we gonna do today to build a realtime Ionic Chat!

In this tutorial we will craft a super simple Node.js server and implement Socket.io on the server-side to open realtime connections to the server so we can chat with other participants of a chatroom.

This is a super lightweight example as we don’t store any of the messages on the server – if you are nor in the chat room you will never get what happened. The result will look like in the image below.

chats demo

Nest Application

This tutorial starts with the actual backend for our app. It’s good to have some Node skills so you could potentially build your own little backend from time to time. First of all we create a new NestJs app for our backend:

nest new chat-api && cd chat-api

then install @nestjs/platform-sockets.io and @types/socket.io:

npm i @nestjs/platform-sockets.io
npm i -D @types/sockets.io

All of our functions are wrapped inside a gateway class, which only listen for events once a client connects to the server.

We listen for set-nickname and add-message and whenever our clients sends out these events the server does something.

If our clients send a new message, the server emit that message to everyone connected as a new object with text, the name of the sending user and a date. Also, we set the name of the socket connection if a users send his nickname.

Finally, if a user disconnects, we inform everyone that somebody just left the room. To do all that we need to generate gateways/messages.js:

nest g ga gateways/messages

then open src/gateways/messages/messages.gateway.ts and change it for next code:

import {OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, WebSocketServer} from '@nestjs/websockets';
import { Socket } from 'socket.io';
import {Server} from "net";

@WebSocketGateway({cors: '*:*'})
export class MessagesGateway implements OnGatewayDisconnect {

  nicknames: Map<string, string> = new Map();

  @WebSocketServer()
  server: Server;

  handleDisconnect(client: Socket) { (1)
    this.server.emit('users-changed', {user: this.nicknames[client.id], event: 'left'});
    this.nicknames.delete(client.id);
  }

  @SubscribeMessage('set-nickname') (2)
  setNickname(client: Socket, nickname: string) {
    this.nicknames[client.id] = nickname;
    this.server.emit('users-changed', {user: nickname, event: 'joined'}); (3)
  }

  @SubscribeMessage('add-message') (4)
  addMessage(client: Socket, message) {
    this.server.emit('message', {text: message.text, from: this.nicknames[client.id], created: new Date()});
  }
}
1 set a function for handle disconnect events which is implemented because of OnGatewayDisconnect interface.
2 set subscriber for set-nickname event.
3 use client.server.emit to broadcast value to all clients
4 set subscriber for add-message event.

Your NestJs backend with Socket.io is now ready! You can start it by running the command below and you should be able to reach it at http://localhost:3000

npm start:dev

Ionic Application

Inside our Ionic chat app we need 2 screens: On the first screen we will pick a name and join the chat, on the second screen is the actual chatroom with messages.

First of all we create a blank new Ionic app and install the ng-socket-io package to easily connect to our Socket backend, so go ahead and run:

ionic start chat-client blank --type=angular
cd chat-client

then start the web-app running:

npm run start

once app starts, navigate to: http://localhost:4200

then we need to add ngx-socket-io and rxjs-compat dependencies, so run:

npm i -s ngx-socket-io rxjs-compat

Now setup the socket-io configuration in the environments/environment.ts and environments/environment.prod.ts files changing then to next code to add the backend url:

export const environment = {
  production: false,
  socketIoConfig: { (1)
    url: 'localhost:3000', (2)
    options: {}
  }
};
1 SocketIO configuration
2 backend url

Now make sure to add ng-socket-io package to our src/app/app.module.ts and pass in the socket-io configuration:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule, RouteReuseStrategy, Routes } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { SocketIoModule } from 'ngx-socket-io'; (1)
import { environment } from '../environments/environment'; (2)

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    SocketIoModule.forRoot(environment.socketIoConfig) (3)
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
1 import package ngx-socket-io
2 import environment configuration
3 add SocketIoModule to imports

Your app is now configured to use the backend, so make sure to run the backend when launching your app!

Joining a Chatroom

Before continue adding more pages to the project I just prefer to have all of them inside a pages folder. So that, move src/app/home directory to src/app/pages/home then open src/app/app-routing.module.ts and change path of home page as fallow:

  { path: 'home', loadChildren: () => import('./pages/home/home.module').then(m => m.HomePageModule) },

After that a user needs to pick a name to join a chatroom. This is just an example so we can actually show who wrote which message, so our view consists of the input field and a button to join the chat. Open your src/app/pages/home/home.html and change it to:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-item>
    <ion-label stacked>Set Nickname</ion-label>
    <ion-input type="text" [(ngModel)]="nickname" placeholder="Nickname"></ion-input>
  </ion-item>
  <ion-button full (click)="joinChat()" [disabled]="nickname === ''">Join Chat as {{ nickname }}</ion-button>
</ion-content>

Inside the class for this view we have the first interaction with Socket. Once we click to join a chat, we need to call connect() so the server recognises a new connection. Then we emit the first message to the server which is to set our nickname.

Once both of this happened we push the next page which is our chatroom, go ahead and change your src/app/pages/home/home.ts to:

import { Component } from '@angular/core';
import { Socket } from 'ngx-socket-io';
import { Router } from '@angular/router';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  nickname = '';

  constructor(private router: Router, private socket: Socket) { }

  joinChat() {
    this.socket.connect();
    this.socket.emit('set-nickname', this.nickname);
    this.router.navigateByUrl(`chat-room/${this.nickname}`);
  }
}

If you put in some logs you should now already receive a connection and the event on the server-side, but we haven’t added the actual chat functionality so let’s do this.

Building the Chat functionality

To receive new chat messages inside the room we have to listen for message socket event which receive the messages from the server.

Whenever we get such a message we simply push the new message to an array of messages. Remember, we are not loading historic data, we will only get messages that come after we are connected!

Sending a new message is almost the same like setting a nickname before, we simply emit our event to the server with the right type.

Finally, we also listen to the events of users joining and leaving the room and display a little toast whenever someone comes in or leaves the room. It’s the same logic again, with the socket.on() we can listen to all the events broadcasted from our server!

Go ahead and generate pages/chat-room:

ionic g page pages/chat-room

then modify the path of the chat-room page in chat-room/chat-room-routing.module.ts to:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { ChatRoomPage } from './chat-room.page';

const routes: Routes = [
  {path: ':nickname', component: ChatRoomPage} (1)
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class ChatRoomPageRoutingModule {}
1 The path takes :nickname as parameter

then change your src/app/pages/chat-room/chat-room.page.ts to:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Socket } from 'ngx-socket-io';
import { ToastController } from '@ionic/angular';

@Component({
  selector: 'app-chat-room',
  templateUrl: './chat-room.page.html',
  styleUrls: ['./chat-room.page.scss'],
})
export class ChatRoomPage implements OnInit, OnDestroy {
  messages = [];
  nickname = '';
  message = '';

  constructor(private route: ActivatedRoute,
              private socket: Socket,
              private toastCtrl: ToastController) { }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.nickname = params.nickname;
    });

    this.socket.on('message', message => this.messages.push(message));

    this.socket.on('users-changed', (data) => {
      const user = data['user'];
      if (data['event'] === 'left') {
        this.showToast('User left: ' + user);
      } else {
        this.showToast('User joined: ' + user);
      }
    });
  }
  sendMessage() {
    this.socket.emit('add-message', { text: this.message });
    this.message = '';
  }

  ngOnDestroy() {
    this.socket.disconnect();
  }

  async showToast(msg) {
    const toast = await this.toastCtrl.create({
      message: msg,
      duration: 2000
    });
    toast.present();
  }
}

The last missing part is now the view for the chatroom. We have to iterate over all of our messages and distinguish if the message was from us or another user. Therefore, we create 2 different ion-col blocks as we want our messages to have some offset to a side. We could also do this only with CSS but I like using the Ionic grid for styling as far as possible.

With some additional styling added to both our and other people’s messages the chatroom will look almost like iMessages or any familiar chat application, so open your src/app/pages/chat-room/chat-room.page.html and insert:

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
    <ion-title>
      Chat
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-grid>
    <ion-row *ngFor="let message of messages">

      <ion-col size="9" *ngIf="message.from !== nickname" class="message"
               [ngClass]="{'my_message': message.from === nickname, 'other_message': message.from !== nickname}">
        <span class="user_name">{{ message.from }}:</span><br>
        <span>{{ message.text }}</span>
        <div class="time">{{message.created | date:'dd.MM hh:MM'}}</div>
      </ion-col>

      <ion-col offset="3" size="9" *ngIf="message.from === nickname" class="message"
               [ngClass]="{'my_message': message.from === nickname, 'other_message': message.from !== nickname}">
        <span class="user_name">{{ message.from }}:</span><br>
        <span>{{ message.text }}</span>
        <div class="time">{{message.created | date:'dd.MM hh:MM'}}</div>
      </ion-col>

    </ion-row>
  </ion-grid>

</ion-content>

<ion-footer>
  <ion-toolbar>
    <ion-row class="message_row">
      <ion-col size="9">
        <ion-item no-lines>
          <ion-input type="text" placeholder="Message" [(ngModel)]="message"></ion-input>
        </ion-item>
      </ion-col>
      <ion-col size="3">
        <ion-button clear color="primary" (click)="sendMessage()" [disabled]="message === ''">
          Send
        </ion-button>
      </ion-col>
    </ion-row>
  </ion-toolbar>
</ion-footer>

Below our messages we also have the footer bar which holds another input to send out messages, nothing really fancy.

To make the chat finally look like a chat, add some more CSS to your src/app/pages/chat-room/chat-room.page.scss:

.message {
  padding: 10px !important;
  transition: all 250ms ease-in-out !important;
  border-radius: 10px !important;
  margin-bottom: 4px !important;
}
.my_message {
  background-color: var(--ion-color-primary) !important;
  color: var(--ion-color-primary-contrast) !important;

  .user_name, .time {
    color: var(--ion-color-light-shade);
  }
}
.other_message {
  background-color: var(--ion-color-light-shade) !important;
  color: var(--ion-color-light-contrast) !important;

  .user_name, .time {
    color: var(--ion-color-dark-tint);
  }
}
.time {
  float: right;
  font-size: small;
}
.message_row {
  background-color: #fff;
}

Now launch your app and make sure your backend is up and running!

For testing, you can open a browser and another incognito browser like in my example at the top to chat with yourself.

Conclusion

Don’t be scared of Socket.io and a NestJs backend, you can easily implement your own realtime backend connection without any problems! Firebase seems often like the easy alternative, but actually we were able to build a live chat app with only a super small backend.

To make this a real Ionic chat you might want to add a database and store all the messages once you receive them and add some routes to return the history of a chatroom. Or you might want to create different chatrooms and separate conversations, but this post could be the starting point to your own chat implementation!

Comments