Ionic Socket Chat
Some time ago, I started played around with Ionic. The framework itself was great, but I really disliked Angular. Since then, both Angular and the Ionic framework have come a long way. Now, I find myself needing to use Ionic at work, so I decided to dive in. The ease of use makes it ideal for fast mobile application development. I mean, like really fast. The UI components are easy to use with minimal configuration. I decided to build another real-time chat app, this time leveraging Ionic for the client.
I'm assuming you have some knowledge of Node, npm, etc. Before we get started, go ahead and install Ionic and cordova: npm install -g cordova ionic
.
The Server
The first thing we'll be building is our backend. For this app, we're keeping it simple. Messages are not being stored at all, and we're not authenticating users at all. We're just implementing some basic socket functions.
We'll start with making a folder for the project and the server, and creating a basic package.json
:
mkdir ionic-socket-chat && cd ionic-socket-chat
mkdir server && cd server
npm init -y
Next, in the server
folder, we'll install our dependencies, Express and Socket.io:
npm install --save express socket.io
# --save can be omitted as of npm version 5
Now, we can create the file index.js
. Here we'll create a simple Express server. All of our functionality will happen inside of the io.on('connection')
block.
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
io.on('connection', socket => {
// All functions will go here
});
const port = process.env.PORT || 3001;
http.listen(port, function() {
console.log(`Listening at http://localhost:${ port }`);
});
The final thing to do for the server, is to create functions to handle the events set-nickname
, add-message
, and disconnect
. This means our Ionic app will emit these events, and our server will then do something. Let's add those now:
...
io.on('connection', socket => {
socket.on('set-nickname', nickname => {
if (nickname) {
socket.nickname = nickname;
console.log(`${ socket.nickname } joined`);
io.emit('users-changed', { user: nickname, event: 'joined' });
}
});
socket.on('add-message', message => {
console.log(`${ socket.nickname } sent: ${ message.text }`);
io.emit('message', { text: message.text, from: socket.nickname, created: new Date() });
});
socket.on('disconnect', function() {
if (socket.nickname) {
console.log(`${ socket.nickname } left`);
io.emit('users-changed', { user: socket.nickname, event: 'left' });
}
});
});
...
That's all for the server. You can start it by running node index
from inside the server
folder.
The Client
As mentioned, the client we're building is going to be an Ionic app. From the project folder (ionic-socket-chat
) run the following commands:
ionic start client blank
cd client
npm install ng-socket-io --save
ionic g page chatRoom
This creates a new Ionic project called 'client' using the 'blank' template, then installs Socket.io for use with Angluar (used by Ionic), and generates a new Ionic page, chatRoom
.
Next we need to add Socket.io to src/app/app.module.ts
, and configure it to use our backend. It should look something like this:
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { SocketIoModule, SocketIoConfig } from 'ng-socket-io';
const config: SocketIoConfig = { url: 'http://localhost:3001', options: {} };
@NgModule({
declarations: [
MyApp,
HomePage
],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
SocketIoModule.forRoot(config),
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
HomePage
],
providers: [
StatusBar,
SplashScreen,
{provide: ErrorHandler, useClass: IonicErrorHandler}
]
})
export class AppModule {}
Join the Chat
First, our user needs a name to join our chat. This view will contain an input field and a button to join. Change src/pages/home/home.html
to this:
<ion-header>
<ion-navbar>
<ion-title>
Join Chat
</ion-title>
</ion-navbar>
</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>
<button ion-button full (click)="joinChat()" [disabled]="nickname === ''">Join as {{ nickname }}</button>
</ion-content>
Next, we'll connect our home
class, we'll set up out first Socket interaction, so the server knows there's a new connection. To do this, we need to add the joinChat()
method to src/pages/home/home.ts
:
...
nickname = '';
constructor(public navCtrl: NavController, private socket: Socket) { }
joinChat() {
this.socket.connect();
this.socket.emit('set-nickname', this.nickname);
this.navCtrl.push('ChatRoomPage', { nickname: this.nickname });
}
...
At this point, our client is making a connection with the server! Now, we can add the actual chat functionality.
Adding the Chat Functionality
When the Chat Room page is loaded, it is passed the nickname we set on the Home page. We need to capture that parameter and this.nickname
to the value provided. In case this page is reloaded, and therefore doesn't have a nickname, we'll also check to make sure the nickname is set, and redirect to the home page if it's not. Update the constructor()
method to look like this:
...
nickname = '';
constructor(private navCtrl: NavController, private navParams: NavParams, private socket: Socket, private toastCtrl: ToastController) {
this.nickname = this.navParams.get('nickname');
if (!this.nickname) {
this.navCtrl.setRoot('HomePage');
}
}
...
To receive messages, we'll create the function getMessages()
. This will return an observable. It will also tell Socket to listen for message
events, and call next()
on the observable to pass the new value through to the page. Whenever we receive a new message, we will push it to an array of messages. As mentioned, we're not storing messages at all. That also means we are not loading historical data. We're only loading messages that are created after we're connected. Add this method to src/pages/chat-room/chat-room.ts
:
...
getMessages() {
let observable = new Observable(observer => {
this.socket.on('message', (data) => {
observer.next(data);
});
});
return observable;
}
...
To make use of the observable, we'll create the this.messages
array, then call this.getMessages()
in our constructor()
method:
...
messages = [];
constructor(private navCtrl: NavController, private navParams: NavParams, private socket: Socket, private toastCtrl: ToastController) {
...
this.getMessages().subscribe(message => {
this.messages.push(message);
});
}
...
Now, when the server emits the message
event, we receive the new message, and push it to our this.messages
array. Of course, this can't happen yet, as our app has no way to send messages. For that, we'll create the method sendMessage()
:
...
message = '';
...
sendMessage() {
this.socket.emit('add-message', { text: this.message });
this.message = '';
}
...
We also want our app to notify us when users join or leave the chat room. Like messages, we'll have a function return an observable for users, then show a "toast" when the users-changed
event is fired.
constructor(private navCtrl: NavController, private navParams: NavParams, private socket: Socket, private toastCtrl: ToastController) {
...
this.getUsers().subscribe(data => {
let user = data['user'];
if (data['event'] === 'left') {
this.showToast('User left: ' + user);
} else {
this.showToast('User joined: ' + user);
}
});
}
getUsers() {
let observable = new Observable(observer => {
this.socket.on('users-changed', (data) => {
observer.next(data);
});
});
return observable;
}
showToast() {
let toast = this.toastCtrl.create({
message: msg,
duration: 2000,
position: 'top',
});
toast.present();
}
That's it for the class. Now, we will create our view. Open src/pages/chat-room/chat-room.html
and change it to the following:
<ion-header>
<ion-navbar>
<ion-title>Chat</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-grid>
<ion-row *ngFor="let message of messages">
<ion-col col-9 class="message" [ngClass]="message.from === nickname ? 'message--mine' : 'message--other'">
<div class="time">{{message.created | date:'MMM. dd @ hh:MM'}}</div>
<span class="user_name">{{ message.from }}:</span><br>
<span>{{ message.text }}</span>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-row class="message_row">
<ion-col col-9>
<ion-item no-lines>
<ion-input type="text" placeholder="Message" [(ngModel)]="message"></ion-input>
</ion-item>
</ion-col>
<ion-col col-3>
<button ion-button clear color="primary" (click)="sendMessage()" [disabled]="message === ''">
Send
</button>
</ion-col>
</ion-row>
</ion-toolbar>
</ion-footer>
This is a pretty straight forward Ionic view. We have our ion-header
which has a navbar with our title. Because we push()
this view, the navbar will have a "Back" button added by Ionic. In ion-content
we output each of the message in our messages
array, using *ngFor
on our ion-row
. We are also conditionally setting the class on ion-col
based on whether the message is our own, or from another user. Finally, ion-footer
and ion-toolbar
hold out message input and the "Send" button. You can see that we're calling sendMessage()
when the button is clicked.
Style It
For the most part, Ionic UI elements are styled exactly how we want/need for our app. In fact, the Home Page view isn't getting any additional styling. The chat room, however, needs just a little bit of styling to achieve the look we're after. Open up src/pages/chat-room/chat-room.scss
and replace the contents with the following:
page-chat-room {
.user_name {
color: #2a2a2a;
}
.message {
padding: 10px !important;
transition: all 250ms ease-in-out !important;
border-radius: 10px !important;
margin-top: 18px !important;
margin-bottom: 4px !important;
}
.message--mine {
margin-left: 25%;
background: color($colors, primary) !important;
color: #fff !important;
.time {
right: 0;
}
}
.message--other {
background: #dcdcdc !important;
color: #000 !important;
.time {
left: 0;
}
}
.time {
color: #afafaf;
position: absolute;
top: -18px;
font-size: small;
}
.message_row {
background-color: #fff;
}
}
Now, our app should look something like this:
Wrap up
That's it! At this point you should have a functioning application. You'll need two terminal windows to run it. In one, go to ~/ionic-socket-chat/server
and run node index.js
. In the other, go to ~/ionic-socket-chat/client
and run ionic serve
. Your browser will open to the app automatically. To really test the app, open an Incognito window and point it to the app at http://localhost:8100/. In each window, type a nickname and chat with yourself.
If you want to run the app on different computers or devices, you'll need to change the Socket config in src/app/app.module.ts
to use your IP address rather than localhost
.