Creating a realtime chat Progressive Web App with Ionic and socket.io - part 2

February 28, 2018, 11:30 pm Categories:

Categories

What we'll be developing

We're going to build on the ionic-communicata application that we created in the previous tutorial to publish a fully functional real-time chat application using Socket.io (a node based library that enables real-time event communication).

We'll begin by developing the node server functionality for facilitating realtime event communication before moving onto developing the Ionic application codebase.

By the end of this second tutorial (in our three-part series) you should have a fully developed chat application like the one displayed below (your content will, of course, vary from mine - unless you happen to enjoy reliving certain characters from 1980's comic culture!)

Ionic real-time chat application in use

It's worth mentioning from the outset that each chat session will NOT be saved, stored or able to be recalled in any way, shape or form whatsoever. This means that once a user has exited the chat session it is NOT able to be recalled and reloaded.

In this time of heightened security surveillance and intelligence agency spying this might not be a bad thing!

That said some of the more eagle-eyed amongst you might have noticed, from the above GIF animation, the export button in the 'logged-in' state of the chat application.

I've added this feature so that chat messages ARE able to be exported for subsequent storage but we'll get to this later on in the tutorial.

Be aware that the following assumes you have the latest version of Ionic CLI installed on your desktop machine along with node and npm.

Ready to get started?

Configuring the node server

We're going to start by configuring the node functionality so, within your system CLI, navigate to a preferred directory on your machine and run the following command to generate the package.json file for the project:

npm init

Follow the terminal prompts asking you to complete information by supplying the following values for each field(shown in brackets):

  • ionic-communicata (package name)
  • 1.0.0 (version)
  • A chat application using Ionic and Socket.io (description)
  • index.js (entry point)
  • test (test command)
  • Leave blank for git repository
  • Leave blank for keywords
  • Your name/organisation (author)
  • (ISC) (License)

Answer Yes for the Is this ok? prompt and then install the following npm packages:

npm install --save express
npm install --save socket.io

Once completed your package.json file should look similar to the following (albeit with minor detail changes for the author field):

{
  "name": "ionic-communicata",
  "version": "1.0.0",
  "description": "A chat application using Ionic and Socket.io",
  "main": "index.js",
  "scripts": {
    "test": "test"
  },
  "author": "Saints at Play Limited",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.2",
    "socket.io": "^2.0.4"
  }
}

With the required npm packages installed we can go ahead and start scripting the socket functionality for the index.js file.

Following the official Socket.io documentation we begin by structuring the index.js file to listen for particular Socket.io events and then handle how messages/events are broadcast to and from the Ionic application like so:

var port    = process.env.PORT || 3000,
    app 	= require('express')(),
	http 	= require('http').Server(app),
	io 		= require('socket.io')(http);



// Allow CORS support and remote requests to the service
app.use(function(req, res, next)
{
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type,Authorization');
    next();
});


// Set up route 
app.get('/', (req, res) =>
{
   res.json({ message: 'hello world' });
});


// Manage the Socket server event listener methods and 
// how realtime chat messages are handled/broadcast
io.on('connection', (socket) =>
{
   
   /* Set up a disconnect event*/
   socket.on('disconnect', ()=>
   {
      // Broadcast the event and return a JavaScript map of values 
      // for use within the Ionic app
      io.emit('user-exited', { user: socket.alias });
   });



   /** 
    * Listen for when a message has been sent from the Ionic app
    */
   socket.on('add-message', (message)=>
   {
      // Broadcast the message and return a JavaScript map of values 
      // for use within the Ionic app
   	  io.emit('message', { message: message.message, sender: socket.alias, tagline: socket.handle, location: socket.location, published: new Date() });
   });



   /** 
    * Listen for when an image has been sent from the Ionic app
    */
   socket.on('add-image', (message)=>
   {
      // Broadcast the message and return a JavaScript map of values 
      // for use within the Ionic app
   	  io.emit('message', { image: message.image, sender: socket.alias, tagline: socket.handle, location: socket.location, published: new Date() });
   });



   /** 
    * Allows the user to join the current chat session
    */
   socket.on('set-alias', (obj)=>
   {
      // Define socket object properties (which we can use with our other
      // Socket.io event listener methods) and return a JavaScript map of 
      // values for use within the Ionic app
   	  socket.alias 		= obj.alias;
   	  socket.handle 	= obj.handle;
   	  socket.location 	= obj.location;
      io.emit('alias-added', { user: obj.alias, tagline: obj.handle, location: obj.location });
   });


});


// Instruct node to run the socket server on the following port
http.listen(port, function()
{
  console.log('listening on port ' + port);
});

It's worth repeating but DO remember that we are providing functionality for non-permanent chat sessions here. This means that our user will NOT be able to pick up from a previous chat they may have been engaged in.

Once the chat is over - it really is over!

There is one consideration we need to bear in mind - in order for the chat server to broadcast and receive messages from our Ionic application (which we'll develop shortly) we need to make sure it's actually running!

One of those minor details I know but kind of important nonetheless.

Simply execute the following command to run the Socket.io chat server on port 3000:

node index.js

Now you're up and running!

With the requirements for the node server covered in full let's now turn our attention to adding the necessary configurations and functionality for the ionic-communicata codebase.

Generating services

The ionic-communicata chat application will make use of the following functionality:

  • Allowing the selection of image files and transferring/rendering these as base64 data uris
  • Using the Socket.io library to manage real-time event communication

It's worth bearing in mind that as we're going to be working with base64 data uris we might experience a 'lag' with data being posted to the node server before being displayed within the chat window.

With this in consideration it's probably wise to have some sort of 'preloading' functionality so that users aren't greeted with momentary blank screens and delayed image rendering.

Using the Ionic CLI go ahead and create the following services - which we'll subsequently add the required functionality to for handling the above scenarios:

ionic g provider images
ionic g provider preloader
ionic g provider sockets

Setting up socket integration

In order to manage Socket.io communication within Ionic we're going to take advantage of the following Angular package and install this using the following CLI command:

npm install --save ng-socket-io

Now we need to supply the Socket.io configuration details for our ionic-communicata application.

Within the following directory:

ionic-communicata/src/

Create a new sub-directory named configurations, place the following code snippet into a file and save this as configuration.ts to the configurations sub-directory:

export const config = {
   io : {
      url 				: "REMOTE-ADDRESS-OF-NODE-SERVER",
      options 			: {}
   }
};

This simply points to the address of the server where the Socket.io library is running.

With this in place you should now have the following file path for the Socket.io configuration file:

ionic-communicata/src/configurations/configuration.ts

Our next step will involve configuring the application root module to import and initialise this configuration file so that the Socket.io modules are made available for import into our application classes.

Open the ionic-communicata/src/app/app.module.ts file and change the current code to the following:

import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
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 { SocketIoModule } from 'ng-socket-io';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { SocketsProvider } from '../providers/sockets/sockets';
import { config } from '../configurations/configuration';
import { ImagesProvider } from '../providers/images/images';
import { PreloaderProvider } from '../providers/preloader/preloader';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    IonicModule.forRoot(MyApp),
    SocketIoModule.forRoot(config.io)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    SocketsProvider,
    ImagesProvider,
    PreloaderProvider
  ]
})
export class AppModule {}

With the necessary configurations and imports in place we can switch our attention to scripting the functionality for the services we created earlier in this tutorial - starting with the PreloaderProvider service.

Preloading images

As explained earlier we're going to allow users to select images which can be published to the chat window - unfortunately, as we're sending the image data as base64 data uris, we are going to encounter some issues with delayed rendering.

To help offset/overcome this we'll make use of Ionic's LoadingController component to provide visual feedback to our users when an image is being published for display in the chat window.

We'll code our own custom methods for showing/hiding the LoadingController component as demonstrated with the following logic for the ionic-communicata/src/providers/preloader/preloader.ts file:

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


@Injectable()
export class PreloaderProvider {


   /**
    * Reference for storing loading bar object
    */
   private loading : any;



   constructor( public http        : HttpClient,
                public loadingCtrl : LoadingController) 
   {  }



   /**
    *
    * Display an animated loading bar
    *
    * @public
    * @method displayPreloader
    * @param message    {String}        Message to be displayed with the loading bar
    * @return {none}
    */
   displayPreloader(message : string) : void
   {
      this.loading = this.loadingCtrl.create({
         content: message
      });

      this.loading.present();
   }




   /**
    *
    * Hide animated loading bar
    *
    * @public
    * @method hidePreloader
    * @return {none}
    */
   hidePreloader() : void
   {
      this.loading.dismiss();
   }

}

Handling socket communication

Thanks to the excellent ng-socket-io package that we installed earlier we can now start to leverage the methods supplied by this library to facilitate all Socket.io communication from node to Ionic and vice-versa.

This will be managed by the SocketsProvider service which is tasked with handling the following:

  • Making a request for a network connection (this will act as a test for network availability for when we convert this to a Progressive Web App in the next tutorial)
  • Allowing a new user to join the chat session
  • Retrieving and displaying chat messages
  • Publishing messages and selected images to the chat window

We'll make use of the ng-socket-io emit method to send out a custom message to the node server. The Socket.io library will listen for messages that have been broadcast from the ionic-communicata application, match these to the appropriate listener and then respond to the event accordingly.

In addition we're also going to broadcast events with the on method which the Socket.io library is programmed to listen for and act upon with the appropriate listener handling method.

In order to accomplish this simply open the ionic-communicata/src/providers/sockets/sockets.ts service and add the following logic:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Socket } from 'ng-socket-io';



@Injectable()
export class SocketsProvider {


   /**
    * @name _SERVER
    * @type object
    * @private
    * @description              The URI where the Socket.io server is running
    */
   private _SERVER 				: string 	=	'REMOTE-ADDRESS-OF-NODE-SERVER';



   constructor(public http 			: HttpClient,
   			   private _SOCKET 		: Socket) 
   {  }




   /**
    * @public
    * @method pollServer
    * @description    			Use Angular Http call to determine if server address is reachable
    * @return {Observable}
    */
   pollServer() : Observable<any>
   {
      return this._HTTP
      		 .get(this._SERVER);
   }




   /**
    * @public
    * @method registerForChatService
    * @param alias  	{string}     	The user's screen name alias
    * @param handle  	{string}     	The user's tagline
    * @param location	{string}     	The user's location
    * @description    					Register's the user with the current Socket.io chat session
    * @return {none}
    */
   registerForChatService(alias 	: string, 
   						  handle 	: string, 
   						  location 	: string) : void
   {
      this._SOCKET.connect();
      this._SOCKET.emit('set-alias', { alias: alias, handle: handle, location: location });
   }




   /**
    * @public
    * @method retrieveMessages
    * @description    					Retrieves the messages currently active in the session
    * @return {Observable}
    */
   retrieveMessages() : Observable
   {
      return new Observable((observer) =>
      {
         this._SOCKET.on('message', (data) => 
         {
            observer.next(data);
         });
      });
   }




   /**
    * @public
    * @method addMessage
    * @description    					Adds a message to the socket.io session
    * @return {none}
    */
   addMessage(message : string) : void
   {
      // Use the emit method of the Socket.io library to broadcast a custom event 
      // ('add-message') to the service - this will then add the supplied data to  
      // the current session message stream
      this._SOCKET.emit('add-message', { message: message });
   }




   /**
    * @public
    * @method addImage
    * @description    					Adds an image to the socket.io session
    * @return {none}
    */
   addImage(image : string) : void
   {
      // Use the emit method of the Socket.io library to broadcast a custom event 
      // ('add-image') to the service - this will then add the supplied data to  
      // the current session message stream      
      this._SOCKET.emit('add-image', { image: image });
   }




   /**
    * @public
    * @method logOut
    * @description    					Closes the current user's socket.io session
    * @return {none}
    */
   logOut() : void
   {
      this._SOCKET.disconnect();
   }



}

It's probably worth adding some methods into here to alert other chat users to when someone new has joined the session and when a user has exited the chat session.

Although this isn't particularly difficult I'll leave these to you to figure out! ;)

Managing image selection

As we're developing a Progressive Web App we'll be making use of the HTML5 FileReader API (which I covered in a previous tutorial) to handle selecting images for posting to the chat window.

With this in mind the ImagesProvider service will be concerned solely with handling the following tasks:

  • Selecting the image file
  • Returning the selected image as a base64 data URI

The ionic-communicata/src/providers/images/images.ts service manages these as follows:

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



@Injectable()
export class ImagesProvider {



   /**
    * @name _READER
    * @type object
    * @private
    * @description              Creates a FileReader API object
    */
   private _READER : any  			=	new FileReader();




   constructor(public http: HttpClient) 
   { }




   /**
    * @public
    * @method selectImage
    * @param event  {any}     	The DOM event that we are capturing from the File input field
    * @description    			Uses the FileReader API to capture the input field event, retrieve
    *                 			the selected image and return that as a base64 data URL courtesy of
    *							an Observable
    * @return {Observable}
    */
   selectImage(event) : Observable
   {
      return Observable.create((observer) =>
      {
         this.handleImageSelection(event)
         .subscribe((res) =>
         {      
            observer.next(res);
            observer.complete();   
         });
      });
   }




   /**
    * @public
    * @method handleImageSelection
    * @param event  {any}     	The DOM event that we are capturing from the File input field
    * @description    			Uses the FileReader API to capture the input field event, retrieve
    *                 			the selected image and return that as a base64 data URL courtesy of
    *							an Observable
    * @return {Observable}
    */
   handleImageSelection(event : any) : Observable
   {
      let file 		: any 		= event.target.files[0];

      this._READER.readAsDataURL(file);
      return Observable.create((observer) =>
      {
         this._READER.onloadend = () =>
         {
            observer.next(this._READER.result);
            observer.complete();
         }
      });
   }

}

That covers all of the service logic required for our application so now let's turn towards coding the logic, templating and styling for the HomePage component.

Coding the HomePage logic

The logic for driving the front-end facing aspect of the chat application is actually quite straightforward (even if it is a little involved in parts) and consists of the following:

  • Triggering an alert window that displays 3 input fields for the user to complete in order to 'log-in' to the current session
  • Retrieving messages for the current chat session from the node server that's running the Socket.io library
  • Allowing messages to be added to the current chat session
  • Allowing images to be uploaded for display to the current chat session
  • Scrolling to the bottom of the content window when a new message has been added to the current chat session
  • Allowing the messages for the current chat session to be exported (well, logged in full to the browser console - you WILL have to wire in the storage/transfer logic for saving those exported messages...I’ve started so you can finish!)
  • Log the user out from the current chat session and reset the application state to its default

We will, of course, be making appropriate use of the ImagesProvider, PreloaderProvider and SocketsProvider services at various junctures within the HomePage component logic.

To implement this functionality simply open the ionic-communicata/src/pages/home/home.ts component class and add the following code:

import { Component, ViewChild } from '@angular/core';
import { Content, NavController, AlertController } from 'ionic-angular';
import { ImagesProvider } from '../../providers/images/images';
import { PreloaderProvider } from '../../providers/preloader/preloader';
import { SocketsProvider } from '../../providers/sockets/sockets';
import 'rxjs/add/operator/toPromise';


@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
   @ViewChild(Content) content : Content;

   
   /**
    * @name alias
    * @type String
    * @public
    * @description              Stores the value for the user's entered alias  
    */
   public alias 				: string;




   /**
    * @name handle
    * @type String
    * @public
    * @description              Stores the value for the user's entered tagline  
    */
   public handle 				: string;




   /**
    * @name location
    * @type String
    * @public
    * @description              Stores the value for the user's entered location  
    */
   public location 				: string;
		
		

		
   /**
    * @name displayRegisterAlias
    * @type boolean
    * @public
    * @description              Determines which template items are displayed  
    */
   public displayRegisterAlias 	: boolean 			= true;
		
		

		
   /**
    * @name messages
    * @type object
    * @public
    * @description              Array that stores all chat data 
    */
   public messages 				: Array		= [];
		
		

		
   /**
    * @name message
    * @type object
    * @public
    * @description              Model for managing data for/from the input field 
    */
   public message 				: any;
		

		
		
   /**
    * @name image
    * @type object
    * @public
    * @description              Stores retrieved image file data
    */
   public image 				: any;




   constructor(public navCtrl 		: NavController,
   			   private _ALERT       : AlertController,
   			   private _IMAGES 		: ImagesProvider,
   			   private _PRELOADER   : PreloaderProvider,
   			   private _SOCKET 		: SocketsProvider) 
   {  }




   /**
    * @public
    * @method ionViewDidLoad
    * @description    	On view loaded detect whether the network is able to be accessed	
    * @return {none}
    */
   ionViewDidLoad() : void
   {
      this.detectNetworkConnection(); 
   }




   /**
    * @public
    * @method detectNetworkConnection
    * @description    	Detects whether the chat server can be contacted			
    * @return {none}
    */
   detectNetworkConnection() : void
   {
      this._SOCKET
      .pollServer()
      .toPromise()
      .then((data : any) =>
      {
         this.displayMessages();
      })
      .catch((error) =>
      {
         this.displayNetworkErrorWarning();
      }); 
   }




   /**
    * @public
    * @method displayNetworkErrorWarning
    * @description    	Displays an alert window informing the user that network connectivity
    * 					cannot be detected		
    * @return {none}
    */
   displayNetworkErrorWarning() : void
   {
      let alert : any = this._ALERT.create({
         title 	    	: 'Network error',
         subTitle     	: 'Please check your network connection and try again',
         buttons 	    : [
            {
               text 	: 'Retry',
               handler 	: (data) =>
               {
                  this.detectNetworkConnection();                  
               }
            }]
            
      });
      alert.present();
   }



   /**
    * @public
    * @method registerAlias
    * @description    			Uses the Ionic AlertController to display a form with 3 input fields:
    *                 			1. alias - the user's chosen screen name
    *							2. handle - their tagline
    *							3. location - their location
    *
    *							This then registers the user for the temporary chat service allowing 
    *							them to post
    * @return {none}
    */
   registerAlias() : void
   {
      let alert : any = this._ALERT.create({
         title 	    	: 'Please supply your screen name, handle and location',
         inputs 	    : [
            {
               type 	: 'text',
               name 	: 'alias',
               placeholder : 'I.e. Ming the merciless'
            },
            {
               type 	: 'text',
               name 	: 'handle',
               placeholder : 'I.e. King of the universe'
            },
            {
               type 	: 'text',
               name 	: 'location',
               placeholder : 'I.e. Here, there and everywhere'
            }
         ],      
         buttons 	    : [
            {
               text 	: 'Cancel',
               handler	: (data) =>
               {
                  console.log('Cancelled');
               }
            },
            {
               text 	: 'Save',
               handler 	: (data) =>
               {
                  this.alias  				      = data.alias;
                  this.handle  				      = data.handle;
                  this.location				      = data.location;
                  this.displayRegisterAlias	      = false;
                  this.registerForChatService(this.alias, this.handle, this.location);                  
               }
            }]
            
      });
      alert.present();
   }
  



   /**
    * @public
    * @method registerForChatService
    * @param alias  	{string}     	The user's screen name alias
    * @param handle  	{string}     	The user's tagline
    * @param location	{string}     	The user's location
    * @description    				Register's the user with the current Socket.io chat session
    * @return {none}
    */
   registerForChatService(alias 	: string, 
   						  handle 	: string, 
   						  location 	: string) : void
   {
      this._SOCKET.registerForChatService(alias, handle, location);
   }




   /**
    * @public
    * @method logOut
    * @description    			Disconnects the user from Socket.io chat service
    *							and resets the application state
    * @return {none}
    */
   logOut() : void
   {
      this._SOCKET.logOut();
      this.alias 					= "";
      this.handle 					= "";
      this.location					= "";
      this.image					= "";
      this.message					= "";
      this.messages					= [];
      this.displayRegisterAlias 	= true;
   }




   /**
    * @public
    * @method displayMessages
    * @description    			Retrieves the posted message from the Socket.io chat service
    *							and publishes these to the template (courtesy of the Observable's 
    *							subscribe method acting as a listener for data changes)
    * @return {none}
    */
   displayMessages() : void
   {
      this._SOCKET.retrieveMessages()
      .subscribe((message) =>
      {

         // Update the messages array
         this.messages.push(message);


         // Trigger the scroll API
         setTimeout(() =>
         {
            this.scrollToLatestMessage();
         }, 500);
      });
   }




   /**
    * @public
    * @method addMessage
    * @description    			Adds a message to the Socket.io chat service and resets the model value
    *							used with the template's input field
    * @return {none}
    */
   addMessage() : void
   {
      this._SOCKET.addMessage(this.message);
      this.message 	= '';
   }




   /**
    * @public
    * @method addImage
    * @description    			Adds an image to the Socket.io chat service and resets the model value
    *							used with the template's file input field
    * @return {none}
    */
   addImage() : void
   {
      this._SOCKET.addImage(this.image);
      this.image 	= '';
      this._PRELOADER.hidePreloader();
   }




   /**
    * @public
    * @method selectImage
    * @param event  {any}     	The DOM event that we are capturing from the File input field
    * @description    			Selects an image to be uploaded to the Socket.io chat service 
    * @return {none}
    */
   selectImage(event : any) : void
   {
      this._IMAGES
      .selectImage(event)
      .subscribe((res) =>
      {
         this._PRELOADER.displayPreloader('Loading...');
         this.image = res;
         this.addImage();
      });
   }




   /**
    * @public
    * @method exportMessages
    * @description    			Publishes a console log of all the current session's chat messages
    * @return {none}
    */
   exportMessages() : void
   {
      console.dir(this.messages);
   }




   /**
    * @public
    * @method scrollToLatestMessage
    * @description    			Triggers the scrollToBottom method of the Ionic Scroll API 
    * @return {none}
    */
   scrollToLatestMessage() : void
   {
      this.content.scrollToBottom(300);
   }


}

Templating the HomePage component

Our HomePage component template is responsible, in coordination with the associated HomePage component class, for managing the display of the logged-out and logged-in states for the chat application.

In particular the template will need to handle the following:

  • Display the Join the chat session button (which will trigger the registerAlias() method and publish a chat session signup form to the window courtesy of Ionic's AlertController API)
  • Display an export chat messages and logout button (using <ion-icons>) once the user has joined the current chat session
  • Render chat messages to the window
  • Allow images to be selected courtesy of the HTML file input field
  • Allow chat messages to be entered

These are subsequently managed by the ionic-communicata/src/pages/home/home.html template as follows (additional comments from myself to help elaborate on each key area of markup displayed):

<ion-header>
  <ion-navbar>
    <ion-title>
      Ionic Communicata
    </ion-title>
    <ion-buttons end>


       <!-- Display the Join Chat session button IF the displayRegisterAlias
            value is set to true -->	
       <button 
          *ngIf="displayRegisterAlias"
          ion-button 
          color="danger" 
          (click)="registerAlias()">Join!</button>


       <!-- Display the Export Chat session button IF the displayRegisterAlias
            value is set to false -->
       <button start
          *ngIf="!displayRegisterAlias"
          ion-button 
          color="danger" 
          (click)="exportMessages()">          
          <ion-icon name="cloud-download"></ion-icon>
      </button>


       <!-- Display the Chat Logout button IF the displayRegisterAlias
            value is set to false -->
       <button end
          *ngIf="!displayRegisterAlias"
          ion-button 
          color="danger" 
          (click)="logOut()">
          <ion-icon name="close"></ion-icon>
       </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>



<ion-content padding>
 
   <!-- IF the displayRegisterAlias value is set to true then the
        user HASN'T joined the chat session so we display a message
        encouraging them to join -->
   <section 
      *ngIf="displayRegisterAlias" 
      class="message">
      <h1>Join now to see the chat!</h1>
   </section>



   <!-- IF the displayRegisterAlias value is set to false then the
        user HAS joined the chat session so we display the current 
        list of messages -->
   <div 
      *ngIf="!displayRegisterAlias" 
      class="chat-messages">
   	  <section 
   	     *ngFor="let message of messages">


         <!-- Here we use Angular 4+ If/then & else syntax to assign the correct 
              template for each posted message (is the current message from the 
              user who has just joined or not? If it is then we need to assign a  
              specific CSS class to differentiate the messages by sender) -->
   	     <section *ngIf="message.sender == alias;then senderTemplate; else recipientTemplate"></section>


         <!-- IF the message was sent by the user who has just joined then we use this template -->
   	     <ng-template #senderTemplate>
   	     	<section  class="chat-message sender">
   	     	   <p>{{ message.message }}</p>


               <!-- IF an image has been posted then we need to display that -->
               <img *ngIf="message.image" [src]="message.image">


               <!-- Display the sender details -->
               <section class="message-footer">
               	<small>Posted by {{ message.sender }} ({{ message.tagline }}) from {{ message.location }} on {{ message.published | date: "medium" }}</small>
               </section>
            </section>
   	     </ng-template>




   	     <!-- IF the message was NOT sent by the user who has just joined then we use this template instead -->
   	     <ng-template #recipientTemplate>   	     	
   	     	<section  class="chat-message other">
   	     	   <p>{{ message.message }}</p>


               <!-- IF an image has been posted then we need to display that -->
               <img *ngIf="message.image" [src]="message.image">

               <!-- Display the sender details -->
               <section class="message-footer">
               	<small>Posted by {{ message.sender }} ({{ message.tagline }}) from {{ message.location }} on {{ message.published | date: "medium" }}</small>
               </section>
            </section>
   	     </ng-template>
   	  </section>
   </div>
    


</ion-content>

<!-- Conditionally display the input field for creating chat messages IF the user has 'logged in' -->
<ion-footer 
   *ngIf="!displayRegisterAlias" 
   class="footer">
   <ion-grid padding>
      <ion-row>


         <!-- Display input field for selecting image files -->
         <ion-col col-2>
         	<div class="upload-button-wrapper" color="primary">
               <ion-icon name="image"></ion-icon>
               <input
                  class="file-upload-button" 
                  type="file" 
                  (change)="selectImage($event)">
            </div>
         </ion-col>


         <!-- Display input field for entering text message -->
         <ion-col col-8>
            <ion-input
               type="text" 
               placeholder="Your chat message..."
	           [(ngModel)]="message"></ion-input>
         </ion-col>


         <!-- Post the image or text message for the current chat session -->
         <ion-col col-1>
            <button 
	           ion-button 
	           clear 
	           color="primary" 
	           (click)="addMessage()">
	           Add
	        </button>
         </ion-col>
      </ion-row>
   </ion-grid>	
</ion-footer>

As you can see there's quite a bit going on with the HomePage component HTML at each of the following key sections of the template:

  • <ion-header>
  • <ion-content>
  • <ion-footer>

That said it's actually not all that complicated so we don't need to linger here with any further explanations - instead let's move onto the styling for the application UI.

Styling the HomePage component

As the interface for the ionic-communicata application is quite customised we'll need to supply the following style rules within the ionic-communicata/src/pages/home/home.scss stylesheet:

page-home {

   ion-icon {
      font-size: 2em;
   }

   /*
      Allow chat messages to 'clear' the footer
    */   
   .scroll-content {
      padding-bottom: 7.5em !important;
   }
   	


   /*
      Styling for the join chat notice
    */   
   .message {
   	  
      background: rgba(240,240,240,1);
      text-align: center;
      height: 100%;
      padding: 0;
      margin: 0;
      display: -webkit-box;
      display: -moz-box;
      display: -ms-flexbox;
      display: -webkit-flex;
      display: flex;
      align-items: center;
      justify-content: center;


      h1 {
         font-size: 1.5em;
         tex-align: center;
      }
   }



   /*
      Styling for the chat messages
    */   
   .chat-messages {
   	  margin-bottom: 10em !important;
   	  padding-bottom: 10em !important;

      .chat-message {
      	 padding: 1em;
      	 margin: 1em 0;
      	 border-radius: 1em;
      	 width: 85%;
      }

      .sender {
      	float: left;
      	 background: rgba(84, 130, 173, 0.27);
      }

      .other {
      	 float: right;
      	 background: rgba(130, 130, 130, 0.27);
      }

      .message-footer {
      	 border-top: 1px solid rgba(180, 180, 180, 0.28);
         padding: 5px 0 0 0;
      }
   }


   

   /*
      Styling for the HomePage footer/chat input fields
    */
   .footer {
   	  background: rgba(230,230,230,1);
   }

   .upload-button-wrapper {
      position: relative;
      overflow: hidden;
      margin: 10px;

      .file-upload-button {
         position: absolute;
         top: 0;
         right: 0;
         margin: 0;
         padding: 0;
         font-size: 1.2em;
         cursor: pointer;
         opacity: 0;
         filter: alpha(opacity=0); // Play nice with older version of Internet Explorer :)
      }
   }

   ion-input {
   	 background: rgba(255, 255, 255, 1);
   }
}

Now all that remains is to actually test the chat application works!

Returning back to the Ionic CLI, and ensuring you are within the root directory of the ionic-communicata project, run the following command to launch the applictaion within your system browser:

ionic serve

All things being well you should be greeted with the application running and, following from this, be able to interact with this as demonstrated with the following GIF animation:

Ionic real-time chat application in use

In summary

Adding realtime chat functionality to an Ionic framework application is relatively simple with the combination of node/Socket.io.

Within this second of our three-part tutorial we've taken the ionic-communicata application that we developed using Ionic Pro/Ionic CLI in the first tutorial and we've added realtime chat capability with the ng-socket-io library along with image selection courtesy of the HTML5 Filereader API.

All of which is handled within a Single Page Application thanks to use of Angular services and Angular/Ionic components.

In part 3 of this tutorial we'll conclude the development of the ionic-communicata app by converting this into a Progressive web App.

If you've enjoyed what you've read and/or found this helpful please feel free to share your comments, thoughts and suggestions in the comments area below.

I explore different aspects of working with the Ionic framework in my e-book featured below and if you're interested in learning more about further articles and e-books that I'm writing please sign up to my FREE mailing list.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top