Fork me on GitHub

WhatsApp Clone with Ionic 2 and Meteor CLI

Legacy Tutorial Version (Last Update: 22.11.2016)

Initial setup

Both Meteor and Ionic took their platform to the next level in tooling. Both provide CLI interface instead of bringing bunch of dependencies and configure build tools. There are also differences between those tools. in this post we will focus on the Meteor CLI.

Start by installing Meteor if you haven't already (See reference).

Now let's create our app -- write this in the command line:

$ meteor create --example angular2-boilerplate whatsapp

Alternatively, use your web browser to access the link: https://github.com/bsliran/angular2-meteor-base Download the template application, and unzip the files inside. Rename the folder to "whatsapp" and place it into the default directory. Or just use clone the repository.

Now let's see what we've got. Go into the new folder:

$ cd whatsapp

It's a boilerplate that you can use anytime you want to create a project based on angular2-meteor.

We are going to add our own files for this tutorial. So let's start by deleting most of the contents in these three folders:

- /client       (delete)
- /server       (delete)
- /both         (delete)

Leave only those files:

- /client/index.html
- /client/main.ts
- /client/imports/app/app.component.ts
- /client/imports/app/app.component.html
- /client/imports/app/app.component.scss
- /client/imports/app/app.module.ts
- /server/imports/server-main/main.ts
- /server/main.ts

Let's now update those files:

  • client/index.html - stays exactly the same

  • client/main.ts - one small change

  • client/imports/app/app.component.ts - stays the same

  • client/imports/app/app.component.html - remove <demo></demo>

  • client/imports/app/app.component.scss - leave it empty

  • client/imports/app/app.module.ts - remove Providers and Components

  • server/main.ts - without any change

  • server/imports/server-main/main.ts - leave only Main class with empty start() method

1.1 Remove boilerplate example files client/imports/app/app.component.html
1
2
3
<div>
    <h1>Hello Angular2-Meteor!</h1>
</div>
1.1 Remove boilerplate example files client/imports/app/app.module.ts
1
2
3
4
5
6
7
8
9
10
11
 
13
14
15
16
17
18
19
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
 
@NgModule({
  // Components, Pipes, Directive
  declarations: [
    AppComponent
  ],
  // Entry Components
  entryComponents: [
...some lines skipped...
  ],
  // Providers
  providers: [
 
  ],
  // Modules
  imports: [
1.1 Remove boilerplate example files client/main.ts
3
4
5
6
7
8
9
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { Meteor } from "meteor/meteor";
import { AppModule } from './imports/app/app.module';
 
enableProdMode();
 
1.1 Remove boilerplate example files package.json
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    "@angular/compiler": "2.1.1",
    "@angular/core": "2.1.1",
    "@angular/forms": "2.1.1",
    "@angular/http": "2.1.1",
    "@angular/platform-browser": "2.1.1",
    "@angular/platform-browser-dynamic": "2.1.1",
    "@angular/platform-server": "2.1.1",
    "@angular/router": "3.1.1",
    "angular2-meteor": "0.7.1",
    "angular2-meteor-polyfills": "0.1.1",
    "angular2-meteor-tests-polyfills": "0.0.2",
    "meteor-node-stubs": "0.2.4",
    "ionic-angular": "^2.0.0-rc.3",
    "ionicons": "^3.0.0",
    "meteor-rxjs": "^0.4.5",
    "reflect-metadata": "0.1.8",
    "rxjs": "5.0.0-beta.12",
1.1 Remove boilerplate example files server/imports/server-main/main.ts
1
2
3
4
5
export class Main {
  start(): void {
 
  }
}

Ionic 2

Our project looks clean now. Since we're going to use Ionic, we have to install a proper package:

$ npm install [email protected] --save

It requires few more dependencies:

$ npm install @angular/{http,platform-server} ionicons --save

Great, we have all packages installed, let's move to the more interesting part.

IonicModule

As you probably know, with Angular 2.0 comes NgModule (see documentation).

Ionic provides their own NgModule, called IonicModule (see documentation).

Let's use the AppComponent as the main component of our app (not the root component):

1.3 Updated the NgModule to use Ionic 2 client/imports/app/app.module.ts
1
2
3
4
5
6
 
17
18
19
20
21
22
23
24
25
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { IonicApp, IonicModule } from "ionic-angular";
 
@NgModule({
  // Components, Pipes, Directive
...some lines skipped...
  ],
  // Modules
  imports: [
    IonicModule.forRoot(AppComponent)
  ],
  // Main Component
  bootstrap: [ IonicApp ]
})
export class AppModule {}

We removed BrowserModule since all the declarations and providers are included in IonicModule.

We also added IonicApp component which is a root component that lives on top of our AppComponent.

Now we have to change the root component's selector inside client/index.html:

1.4 Changed the root Component tag client/index.html
2
3
4
5
6
    <base href="/">
</head>
<body>
  <ion-app>Loading...</ion-app>
</body>

Styles

We need to create our own Ionic stylesheet based on the source:

1.5 Added import for Ionic 2 stylesheet client/styles/ionic.scss
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
@charset "UTF-8";
 
@import "{}/node_modules/ionicons/dist/scss/ionicons.scss";
 
// Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Sass variables found in Ionic's source scss files.
// To view all the possible Ionic variables, see:
// http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/
 
$text-color:        #000;
$background-color:  #fff;
 
 
// Named Color Variables
// --------------------------------------------------
// Named colors makes it easy to reuse colors on various components.
// It's highly recommended to change the default colors
// to match your app's branding. Ionic uses a Sass map of
// colors so you can add, rename and remove colors as needed.
// The "primary" color is the only required color in the map.
 
$colors: (
        primary:    #387ef5,
        secondary:  #32db64,
        danger:     #f53d3d,
        light:      #f4f4f4,
        dark:       #222,
        favorite:   #69BB7B
);
 
// Components
// --------------------------------------------------
 
@import
"{}/node_modules/ionic-angular/components/action-sheet/action-sheet",
"{}/node_modules/ionic-angular/components/action-sheet/action-sheet.ios",
"{}/node_modules/ionic-angular/components/action-sheet/action-sheet.md",
"{}/node_modules/ionic-angular/components/action-sheet/action-sheet.wp";
 
@import
"{}/node_modules/ionic-angular/components/alert/alert",
"{}/node_modules/ionic-angular/components/alert/alert.ios",
"{}/node_modules/ionic-angular/components/alert/alert.md",
"{}/node_modules/ionic-angular/components/alert/alert.wp";
 
@import
"{}/node_modules/ionic-angular/components/app/app",
"{}/node_modules/ionic-angular/components/app/app.ios",
"{}/node_modules/ionic-angular/components/app/app.md",
"{}/node_modules/ionic-angular/components/app/app.wp";
 
@import
"{}/node_modules/ionic-angular/components/backdrop/backdrop";
 
@import
"{}/node_modules/ionic-angular/components/badge/badge",
"{}/node_modules/ionic-angular/components/badge/badge.ios",
"{}/node_modules/ionic-angular/components/badge/badge.md",
"{}/node_modules/ionic-angular/components/badge/badge.wp";
 
@import
"{}/node_modules/ionic-angular/components/button/button",
"{}/node_modules/ionic-angular/components/button/button-icon",
"{}/node_modules/ionic-angular/components/button/button.ios",
"{}/node_modules/ionic-angular/components/button/button.md",
"{}/node_modules/ionic-angular/components/button/button.wp";
 
@import
"{}/node_modules/ionic-angular/components/card/card",
"{}/node_modules/ionic-angular/components/card/card.ios",
"{}/node_modules/ionic-angular/components/card/card.md",
"{}/node_modules/ionic-angular/components/card/card.wp";
 
@import
"{}/node_modules/ionic-angular/components/checkbox/checkbox.ios",
"{}/node_modules/ionic-angular/components/checkbox/checkbox.md",
"{}/node_modules/ionic-angular/components/checkbox/checkbox.wp";
 
@import
"{}/node_modules/ionic-angular/components/chip/chip",
"{}/node_modules/ionic-angular/components/chip/chip.ios",
"{}/node_modules/ionic-angular/components/chip/chip.md",
"{}/node_modules/ionic-angular/components/chip/chip.wp";
 
@import
"{}/node_modules/ionic-angular/components/content/content",
"{}/node_modules/ionic-angular/components/content/content.ios",
"{}/node_modules/ionic-angular/components/content/content.md",
"{}/node_modules/ionic-angular/components/content/content.wp";
 
@import
"{}/node_modules/ionic-angular/components/datetime/datetime",
"{}/node_modules/ionic-angular/components/datetime/datetime.ios",
"{}/node_modules/ionic-angular/components/datetime/datetime.md",
"{}/node_modules/ionic-angular/components/datetime/datetime.wp";
 
@import
"{}/node_modules/ionic-angular/components/fab/fab",
"{}/node_modules/ionic-angular/components/fab/fab.ios",
"{}/node_modules/ionic-angular/components/fab/fab.md",
"{}/node_modules/ionic-angular/components/fab/fab.wp";
 
@import
"{}/node_modules/ionic-angular/components/grid/grid";
 
@import
"{}/node_modules/ionic-angular/components/icon/icon",
"{}/node_modules/ionic-angular/components/icon/icon.ios",
"{}/node_modules/ionic-angular/components/icon/icon.md",
"{}/node_modules/ionic-angular/components/icon/icon.wp";
 
@import
"{}/node_modules/ionic-angular/components/img/img";
 
@import
"{}/node_modules/ionic-angular/components/infinite-scroll/infinite-scroll";
 
@import
"{}/node_modules/ionic-angular/components/input/input",
"{}/node_modules/ionic-angular/components/input/input.ios",
"{}/node_modules/ionic-angular/components/input/input.md",
"{}/node_modules/ionic-angular/components/input/input.wp";
 
@import
"{}/node_modules/ionic-angular/components/item/item",
"{}/node_modules/ionic-angular/components/item/item-media",
"{}/node_modules/ionic-angular/components/item/item-reorder",
"{}/node_modules/ionic-angular/components/item/item-sliding",
"{}/node_modules/ionic-angular/components/item/item.ios",
"{}/node_modules/ionic-angular/components/item/item.md",
"{}/node_modules/ionic-angular/components/item/item.wp";
 
@import
"{}/node_modules/ionic-angular/components/label/label",
"{}/node_modules/ionic-angular/components/label/label.ios",
"{}/node_modules/ionic-angular/components/label/label.md",
"{}/node_modules/ionic-angular/components/label/label.wp";
 
@import
"{}/node_modules/ionic-angular/components/list/list",
"{}/node_modules/ionic-angular/components/list/list.ios",
"{}/node_modules/ionic-angular/components/list/list.md",
"{}/node_modules/ionic-angular/components/list/list.wp";
 
@import
"{}/node_modules/ionic-angular/components/loading/loading",
"{}/node_modules/ionic-angular/components/loading/loading.ios",
"{}/node_modules/ionic-angular/components/loading/loading.md",
"{}/node_modules/ionic-angular/components/loading/loading.wp";
 
@import
"{}/node_modules/ionic-angular/components/menu/menu",
"{}/node_modules/ionic-angular/components/menu/menu.ios",
"{}/node_modules/ionic-angular/components/menu/menu.md",
"{}/node_modules/ionic-angular/components/menu/menu.wp";
 
@import
"{}/node_modules/ionic-angular/components/modal/modal",
"{}/node_modules/ionic-angular/components/modal/modal.ios",
"{}/node_modules/ionic-angular/components/modal/modal.md",
"{}/node_modules/ionic-angular/components/modal/modal.wp";
 
@import
"{}/node_modules/ionic-angular/components/picker/picker",
"{}/node_modules/ionic-angular/components/picker/picker.ios",
"{}/node_modules/ionic-angular/components/picker/picker.md",
"{}/node_modules/ionic-angular/components/picker/picker.wp";
 
@import
"{}/node_modules/ionic-angular/components/popover/popover",
"{}/node_modules/ionic-angular/components/popover/popover.ios",
"{}/node_modules/ionic-angular/components/popover/popover.md",
"{}/node_modules/ionic-angular/components/popover/popover.wp";
 
@import
"{}/node_modules/ionic-angular/components/radio/radio.ios",
"{}/node_modules/ionic-angular/components/radio/radio.md",
"{}/node_modules/ionic-angular/components/radio/radio.wp";
 
@import
"{}/node_modules/ionic-angular/components/range/range",
"{}/node_modules/ionic-angular/components/range/range.ios",
"{}/node_modules/ionic-angular/components/range/range.md",
"{}/node_modules/ionic-angular/components/range/range.wp";
 
@import
"{}/node_modules/ionic-angular/components/refresher/refresher";
 
@import
"{}/node_modules/ionic-angular/components/scroll/scroll";
 
@import
"{}/node_modules/ionic-angular/components/searchbar/searchbar",
"{}/node_modules/ionic-angular/components/searchbar/searchbar.ios",
"{}/node_modules/ionic-angular/components/searchbar/searchbar.md",
"{}/node_modules/ionic-angular/components/searchbar/searchbar.wp";
 
@import
"{}/node_modules/ionic-angular/components/segment/segment",
"{}/node_modules/ionic-angular/components/segment/segment.ios",
"{}/node_modules/ionic-angular/components/segment/segment.md",
"{}/node_modules/ionic-angular/components/segment/segment.wp";
 
@import
"{}/node_modules/ionic-angular/components/select/select",
"{}/node_modules/ionic-angular/components/select/select.ios",
"{}/node_modules/ionic-angular/components/select/select.md",
"{}/node_modules/ionic-angular/components/select/select.wp";
 
@import
"{}/node_modules/ionic-angular/components/show-hide-when/show-hide-when";
 
@import
"{}/node_modules/ionic-angular/components/slides/slides";
 
@import
"{}/node_modules/ionic-angular/components/spinner/spinner",
"{}/node_modules/ionic-angular/components/spinner/spinner.ios",
"{}/node_modules/ionic-angular/components/spinner/spinner.md",
"{}/node_modules/ionic-angular/components/spinner/spinner.wp";
 
@import
"{}/node_modules/ionic-angular/components/tabs/tabs",
"{}/node_modules/ionic-angular/components/tabs/tabs.ios",
"{}/node_modules/ionic-angular/components/tabs/tabs.md",
"{}/node_modules/ionic-angular/components/tabs/tabs.wp";
 
@import
"{}/node_modules/ionic-angular/components/toast/toast",
"{}/node_modules/ionic-angular/components/toast/toast.ios",
"{}/node_modules/ionic-angular/components/toast/toast.md",
"{}/node_modules/ionic-angular/components/toast/toast.wp";
 
@import
"{}/node_modules/ionic-angular/components/toggle/toggle.ios",
"{}/node_modules/ionic-angular/components/toggle/toggle.md",
"{}/node_modules/ionic-angular/components/toggle/toggle.wp";
 
@import
"{}/node_modules/ionic-angular/components/toolbar/toolbar",
"{}/node_modules/ionic-angular/components/toolbar/toolbar-button",
"{}/node_modules/ionic-angular/components/toolbar/toolbar.ios",
"{}/node_modules/ionic-angular/components/toolbar/toolbar.md",
"{}/node_modules/ionic-angular/components/toolbar/toolbar.wp";
 
@import
"{}/node_modules/ionic-angular/components/typography/typography",
"{}/node_modules/ionic-angular/components/typography/typography.ios",
"{}/node_modules/ionic-angular/components/typography/typography.md",
"{}/node_modules/ionic-angular/components/typography/typography.wp";
 
@import
"{}/node_modules/ionic-angular/components/virtual-scroll/virtual-scroll";
 
 
// Platforms
// --------------------------------------------------
@import
"{}/node_modules/ionic-angular/platform/cordova",
"{}/node_modules/ionic-angular/platform/cordova.ios",
"{}/node_modules/ionic-angular/platform/cordova.md",
"{}/node_modules/ionic-angular/platform/cordova.wp";
 
 
// Ionic Variables and Theming. For more info, please see:
// http://ionicframework.com/docs/v2/theming/
@import "{}/node_modules/ionic-angular/themes/ionic.globals.scss";
 
 
// App Theme
// --------------------------------------------------
// Ionic apps can have different themes applied, which can
// then be future customized. This import comes last
// so that the above variables are used and Ionic's
// default are overridden.
 
@import "{}/node_modules/ionic-angular/themes/ionic.theme.default.scss";
 

You can just copy paste it.

Fonts

Ionic looks for fonts in directory we can't access. To fix it, we will use mys:font package to teach Meteor how to put them there.

$ meteor add mys:fonts

That plugin needs to know which font we want to use and where it should be available.

Configuration is pretty easy, you will catch it by just looking on an example:

1.7 Added fonts file declaration fonts.json
1
2
3
4
5
6
7
8
9
10
11
{
  "map": {
    "node_modules/ionic-angular/fonts/roboto-medium.ttf": "fonts/roboto-medium.ttf",
    "node_modules/ionic-angular/fonts/roboto-regular.ttf": "fonts/roboto-regular.ttf",
    "node_modules/ionic-angular/fonts/roboto-medium.woff": "fonts/roboto-medium.woff",
    "node_modules/ionic-angular/fonts/roboto-regular.woff": "fonts/roboto-regular.woff",
    "node_modules/ionicons/dist/fonts/ionicons.woff": "fonts/ionicons.woff",
    "node_modules/ionicons/dist/fonts/ionicons.woff2": "fonts/ionicons.woff2",
    "node_modules/ionicons/dist/fonts/ionicons.ttf": "fonts/ionicons.ttf"
  }
}

Now roboto-medium.ttf is available under http://localhost:3000/fonts/roboto-medium.ttf.

Native

Yes, with Ionic you're able to use any native functionality you need.

$ npm install ionic-native --save

Now we can use one of those functionalities. Let's work with Status Bar:

1.9 Basic native functionality added client/imports/app/app.component.ts
1
2
3
4
5
6
 
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';
import { Platform } from "ionic-angular";
import { StatusBar } from "ionic-native";
import template from './app.component.html';
 
@Component({
...some lines skipped...
  template
})
export class AppComponent {
  constructor(platform: Platform) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      StatusBar.styleDefault();
    });
  }
}

Do a quick overview:

  • platform.ready returns a Promise and tell us that the platform is ready and our plugins are available
  • StatusBar.styleDefault() makes the app uses the default statusbar (dark text, for light backgrounds).

Mobile platform

To add mobile support, select the platform(s) you want and run the following command:

$ meteor add-platform ios
# OR / AND
$ meteor add-platform android

To run an app in the emulator use:

$ meteor run ios
# OR
$ meteor run android

To learn more about Mobile in Meteor read the "Mobile" chapter of the Meteor Guide.

We also need to add few meta tags:

1.11 Added missing Ionic 2 meta tags client/index.html
1
2
3
4
5
6
7
8
9
10
<head>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="theme-color" content="#4e8ef7">
</head>
<body>
<ion-app>Loading...</ion-app>
</body>

Now, in order to get smooth mobile experience in Ionic 2, we need to make some modifications to Meteor's default packages.

Meteor comes with a mobile support package called mobile-experience which is a bundle for three packages: fastclick, mobile-status-bar and launch-screen, and we need to remove fastclick in order to get better result.

So let's make those changes:

$ meteor remove mobile-experience
$ meteor add mobile-status-bar
$ meteor add launch-screen

Web

You can still use Ionic app in a browser. Just run:

$ meteor

Or with usage of npm script we have predefined in the boilerplate at the very beginning:

$ npm start

Go to http://localhost:3000 to play with the app.

{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2-tutorial" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/chats-page"}}}