Fork me on GitHub

WhatsApp Clone with Ionic 2 and Meteor 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.

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

An Ionic application is made out of pages, each page is an Angular component.

First page

Let's create the first page and call it TabsContainer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {Component} from "@angular/core";
 
@Component({
  selector: "tabs-container",
  template: `
  <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>
  `
})
export class TabsContainerComponent {
  constructor() {
 
  }
}

We defined 3 tabs (see documentation): chats, contacts, favorites. 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.

Now we need to include this component in the AppModule to make it available for our application:

2.2 Added the Component to the NgModule client/imports/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { IonicApp, IonicModule } from "ionic-angular";
import {TabsContainerComponent} from "../pages/tabs-container/tabs-container.component";
 
@NgModule({
  // Components, Pipes, Directive
  declarations: [
    AppComponent,
    TabsContainerComponent
  ],
  // Entry Components
  entryComponents: [
    AppComponent,
    TabsContainerComponent
  ],
  // Providers
  providers: [

One thing is missing and that's the root page. The application doesn't know which page to load at the beginning.

Navigation is handled through the <ion-nav> component. Go to AppComponent's template to change it:

2.3 Updated the main component to use Ionic navigation client/imports/app/app.component.html
1
<ion-nav [root]="rootPage"></ion-nav>

Now we can define rootPage and use TabsContainerComponent:

2.4 Added missing rootPage variable client/imports/app/app.component.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Platform } from "ionic-angular";
import { StatusBar } from "ionic-native";
import template from './app.component.html';
import {TabsContainerComponent} from "../pages/tabs-container/tabs-container.component";
 
@Component({
  selector: 'app',
  template
})
export class AppComponent {
  rootPage = TabsContainerComponent;
 
  constructor(platform: Platform) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.

Navigation in Ionic works as a simple stack. New pages are pushed onto and popped off of, corresponding to moving forward and backward in history.

Chats

We're going to create a component that contains list of chats.

First thing, a template:

2.5 Added the chats page template client/imports/pages/chats/chats.component.html
1
2
3
4
5
6
7
8
9
<ion-header>
  <ion-navbar>
    <ion-title>Chats</ion-title>
  </ion-navbar>
</ion-header>
 
<ion-content padding>
  <h2>Welcome!</h2>
</ion-content>

Then, the actual component, called ChatsComponent:

2.6 Added the chats page component client/imports/pages/chats/chats.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
import {Component} from "@angular/core";
import template from "./chats.component.html"
 
@Component({
  selector: "chats",
  template
})
export class ChatsComponent {
  constructor() {
 
  }
}

As you probably remember, it still need to be added to AppModule:

2.7 Added the chats page to the NgModule client/imports/app/app.module.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { AppComponent } from './app.component';
import { IonicApp, IonicModule } from "ionic-angular";
import {TabsContainerComponent} from "../pages/tabs-container/tabs-container.component";
import {ChatsComponent} from "../pages/chats/chats.component";
 
@NgModule({
  // Components, Pipes, Directive
  declarations: [
    AppComponent,
    TabsContainerComponent,
    ChatsComponent
  ],
  // Entry Components
  entryComponents: [
    AppComponent,
    TabsContainerComponent,
    ChatsComponent
  ],
  // Providers
  providers: [

Since, the component is available, we can bind it to Chats tab:

2.8 Use the chats page component in ion-tabs client/imports/pages/tabs-container/tabs-container.component.ts
1
2
3
4
5
6
7
8
9
10
11
 
13
14
15
16
17
18
19
20
import {Component} from "@angular/core";
import {ChatsComponent} from "../chats/chats.component";
 
@Component({
  selector: "tabs-container",
  template: `
  <ion-tabs>
    <ion-tab [root]="chatsRoot" tabIcon="chatboxes"></ion-tab>
    <ion-tab tabIcon="contacts"></ion-tab>
    <ion-tab tabIcon="star"></ion-tab>
    <ion-tab tabIcon="clock"></ion-tab>
...some lines skipped...
  `
})
export class TabsContainerComponent {
  chatsRoot = ChatsComponent;
 
  constructor() {
 
  }

Theme

Ionic2 provides us with a new theming system. The theme is determined thanks to SASS variables located in the file client/styles/ionic.scss. By changing these variables our entire app's theme will be changed as well. Not only that, but you can also add new theming colors, and they should be available on the HTML as attributes, and the should affect the theming of most Ionic elements once we use them.

Since we want our app to have a Whatsapp theme, we gonna define a new variable called whatsapp:

2.9 Add whatsapp theme variable client/styles/ionic.scss
27
28
29
30
31
32
33
34
        danger:     #f53d3d,
        light:      #f4f4f4,
        dark:       #222,
        favorite:   #69BB7B,
        whatsapp:   #075E54
);
 
// Components

Now whenever we will use it as an HTML attribute we gonna have a greenish background, just like Whatsapp.

2.10 Added theme usage for tabs component client/imports/pages/tabs-container/tabs-container.component.ts
4
5
6
7
8
9
10
@Component({
  selector: "tabs-container",
  template: `
  <ion-tabs color="whatsapp">
    <ion-tab [root]="chatsRoot" tabIcon="chatboxes"></ion-tab>
    <ion-tab tabIcon="contacts"></ion-tab>
    <ion-tab tabIcon="star"></ion-tab>

Models

It's time to think about the data structure of chats and messages.

Let's begin with a message. It should contain content and date of creating.

2.11 Added message model both/models/message.model.ts
1
2
3
4
5
export interface Message {
  _id?: string;
  content?: string;
  createdAt?: Date;
}

Because it represents a Mongo Object we also added _id property.

Do the same for a chat:

2.12 Added chat model both/models/chat.model.ts
1
2
3
4
5
6
7
8
import {Message} from "./message.model";
 
export interface Chat {
  _id?: string;
  title?: string;
  picture?: string;
  lastMessage?: Message;
}

Chat has title, picture and an object with a last message.

Data

Whatsapp needs data, so we going to define dummy chats just so we can test our view.

2.14 Added list of stub chats client/imports/pages/chats/chats.component.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
import {Component} from "@angular/core";
import template from "./chats.component.html"
import {Observable} from "rxjs";
import {Chat} from "../../../../both/models/chat.model";
import * as moment from "moment";
 
@Component({
  selector: "chats",
  template
})
export class ChatsComponent {
  chats: Observable<Chat[]>;
 
  constructor() {
    this.chats = 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()
        }
      }
    ])
  }
}

As you can see we're using a package called Moment to fabricate some dates. Let's install it:

$ npm install moment

It requires declarations:

$ typings install --save --global dt~moment

We used Observable.of that creates an Observable that emits values we specified as arguments.

View

Let's update the view of ChatsComponent:

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
<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 placed two buttons at the end of Navigation Bar. First's purpose is to add new chat, but second's to open a menu with more options.

New ion-content contains list of chats. Each element has a picture, title and an information about the last message.

NOTE: Ionic elements will always have a prefix of ion and are self explanatory. Further information about Ionic's HTML elements can be found here. It's very important to use these elemnts since they are the ones who provides us with the mobile-app look.

The *ngFor attribute is used for iteration and is equivalent to Angular1's ng-for attribute. The '*' sign just tells us that this is a template directive we're dealing with (A directive that should eventually be rendered in the view). As you probably noticed, we used AsyncPipe to display the result of Observable under chat property.

Let' make it to look better by creating the chats.component.scss file:

2.16 Added some css styles to the chats list client/imports/pages/chats/chats.component.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;
    }
  }
}

To include those styles in our component we need to:

2.17 Import the new styles into the Component client/imports/pages/chats/chats.component.ts
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {Observable} from "rxjs";
import {Chat} from "../../../../both/models/chat.model";
import * as moment from "moment";
import style from "./chats.component.scss";
 
 
@Component({
  selector: "chats",
  template,
  styles: [
    style
  ]
})
export class ChatsComponent {
  chats: Observable<Chat[]>;

We also want to display date under createdAt property in a proper way. Moment library contains a package for Angular that will help us.

$ npm install [email protected] --save

It's not yet available to Whatsapp. Let's change it:

2.19 Added MomentModule to the NgModule client/imports/app/app.module.ts
3
4
5
6
7
8
9
 
24
25
26
27
28
29
30
31
import { IonicApp, IonicModule } from "ionic-angular";
import {TabsContainerComponent} from "../pages/tabs-container/tabs-container.component";
import {ChatsComponent} from "../pages/chats/chats.component";
import {MomentModule} from "angular2-moment";
 
@NgModule({
  // Components, Pipes, Directive
...some lines skipped...
  ],
  // Modules
  imports: [
    IonicModule.forRoot(AppComponent),
    MomentModule
  ],
  // Main Component
  bootstrap: [ IonicApp ]

Now we can use AmCalendarPipe:

2.20 Use angular2-moment pipe client/imports/pages/chats/chats.component.html
19
20
21
22
23
24
25
 
        <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>

Pipes serves the same proposes as AngularJS' filters and they share exactly the same syntax, only they are called in a different name.

{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/setup" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/meteor-server-side"}}}