Fork me on GitHub

WhatsApp Clone with Meteor and Ionic 2 CLI

Legacy Tutorial Version (Last Update: 22.11.2016)

Chats page

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

First Ionic Component

Now that we're finished with the initial setup, we can start building our app.

An application created by Ionic's CLI will have a very clear methodology. The app is made out of pages, each page is made out of 3 files:

  • .html - A view template file written in HTML based on Angular2's new template engine.
  • .scss - A stylesheet file written in a CSS pre-process language called SASS.
  • .ts - A script file written in Typescript.

By default, the application will be created with 3 pages - about, home and contact. Since our app's flow doesn't contain any of them, we first gonna clean them up by running the following commands:

$ rm -rf src/pages/about
$ rm -rf src/pages/home
$ rm -rf src/pages/contact

Second, we will remove their declaration in the app module:

2.1 Remove irrelevant pages src/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
 
14
15
16
17
18
19
import { NgModule, ErrorHandler } from [email protected]/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { TabsPage } from '../pages/tabs/tabs';
 
@NgModule({
  declarations: [
    MyApp,
    TabsPage
  ],
  imports: [
...some lines skipped...
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    TabsPage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]

And finally, will their usage in that tabs component:

2.2 Removed tabs from the Component src/pages/tabs/tabs.ts
1
2
3
4
5
6
7
8
9
import { Component } from [email protected]/core';
 
@Component({
  templateUrl: 'tabs.html'
})
export class TabsPage {
  constructor() {
 
  }

Now we're gonna define 4 tabs: chats, contacts, favorites and recents. In this tutorial we want to focus only on the messaging system, therefore we only gonna implement the chats tab, the rest is just for the layout:

2.3 Edit tabs template to contain the necessary tabs src/pages/tabs/tabs.html
1
2
3
4
5
6
<ion-tabs>
  <ion-tab tabIcon="chatboxes"></ion-tab>
  <ion-tab tabIcon="contacts"></ion-tab>
  <ion-tab tabIcon="star"></ion-tab>
  <ion-tab tabIcon="clock"></ion-tab>
</ion-tabs>

If you will take a closer look at the view template we've just defined, you can see that one of the tab's attributes is wrapped with [square brackets]. This is part of Angular2's new template syntax and what it means is that the property called root of the HTML element is bound to the chatsTabRoot property of the component.

Our next step would be implementing the chats tab; First let's start by adding moment as a dependency - a utility library in JavaScript which will help us parse, validate, manipulate, and display dates:

$ npm install --save moment

We will start by implementing a view stub, just so we can get the idea of Ionic's components:

2.5 Added ChatsPage view src/pages/chats/chats.html
1
2
3
4
5
6
7
8
9
10
11
<ion-header>
  <ion-navbar>
    <ion-title>
      Chats
    </ion-title>
  </ion-navbar>
</ion-header>
 
<ion-content padding>
  Hello!
</ion-content>

Once creating an Ionic page it's recommended to use the following layout:

  • <ion-header> - The header of the page. Will usually contain content that should be bounded to the top like navbar.
  • <ion-content> - The content of the page. Will usually contain it's actual content like text.
  • <ion-footer> - The footer of the page. Will usually contain content that should be bounded to the bottom like toolbars.

To display a view, we will need to have a component as well, whose basic structure should look like so:

2.6 Added ChatsPage Component src/pages/chats/chats.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import * as moment from 'moment';
import { Component } from [email protected]/core';
import { Observable } from "rxjs";
 
@Component({
  templateUrl: 'chats.html'
})
export class ChatsPage {
  chats: Observable<any[]>;
 
  constructor() {
    this.chats = this.findChats();
  }
 
  private findChats(): Observable<any[]> {
    return Observable.of([
      {
        _id: '0',
        title: 'Ethan Gonzalez',
        picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg',
        lastMessage: {
          content: 'You on your way?',
          createdAt: moment().subtract(1, 'hours').toDate()
        }
      },
      {
        _id: '1',
        title: 'Bryan Wallace',
        picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg',
        lastMessage: {
          content: 'Hey, it\'s me',
          createdAt: moment().subtract(2, 'hours').toDate()
        }
      },
      {
        _id: '2',
        title: 'Avery Stewart',
        picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg',
        lastMessage: {
          content: 'I should buy a boat',
          createdAt: moment().subtract(1, 'days').toDate()
        }
      },
      {
        _id: '3',
        title: 'Katie Peterson',
        picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg',
        lastMessage: {
          content: 'Look at my mukluks!',
          createdAt: moment().subtract(4, 'days').toDate()
        }
      },
      {
        _id: '4',
        title: 'Ray Edwards',
        picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg',
        lastMessage: {
          content: 'This is wicked good ice cream.',
          createdAt: moment().subtract(2, 'weeks').toDate()
        }
      }
    ]);
  }
}

The logic is simple. Once the component is created we gonna a define dummy chats array just so we can test our view. As you can see we're using the moment package to fabricate date values. The Observable.of syntax is used to create an Observable with a single value.

Now let's load the newly created component in the module definition so our Angular 2 app will recognize it:

2.7 Added ChatsPage to the module definition src/app/app.module.ts
2
3
4
5
6
7
8
9
10
11
12
13
 
16
17
18
19
20
21
22
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { TabsPage } from '../pages/tabs/tabs';
import { ChatsPage } from "../pages/chats/chats";
 
@NgModule({
  declarations: [
    MyApp,
    ChatsPage,
    TabsPage
  ],
  imports: [
...some lines skipped...
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    ChatsPage,
    TabsPage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]

Let's define it on the tabs component:

2.8 Added chats tab root src/pages/tabs/tabs.ts
1
2
3
4
5
6
7
8
9
10
11
12
import { Component } from [email protected]/core';
import { ChatsPage } from "../chats/chats";
 
@Component({
  templateUrl: 'tabs.html'
})
export class TabsPage {
  chatsTab = ChatsPage;
 
  constructor() {
 
  }

And define it as the root tab, which means that once we enter the tabs view, this is the initial tab which is gonna show up:

2.9 Added root to the tab element src/pages/tabs/tabs.html
1
2
3
4
5
<ion-tabs>
  <ion-tab [root]="chatsTab" tabIcon="chatboxes"></ion-tab>
  <ion-tab tabIcon="contacts"></ion-tab>
  <ion-tab tabIcon="star"></ion-tab>
  <ion-tab tabIcon="clock"></ion-tab>

TypeScript Interfaces

Now, because we use TypeScript, we can define our own data-types and use then in our app, which will give you a better auto-complete and developing experience in most IDEs. In our application, we have 2 models at the moment: a chat model and a message model. We will define their interfaces in a file located under the path /modes/whatsapp-models.d.ts:

2.10 Added chat and message models models/whatsapp-models.d.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare module 'api/models/whatsapp-models' {
  interface Chat {
    _id?: string;
    title?: string;
    picture?: string;
    lastMessage?: Message;
  }
 
  interface Message {
    _id?: string;
    chatId?: string;
    content?: string;
    createdAt?: Date;
  }
}

The d.ts extension stands for declaration - TypeScipt, which basically tells the compiler that this is a declaration file, just like C++'s header files. In addition, we will need to add a reference to our models declaration file, so the compiler will recognize it:

2.11 Added models typings into the config src/declarations.d.ts
11
12
13
14
15
16
  For more info on type definition files, check out the Typescript docs here:
  https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html
*/
/// <reference path="../models/whatsapp-models.d.ts" />
declare module '*';
 

Now we can finally use the chat model in the ChatsPage:

2.12 Use Chat model in ChatsPage src/pages/chats/chats.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as moment from 'moment';
import { Component } from [email protected]/core';
import { Observable } from "rxjs";
import { Chat } from "api/models/whatsapp-models";
 
@Component({
  templateUrl: 'chats.html'
})
export class ChatsPage {
  chats: Observable<Chat[]>;
 
  constructor() {
    this.chats = this.findChats();
  }
 
  private findChats(): Observable<Chat[]> {
    return Observable.of([
      {
        _id: '0',

Ionic Themes

Ionic 2 provides us with a comfortable theming system which is based on SASS variables. The theme definition file is located in src/theme/variable.scss. Since we want our app to have a "Whatsappish" look, we will define a new SASS variable called whatsapp in the variables file:

2.13 Added theme color definition src/theme/variables.scss
28
29
30
31
32
33
34
35
  secondary:  #32db64,
  danger:     #f53d3d,
  light:      #f4f4f4,
  dark:       #222,
  whatsapp:   #075E54
);
 
 

The whatsapp color can be used by adding an attribute called color with a value whatsapp to any Ionic component:

2.14 Apply whatsapp theme on tabs view src/pages/tabs/tabs.html
1
2
3
4
<ion-tabs color="whatsapp">
  <ion-tab [root]="chatsTab" tabIcon="chatboxes"></ion-tab>
  <ion-tab tabIcon="contacts"></ion-tab>
  <ion-tab tabIcon="star"></ion-tab>

Now let's implement the chats view properly in the ChatsView by showing all available chat items:

2.15 Added chats page view with a list of chats src/pages/chats/chats.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<ion-header>
  <ion-navbar color="whatsapp">
    <ion-title>
      Chats
    </ion-title>
    <ion-buttons end>
      <button ion-button icon-only class="add-chat-button">
        <ion-icon name="person-add"></ion-icon>
      </button>
      <button ion-button icon-only class="options-button">
        <ion-icon name="more"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 
<ion-content class="chats-page-content">
  <ion-list class="chats">
    <button ion-item *ngFor="let chat of chats | async" class="chat">
      <img class="chat-picture" [src]="chat.picture">
 
      <div class="chat-info">
        <h2 class="chat-title">{{chat.title}}</h2>
 
        <span *ngIf="chat.lastMessage" class="last-message">
          <p class="last-message-content">{{chat.lastMessage.content}}</p>
          <span class="last-message-timestamp">{{chat.lastMessage.createdAt}}</span>
        </span>
      </div>
    </button>
  </ion-list>
</ion-content>

We use ion-list which Ionic translate into a list, and use ion-item for each one of the items in the list. A chat item includes an image, the receiver's name, and its recent message.

The async pipe is used to iterate through data which should be fetched asynchronously, in this case, observables

Now, in order to finish our theming and styling, let's create a stylesheet file for our component:

2.16 Added some styles src/pages/chats/chats.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.chats-page-content {
  .chat-picture {
    border-radius: 50%;
    width: 50px;
    float: left;
  }
 
  .chat-info {
    float: left;
    margin: 10px 0 0 20px;
 
    .last-message-timestamp {
      position: absolute;
      top: 10px;
      right: 10px;
      font-size: 14px;
      color: #9A9898;
    }
  }
}

Ionic will load newly defined stylesheet files automatically, so you shouldn't be worried for importations.

External Angular 2 Modules

Since Ionic 2 uses Angular 2 as the layer view, we can load Angular 2 modules just like any other plain Angular 2 application. One module that may come in our interest would be the angular2-moment module, which will provide us with the ability to use moment's utility functions in the view using pipes.

It requires us to install angular2-moment module using the following command:

$ npm install --save angular2-moment

Now we will need to declare this module in the app's main component:

2.18 Import MomentModule into the module definition src/app/app.module.ts
3
4
5
6
7
8
9
 
12
13
14
15
16
17
18
19
import { MyApp } from './app.component';
import { TabsPage } from '../pages/tabs/tabs';
import { ChatsPage } from "../pages/chats/chats";
import { MomentModule } from "angular2-moment";
 
@NgModule({
  declarations: [
...some lines skipped...
    TabsPage
  ],
  imports: [
    IonicModule.forRoot(MyApp),
    MomentModule
  ],
  bootstrap: [IonicApp],
  entryComponents: [

Which will make moment available as a pack of pipes, as mentioned earlier:

2.19 Apply calendar pipe to chats view template src/pages/chats/chats.html
24
25
26
27
28
29
30
 
        <span *ngIf="chat.lastMessage" class="last-message">
          <p class="last-message-content">{{chat.lastMessage.content}}</p>
          <span class="last-message-timestamp">{{chat.lastMessage.createdAt | amCalendar }}</span>
        </span>
      </div>
    </button>

Ionic Touch Events

Ionic provides us with components which can handle mobile events like: slide, tap and pinch. In the chats view we're going to take advantage of the slide event, by implementing sliding buttons using the ion-item-sliding component:

2.20 Add chat removal button to view template src/pages/chats/chats.html
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 
<ion-content class="chats-page-content">
  <ion-list class="chats">
    <ion-item-sliding *ngFor="let chat of chats | async">
      <button ion-item class="chat">
        <img class="chat-picture" [src]="chat.picture">
 
        <div class="chat-info">
          <h2 class="chat-title">{{chat.title}}</h2>
 
          <span *ngIf="chat.lastMessage" class="last-message">
            <p class="last-message-content">{{chat.lastMessage.content}}</p>
            <span class="last-message-timestamp">{{chat.lastMessage.createdAt | amCalendar }}</span>
          </span>
        </div>
      </button>
      <ion-item-options class="chat-options">
        <button ion-button color="danger" class="option option-remove" (click)="removeChat(chat)">Remove</button>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
</ion-content>

This implementation will reveal a remove button once we slide a chat item to the left. The only thing left to do now would be implementing the chat removal event handler in the chats component:

2.21 Add chat removal stub method to chats component src/pages/chats/chats.ts
62
63
64
65
66
67
68
69
70
71
72
73
      }
    ]);
  }
 
  removeChat(chat: Chat): void {
    this.chats = this.chats.map<Chat[]>(chatsArray => {
      const chatIndex = chatsArray.indexOf(chat);
      chatsArray.splice(chatIndex, 1);
      return chatsArray;
    });
  }
}