If you got directly into here, please read the whole intro section explaining the goals for this tutorial and project.
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.
If you are interested in the Ionic CLI, the steps needed to use it with Meteor are almost identical to the steps required by 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 whatsapp
And let's see what we've got. Go into the new folder:
$ cd whatsapp
A Meteor
project will contain the following dirs by default:
These scripts should be loaded automatically by their alphabetic order on their belonging platform, e.g. a script defined under the client dir should be loaded by Meteor
only on the client. A script defined in neither of these folders should be loaded on both.
Meteor
apps use Blaze as its default template engine and Babel as its default script pre-processor. In this tutorial, we're interested in using Angular 2's template engine and Typescript as our script pre-processor; Therefore will remove the blaze-html-templates
package:
$ meteor remove blaze-html-templates
And we will replace it with a package called angular2-compilers
:
$ meteor add angular2-compilers
The angular2-compilers
package not only replace the Blaze
template engine, but it also applies Typescript
to our Meteor
project, as it's the recommended scripting language recommended by the Angular
team. In addition, all CSS
files will be compiled with a pre-processor called SASS, something which will definitely ease the styling process.
The Typescript
compiler operates based on a user defined config, and without it, it's not going to behave expectedly; Therefore, we will define the following Typescript
config file:
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
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"dom",
"es2015"
],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"target": "es5",
"skipLibCheck": true,
"stripInternal": true,
"noImplicitAny": false,
"types": [
"meteor-typings",
"@types/underscore"
]
},
"include": [
"client/**/*.ts",
"server/**/*.ts",
"imports/**/*.ts"
],
"exclude": [
"node_modules"
],
"compileOnSave": false,
"atom": {
"rewriteTsconfig": false
}
}
More information regards
Typescript
configuration file can be found here.
Not all third-party libraries have any support for Typescript
whatsoever, something that we should be careful with when building a Typescript
app. To allow non-supported third-party libraries, we will add the following declaration file:
1
declare module '*';
Like most libraries, Angular 2
is relied on peer dependencies, which we will have to make sure exist in our app by installing the following packages:
$ meteor npm install --save @angular/common
$ meteor npm install --save @angular/compiler
$ meteor npm install --save @angular/compiler-cli
$ meteor npm install --save @angular/core
$ meteor npm install --save @angular/forms
$ meteor npm install --save @angular/http
$ meteor npm install --save @angular/platform-browser
$ meteor npm install --save @angular/platform-browser-dynamic
$ meteor npm install --save @angular/platform-server
$ meteor npm install --save meteor-rxjs
$ meteor npm install --save reflect-metadata
$ meteor npm install --save rxjs
$ meteor npm install --save zone.js
$ meteor npm install --save-dev @types/meteor
$ meteor npm install --save-dev @types/meteor-accounts-phone
$ meteor npm install --save-dev @types/underscore
$ meteor npm install --save-dev meteor-typings
Now that we have all the compilers and their dependencies ready, we will have to transform our project files into their supported extension. .js
files should be renamed to .ts
files, and .css
files should be renamed to .scss
files.
$ mv server/main.js server/main.ts
$ mv client/main.js client/main.ts
$ mv client/main.css client/main.scss
Last but not least, we will add some basic Cordova plugins which will take care of mobile specific features:
$ meteor add cordova:[email protected]
$ meteor add cordova:[email protected]
$ meteor add cordova:[email protected]
$ meteor add cordova:[email protected]
$ meteor add cordova:[email protected]
$ meteor add cordova:[email protected]
The least above was determined based on Ionic 2's app base.
Everything is set, and we can start using the Angular 2
framework. We will start by setting the basis for our app:
1
2
3
4
5
6
7
8
import { Component } from '@angular/core';
import template from "./app.html";
@Component({
selector: 'my-app',
template
})
export class MyApp {}
1
My App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NgModule, ErrorHandler } from '@angular/core';
import { MyApp } from './app.component';
@NgModule({
declarations: [
MyApp
],
entryComponents: [
MyApp
],
providers: [
{ provide: ErrorHandler }
]
})
export class AppModule {}
1
2
3
4
5
6
// App Global Sass
// --------------------------------------------------
// Put style rules here that you want to apply globally. These
// styles are for the entire app and not just one component.
// Additionally, this file can be also used as an entry point
// to import other Sass files to be included in the output CSS.
3
4
5
6
7
</head>
<body>
<my-app></my-app>
</body>
1
2
// App
@import "imports/app/app";
1
2
3
4
5
6
7
8
9
10
import 'zone.js';
import 'reflect-metadata';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Meteor } from 'meteor/meteor';
import { AppModule } from './imports/app/app.module';
Meteor.startup(() => {
platformBrowserDynamic().bootstrapModule(AppModule);
});
1
2
3
4
5
/*
A wildcard module is declared below to allow third party libraries to be used in an app even if they don't
provide their own type declarations.
*/
declare module '*';
In the step above, we simply created the entry module and component for our application, so as we go further in this tutorial and add more features, we can simply easily extend our module and app main component.
Ionic is a free and open source mobile SDK for developing native and progressive web apps with ease. When using Ionic
CLI, it comes with a boilerplate, just like Meteor
does, but since we're not using Meteor
CLI and not Ionic
's, we will have to set it up manually.
The first thing we're going to do in order to integrate Ionic 2
in our app would be installing its NPM
dependencies:
$ meteor npm install --save @ionic/storage
$ meteor npm install --save ionic-angular
$ meteor npm install --save ionic-native
$ meteor npm install --save ionicons
Ionic
build system comes with a built-in theming system which helps its users design their app. It's a powerful tool which we wanna take advantage of. In-order to do that, we will define the following SCSS
file, and we will import it:
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
// 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/note/note.ios",
"{}/node_modules/ionic-angular/components/note/note.md",
"{}/node_modules/ionic-angular/components/note/note.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";
// Fonts
// --------------------------------------------------
@import
"ionicons",
"{}/node_modules/ionic-angular/fonts/noto-sans",
"{}/node_modules/ionic-angular/fonts/roboto";
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
// Ionicons Icon Font CSS
// --------------------------
// Ionicons CSS for Ionic's <ion-icon> element
// ionicons-icons.scss has the icons and their unicode characters
$ionicons-font-path: $font-path !default;
@import "{}/node_modules/ionicons/dist/scss/ionicons-icons";
@import "{}/node_modules/ionicons/dist/scss/ionicons-variables";
@font-face {
font-family: "Ionicons";
src: url("#{$ionicons-font-path}/ionicons.woff2?v=#{$ionicons-version}") format("woff2"),
url("#{$ionicons-font-path}/ionicons.woff?v=#{$ionicons-version}") format("woff"),
url("#{$ionicons-font-path}/ionicons.ttf?v=#{$ionicons-version}") format("truetype");
font-weight: normal;
font-style: normal;
}
ion-icon {
display: inline-block;
font-family: "Ionicons";
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-style: normal;
font-variant: normal;
font-weight: normal;
line-height: 1;
text-rendering: auto;
text-transform: none;
speak: none;
}
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
// 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
);
// Components
// --------------------------------------------------
@import "components";
// 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";
1
2
3
4
5
// Theme
@import "imports/theme/variables";
// App
@import "imports/app/app";
The
variables.scss
file is hooked to differentIonic
packages located in thenode_modules
dir. It means that the logic already existed, we only bound all the necessary modules together in order to emulateIonic
's theming system.
Ionic
looks for fonts in directory we can't access. To fix it, we will use the 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"extensions": ["eot", "ttf", "woff", "woff2"],
"map": {
"node_modules/ionic-angular/fonts/ionicons.eot": "fonts/ionicons.eot",
"node_modules/ionic-angular/fonts/ionicons.ttf": "fonts/ionicons.ttf",
"node_modules/ionic-angular/fonts/ionicons.woff": "fonts/ionicons.woff",
"node_modules/ionic-angular/fonts/ionicons.woff2": "fonts/ionicons.woff2",
"node_modules/ionic-angular/fonts/noto-sans-bold.ttf": "fonts/noto-sans-bold.ttf",
"node_modules/ionic-angular/fonts/noto-sans-regular.ttf": "fonts/noto-sans-regular.ttf",
"node_modules/ionic-angular/fonts/noto-sans.scss": "fonts/noto-sans.scss",
"node_modules/ionic-angular/fonts/roboto-bold.ttf": "fonts/roboto-bold.ttf",
"node_modules/ionic-angular/fonts/roboto-bold.woff": "fonts/roboto-bold.woff",
"node_modules/ionic-angular/fonts/roboto-light.ttf": "fonts/roboto-light.ttf",
"node_modules/ionic-angular/fonts/roboto-light.woff": "fonts/roboto-light.woff",
"node_modules/ionic-angular/fonts/roboto-medium.ttf": "fonts/roboto-medium.ttf",
"node_modules/ionic-angular/fonts/roboto-medium.woff": "fonts/roboto-medium.woff",
"node_modules/ionic-angular/fonts/roboto-regular.ttf": "fonts/roboto-regular.ttf",
"node_modules/ionic-angular/fonts/roboto-regular.woff": "fonts/roboto-regular.woff"
}
}
Ionic
is set. We will have to make few adjustments in our app in order to use Ionic
, mostly importing its modules and using its components:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import template from "./app.html";
@Component({
template
})
export class MyApp {
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.
if (platform.is('cordova')) {
StatusBar.styleDefault();
Splashscreen.hide();
}
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
@NgModule({
declarations: [
MyApp
],
imports: [
IonicModule.forRoot(MyApp),
],
bootstrap: [IonicApp],
entryComponents: [
MyApp
],
providers: [
{ provide: ErrorHandler, useClass: IonicErrorHandler }
]
})
export class AppModule {}
1
2
3
4
5
6
7
8
9
10
11
<head>
<title>Ionic2-MeteorCLI-WhatsApp</title>
<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></ion-app>
</body>
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.
Meteor
projects come with a package called mobile-experience
by default, which is a bundle of fastclick
, mobile-status-bar
and launch-screen
. The fastclick
package might cause some conflicts between Meteor
and Ionic
's functionality, something which will probably cause some unexpected behavior. To fix it, we're going to remove mobile-experience
and install the rest of its packages explicitly:
$ meteor remove mobile-experience
$ meteor add mobile-status-bar
$ meteor add launch-screen
Ionic
apps are still usable in the browser. You can run them using the command:
$ meteor start
# OR
$ npm start
The app should be running on port 3000
, and can be changed by specifying a --port
option.
{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2-tutorial" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/chats-page"}}}