Fork me on GitHub

WhatsApp Clone with Meteor CLI and Ionic

Layout, coding style & structure

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

We will start by creating the project’s folder structure, Meteor has a special behavior for certain folders:

  • client - These files will be available only in the client side.
  • server - These files will be available only in the server side.
  • public - These files will be served as is to the client e.g. assets like images, fonts, etc.
  • lib - Any folder named lib (in any hierarchy) will be loaded first.
  • Any other folder name will be included in both client and server and will be used for code-sharing.

So this will be our folder structure to the project:

  • client (client side with AngularJS and Ionic code)
    • scripts
    • templates
    • styles
    • index.html
  • server (server side code only)
  • public (assets, images)
  • lib (define methods and collections in order to make them available in both client and server)

So let’s start by creating our first file, the index.html which will be placed under the client folder:

1.1 Create main html file client/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
  <title>Whatsapp Meteor</title>
</head>
 
<body>
<!--
  The nav bar that will be updated as we navigate between views.
-->
<ion-nav-bar class="bar-stable">
  <ion-nav-back-button>
  </ion-nav-back-button>
</ion-nav-bar>
<!--
  The views will be rendered in the <ion-nav-view> directive below
  Templates are in the /templates folder (but you could also
  have templates inline in this html file if you'd like).
-->
<ion-nav-view></ion-nav-view>
 
</body>

We used some ionic tags to achieve mobile style:

  • ion-nav-bar - Create a navigation bar in the page header.
  • ion-nav-view - This is a placeholder to the real content. AngularJS and Ionic will put your content inside this tag automatically.

Note that we only provide the <head> and <body> tags because Meteor takes care of appending the relevant html parts into one file, and any tag we will use here will be added to Meteor's main index.html file.

This feature is really useful because we do not need to take care of including our files in index.html since it will be maintained automatically.

Our next step is to create the AngularJS module and bootstrap it according to our platform. We will create a new file called app.js.

This bootstrap file should be loaded first, because any other AngularJS code will depend on this module, so we need to put this file inside a folder called lib, so we will create a file in this path: client/scripts/lib/app.js.

In this file we will initialize all the modules we need and load our module-helpers, so any time we create a module-helper it should be loaded here right after.

We will also check for the current platform (browser or mobile) and initialize the module according to the result:

1.2 Create main app file client/scripts/lib/app.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
// Libs
import 'angular-animate';
import 'angular-meteor';
import 'angular-sanitize';
import 'angular-ui-router';
import 'ionic-scripts';
import Angular from 'angular';
import { Meteor } from 'meteor/meteor';
 
// Modules
 
const App = 'Whatsapp';
 
// App
Angular.module(App, [
  'angular-meteor',
  'ionic'
]);
 
// Startup
if (Meteor.isCordova) {
  Angular.element(document).on('deviceready', onReady);
}
else {
  Angular.element(document).ready(onReady);
}
 
function onReady() {
  Angular.bootstrap(document, [App]);
}

Before we dive into building our app's different components, we need a way to write them using es6's new class system. For this purpose we will use angular-ecmascript npm package. Let's install it:

$ meteor npm install angular-ecmascript --save

angular-ecmascript is a utility library which will help us write an AngularJS app using es6's class system. As for now there is no official way to do so, however using es6 syntax is recommended, hence angular-ecmascript was created.

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 reference). The API shouldn't be too complicated to understand, and we will get familiar with it as we make progress with this tutorial.

Our next step is to create the states and routes for the views.

Our app uses Ionic to create 5 tabs: favorites, recents, contacts, chats, and settings.

We will define our routes and states with angular-ui-router (which is included by Ionic), and at the moment we will add the main page which is the chats tab:

1.4 Add initial routes client/scripts/routes.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
import { Config } from 'angular-ecmascript/module-helpers';
 
import chatsTemplateUrl from '../templates/chats.html';
import tabsTemplateUrl from '../templates/tabs.html';
 
export default class RoutesConfig extends Config {
  configure() {
    this.$stateProvider
      .state('tab', {
        url: '/tab',
        abstract: true,
        templateUrl: tabsTemplateUrl
      })
      .state('tab.chats', {
        url: '/chats',
        views: {
          'tab-chats': {
            templateUrl: chatsTemplateUrl
          }
        }
      });
 
    this.$urlRouterProvider.otherwise('tab/chats');
  }
}
 
RoutesConfig.$inject = ['$stateProvider', '$urlRouterProvider'];
1.5 Load routes config client/scripts/lib/app.js
5
6
7
8
9
10
11
12
13
14
15
 
19
20
21
22
23
24
25
26
27
import 'angular-ui-router';
import 'ionic-scripts';
import Angular from 'angular';
import Loader from 'angular-ecmascript/module-loader';
import { Meteor } from 'meteor/meteor';
 
// Modules
import RoutesConfig from '../routes';
 
const App = 'Whatsapp';
 
...some lines skipped...
  'ionic'
]);
 
new Loader(App)
  .load(RoutesConfig);
 
// Startup
if (Meteor.isCordova) {
  Angular.element(document).on('deviceready', onReady);

And this is the HTML template for the footer that includes our tabs:

1.6 Create tabs view client/templates/tabs.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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>

Let's create the stub for our default tab - the chats tab:

1.7 Create chats view client/templates/chats.html
1
2
3
4
5
<ion-view view-title="Chats">
  <ion-content>
 
  </ion-content>
</ion-view>

Our next step will go through creating basic views with some static data using Ionic and css pre-processor called sass.

Let’s create an AngularJS controller that we will connect to the chats view later on, and we will call it ChatsCtrl:

1
2
3
4
5
6
import { Controller } from 'angular-ecmascript/module-helpers';
 
export default class ChatsCtrl extends Controller {
}
 
ChatsCtrl.$name = 'ChatsCtrl';
1.10 Load chats controller client/scripts/lib/app.js
9
10
11
12
13
14
15
 
21
22
23
24
25
26
27
import { Meteor } from 'meteor/meteor';
 
// Modules
import ChatsCtrl from '../controllers/chats.controller';
import RoutesConfig from '../routes';
 
const App = 'Whatsapp';
...some lines skipped...
]);
 
new Loader(App)
  .load(ChatsCtrl)
  .load(RoutesConfig);
 
// Startup

From now on we will use our controller as the view model using the controllerAs syntax, which basically means that instead of defining data models on the $scope we will define them on the controller itself using the this argument. For more information, see AngularJS's docs about ngController.

Now we want to add some static data to this controller, we will use moment package to easily create time object, so let’s add it to the project using this command:

$ meteor npm install moment --save

The moment package will be added to package.json by npm:

1.11 Add moment npm package package.json
12
13
14
15
16
17
18
    "angular-sanitize": "^1.5.8",
    "angular-ui-router": "^0.3.2",
    "ionic-scripts": "^1.3.5",
    "meteor-node-stubs": "~0.2.0",
    "moment": "^2.12.0"
  }
}

Now let’s add the static data to the ChatsCtrl. We will create a stub schema for chats and messages:

1.12 Add data stub to chats controller client/scripts/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';

Connect the chats view to the ChatsCtrl:

1.13 Connect chats controller to chats view client/scripts/routes.js
15
16
17
18
19
20
21
22
        url: '/chats',
        views: {
          'tab-chats': {
            templateUrl: chatsTemplateUrl,
            controller: 'ChatsCtrl as chats'
          }
        }
      });

Note that we used the controllerAs syntax with the chats value. This means that that the controller should be accessed from the scope through a data model called chats, which is just a reference to the scope.

Now we will make the data stubs appear in our view.

We will use Ionic's directives to create a container with a list view (ion-list and ion-item), and add ng-repeat to iterate over the chats:

1.14 Add data stub to chats view client/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>

And this is how it looks like:

{{tutorialImage 'whatsapp-meteor' '3.png' 500}}

You might notice that the dates are not formatted, so let's create a simple AngularJS filter that uses moment npm package to convert the date into a formatted text, we will place it in a file named client/scripts/filters/calendar.filter.js:

1.15 Create calendar filter client/scripts/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 {
  filter(time) {
    if (!time) return;
 
    return Moment(time).calendar(null, {
      lastDay : '[Yesterday]',
      sameDay : 'LT',
      lastWeek : 'dddd',
      sameElse : 'DD/MM/YY'
    });
  }
}
 
CalendarFilter.$name = 'calendar';
1.16 Load calendar filter client/scripts/lib/app.js
10
11
12
13
14
15
16
 
23
24
25
26
27
28
29
 
// Modules
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);
 
// Startup

And let's use it in our view:

1.17 Apply calendar filter to chats view client/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>

To add a delete button to our view, we will use a ion-option-button which is a button that's visible when we swipe over the list item.

1.18 Add delete button to chats view client/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>

Implement the remove(chat) method inside our ChatsCtrl:

1.19 Add delete button logic to chats controller client/scripts/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 we want to add some styles and make some small css modifications to make it look more like Whatsapp.

We want to use sass in our project, so we need to add the sass package to our project:

$ meteor add fourseven:scss

And now we will create our first sass file, we will place it under client/styles/chats.scss, and add some css rules:

1.21 Add chats stylesheet client/styles/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;
  }
}

And we are done with this view! As you can probably see it has a Whatsapp style theme.