Fork me on GitHub

WhatsApp Clone with Meteor and Ionic CLI

Layout, coding style & structure

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

Now that we've finished making our initial setup, let's dive into the code of our app.

First, we will need some helpers which will help us write some AngularJS code using es6's class system. For this purpose we will use angular-ecmascript npm package. Let's install it:

$ npm install angular-ecmascript --save

angular-ecmascript is a utility library which will help us write an AngularJS app using es6's class system. In addition, angular-ecmascript provides us with some very handy features, like auto-injection without using any pre-processors like ng-annotate, or setting our controller as the view model any time it is created (See referene). The API shouldn't be too complicated to understand, and we will get familiar with it as we make progress with this tutorial.

NOTE: As for now there is no best pratice for writing AngularJS es6 code, this is one method we recommend out of many possible other options.

Now that everything is set, let's create the RoutesConfig using the Config module-helper:

2.2 Add initial routes config src/routes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Config } from 'angular-ecmascript/module-helpers';
 
export default class RoutesConfig extends Config {
  static $inject = ['$stateProvider']
 
  configure() {
    this.$stateProvider
      .state('tab', {
        url: '/tab',
        abstract: true,
        templateUrl: 'templates/tabs.html'
      });
  }
}

This will be our main app router which is implemented using angular-ui-router, and anytime we would like to add some new routes and configure them, this is where we do so.

After we define a helper, we shall always load it in the main app file. Let's do so:

2.3 Load routes src/app.js
2
3
4
5
6
7
8
9
10
 
12
13
14
15
16
17
18
19
20
import Ionic from 'ionic';
import Keyboard from 'cordova/keyboard';
import StatusBar from 'cordova/status-bar';
import Loader from 'angular-ecmascript/module-loader';
 
import RoutesConfig from './routes';
 
const App = 'whatsapp';
 
...some lines skipped...
  'ionic'
]);
 
new Loader(App)
  .load(RoutesConfig);
 
Ionic.Platform.ready(() => {
  if (Keyboard) {
    Keyboard.hideKeyboardAccessoryBar(true);

As you can see there is only one route state defined as for now, called tabs, which is connected to the tabs view. Let's add it:

2.4 Add tabs view www/templates/tabs.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ion-tabs class="tabs-stable tabs-icon-top tabs-color-positive" ng-cloak>
  <ion-tab title="Favorites" icon-on="ion-ios-star" icon-off="ion-ios-star-outline" href="#/tab/favorites">
    <ion-nav-view name="tab-favorites"></ion-nav-view>
  </ion-tab>
 
  <ion-tab title="Recents" icon-on="ion-ios-clock" icon-off="ion-ios-clock-outline" href="#/tab/recents">
    <ion-nav-view name="tab-recents"></ion-nav-view>
  </ion-tab>
 
  <ion-tab title="Contacts" icon-on="ion-ios-person" icon-off="ion-ios-person-outline" href="#/tab/contacts">
    <ion-nav-view name="tab-contacts"></ion-nav-view>
  </ion-tab>
 
  <ion-tab title="Chats" icon-on="ion-ios-chatbubble" icon-off="ion-ios-chatbubble-outline" href="#/tab/chats">
    <ion-nav-view name="tab-chats"></ion-nav-view>
  </ion-tab>
 
  <ion-tab title="Settings" icon-on="ion-ios-cog" icon-off="ion-ios-cog-outline" href="#/tab/settings">
    <ion-nav-view name="tab-settings"></ion-nav-view>
  </ion-tab>
</ion-tabs>

In our app we will have 5 tabs: Favorites, Recents, Contacts, Chats, and Settings. In this tutorial we will only focus on implementing the Chats and the Settings tabs, but your'e more than free to continue on with this tutorial and implement the rest of the tabs.

Let's create Chats view which will appear one we click on the Chats tab. But first, let's install an npm package called Moment which is a utility library for manipulating date object. It will soon come in handy:

$ npm install moment --save

Our package.json should look like so:

2.5 Install moment npm package package.json
19
20
21
22
23
24
25
    "lodash.camelcase": "^4.1.1",
    "lodash.upperfirst": "^4.2.0",
    "script-loader": "^0.7.0",
    "moment": "^2.13.0",
    "webpack": "^1.13.0"
  },
  "devDependencies": {

Now that we have installed Moment, we need to expose it to our environment, since some libraries we load which are not using es6's module system rely on it being defined as a global variable. For these purposes we shall use the expose-loader. Simply, add to our index.js file:

2.6 Import and expose moment in index js src/index.js
1
2
3
4
5
// modules
import 'expose?moment!moment';
// libs
import 'script!lib/angular/angular';
import 'script!lib/angular-animate/angular-animate';

After the ? comes the variable name which shuold be defined on the global scope, and after the ! comes the library we would like to load. In this case we load the Moment library and we would like to expose it as window.global.

NOTE: Altough Moment is defined on the global scope, we will keep importing it in every module we wanna use it, since it's more declerative and clearer.

Now that we have Moment lock and loaded, we will create our Chats controller and we will use it to create some data stubs:

2.7 Add chats controller with data stubs src/controllers/chats.controller.js
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
import Moment from 'moment';
import { Controller } from 'angular-ecmascript/module-helpers';
 
export default class ChatsCtrl extends Controller {
  constructor() {
    super(...arguments);
 
    this.data = [
      {
        _id: 0,
        name: 'Ethan Gonzalez',
        picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg',
        lastMessage: {
          text: 'You on your way?',
          timestamp: Moment().subtract(1, 'hours').toDate()
        }
      },
      {
        _id: 1,
        name: 'Bryan Wallace',
        picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg',
        lastMessage: {
          text: 'Hey, it\'s me',
          timestamp: Moment().subtract(2, 'hours').toDate()
        }
      },
      {
        _id: 2,
        name: 'Avery Stewart',
        picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg',
        lastMessage: {
          text: 'I should buy a boat',
          timestamp: Moment().subtract(1, 'days').toDate()
        }
      },
      {
        _id: 3,
        name: 'Katie Peterson',
        picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg',
        lastMessage: {
          text: 'Look at my mukluks!',
          timestamp: Moment().subtract(4, 'days').toDate()
        }
      },
      {
        _id: 4,
        name: 'Ray Edwards',
        picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg',
        lastMessage: {
          text: 'This is wicked good ice cream.',
          timestamp: Moment().subtract(2, 'weeks').toDate()
        }
      }
    ];
  }
}
 
ChatsCtrl.$name = 'ChatsCtrl';

And we will load it:

2.8 Load chats controller src/app.js
4
5
6
7
8
9
10
 
14
15
16
17
18
19
20
import StatusBar from 'cordova/status-bar';
import Loader from 'angular-ecmascript/module-loader';
 
import ChatsCtrl from './controllers/chats.controller';
import RoutesConfig from './routes';
 
const App = 'whatsapp';
...some lines skipped...
]);
 
new Loader(App)
  .load(ChatsCtrl)
  .load(RoutesConfig);
 
Ionic.Platform.ready(() => {

NOTE: From now on any component we create we will also load it right after, without any further explenations.

The data stubs are just a temporary fabricated data which will be used to test our application and how it reacts with it. You can also look at our scheme and figure out how our application is gonna look like.

Now that we have the controller with the data, we need a view to present it. We will use ion-list and ion-item directives, which provides us a list layout, and we will iterate our static data using ng-repeat and we will display the chat's name, image and timestamp.

Let's create it:

2.9 Add chats view www/templates/chats.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ion-view view-title="Chats">
  <ion-content>
    <ion-list>
      <ion-item ng-repeat="chat in chats.data | orderBy:'-lastMessage.timestamp'"
                class="item-chat item-remove-animate item-avatar item-icon-right"
                type="item-text-wrap">
        <img ng-src="{{ chat.picture }}">
        <h2>{{ chat.name }}</h2>
        <p>{{ chat.lastMessage.text }}</p>
        <span class="last-message-timestamp">{{ chat.lastMessage.timestamp }}</span>
        <i class="icon ion-chevron-right icon-accessory"></i>
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>

We also need to define the appropriate route state which will be navigated any time we press the Chats tab. Let's do so:

2.10 Add chats route state src/routes.js
1
2
3
4
5
6
7
 
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Config } from 'angular-ecmascript/module-helpers';
 
export default class RoutesConfig extends Config {
  static $inject = ['$stateProvider', '$urlRouterProvider']
 
  configure() {
    this.$stateProvider
...some lines skipped...
        url: '/tab',
        abstract: true,
        templateUrl: 'templates/tabs.html'
      })
      .state('tab.chats', {
        url: '/chats',
        views: {
          'tab-chats': {
            templateUrl: 'templates/chats.html',
            controller: 'ChatsCtrl as chats'
          }
        }
      });
 
    this.$urlRouterProvider.otherwise('tab/chats');
  }
}

If you look closely we used the controllerAs syntax, which means that our data models should be stored on the controller and not on the scope.

We also used the $urlRouterProvider.otherwise() which defines our Chats state as the default one, so any unrecognized route state we navigate to our router will automatically redirect us to this state.

As for now, our chats' dates are presented in a very messy format which is not very informative for the every-day user. We wanna present it in a calendar format. Inorder to do that we need to define a Filter, which is provided by Angular and responsibe for projecting our data presented in the view. Let's add the CalendarFilter:

2.11 Add calendar filter src/filters/calendar.filter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Moment from 'moment';
import { Filter } from 'angular-ecmascript/module-helpers';
 
export default class CalendarFilter extends Filter {
  static $name = 'calendar'
 
  filter(time) {
    if (!time) return;
 
    return Moment(time).calendar(null, {
      lastDay : '[Yesterday]',
      sameDay : 'LT',
      lastWeek : 'dddd',
      sameElse : 'DD/MM/YY'
    });
  }
}
2.12 Load calendar filter src/app.js
5
6
7
8
9
10
11
 
16
17
18
19
20
21
22
import Loader from 'angular-ecmascript/module-loader';
 
import ChatsCtrl from './controllers/chats.controller';
import CalendarFilter from './filters/calendar.filter';
import RoutesConfig from './routes';
 
const App = 'whatsapp';
...some lines skipped...
 
new Loader(App)
  .load(ChatsCtrl)
  .load(CalendarFilter)
  .load(RoutesConfig);
 
Ionic.Platform.ready(() => {

And now let's apply it to the view:

2.13 Apply calendar filter in chats view www/templates/chats.html
7
8
9
10
11
12
13
        <img ng-src="{{ chat.picture }}">
        <h2>{{ chat.name }}</h2>
        <p>{{ chat.lastMessage.text }}</p>
        <span class="last-message-timestamp">{{ chat.lastMessage.timestamp | calendar }}</span>
        <i class="icon ion-chevron-right icon-accessory"></i>
      </ion-item>
    </ion-list>

As you can see, inorder to apply a filter in the view we simply pipe it next to our data model.

We would also like to be able to remove a chat, let's add a delete button for each chat:

2.14 Add delete button to chats view www/templates/chats.html
9
10
11
12
13
14
15
16
17
        <p>{{ chat.lastMessage.text }}</p>
        <span class="last-message-timestamp">{{ chat.lastMessage.timestamp | calendar }}</span>
        <i class="icon ion-chevron-right icon-accessory"></i>
        <ion-option-button class="button-assertive" ng-click="chats.remove(chat)">
          Delete
        </ion-option-button>
      </ion-item>
    </ion-list>
  </ion-content>

And implement its logic in the controller:

2.15 Implement chat removal logic in chats controller src/controllers/chats.controller.js
53
54
55
56
57
58
59
60
61
62
      }
    ];
  }
 
  remove(chat) {
    this.data.splice(this.data.indexOf(chat), 1);
  }
}
 
ChatsCtrl.$name = 'ChatsCtrl';

Now everything is ready, but it looks a bit dull. Let's add some style to it:

2.16 Add chats stylesheet scss/chats.scss
1
2
3
4
5
6
7
8
9
.item-chat {
  .last-message-timestamp {
    position: absolute;
    top: 16px;
    right: 38px;
    font-size: 14px;
    color: #9A9898;
  }
}

Since the stylesheet was written in SASS, we need to import it into our main scss file:

2.17 Import chats stylesheet scss/ionic.app.scss
21
22
23
24
// Include all of Ionic
@import "www/lib/ionic/scss/ionic";
 
@import "chats";

NOTE: From now on every scss file we write will be imported right after without any further explenations.

Our Chats tab is now ready. You can run it inside a browser, or if you prefer to see it in a mobile layout, you should use Ionic's simulator. Just follow the following instructions:

$ npm install -g ios-sim
$ cordova platform add i The API shouldn't be too complicated to understand, and we will get familiar with it as we make progress with this tutorial.

{{tutorialImage 'ionic' '1.png' 500}}

And if you swipe a menu item to the left:

{{tutorialImage 'ionic' '2.png' 400}}