Fork me on GitHub

WhatsApp Clone with Meteor and Ionic 2 CLI

Ionic 3 Version (Last Update: 2017-06-15)

Realtime Meteor Server

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

Now that we have the initial chats layout and its component, we will take it a step further by providing the chats data from a server instead of having it locally. In this step we will be implementing the API server and we will do so using Meteor.

First make sure that you have Meteor installed. If not, install it by typing the following command:

$ curl https://install.meteor.com/ | sh

We will start by creating the Meteor project which will be placed inside the api dir:

$ meteor create api
$ rm -rf api/node_modules

A Meteor project will contain the following dirs by default:

  • client - A dir containing all client scripts.
  • server - A dir containing all server scripts.

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. Since we're using Ionic's CLI for the client code we have no need in the client dir in the Meteor project. Let's get rid of it:

api$ rm -rf client

Since we don't wanna have duplicate resources between the client and the server, we will remove the package.json file in the api dir:

api$ rm package.json

And we will add a symbolic link between Ionic's node_modules and Meteor's node_modules:

api$ ln -s ../node_modules

Since we will be writing our app using Typescript, we will need to support it in our Meteor project as well, especially when the client and the server share some of the script files. To add this support we will add the following package to our Meteor project:

api$ meteor add barbatus:typescript

We will also need to add a configuration file for the TypeScript compiler in the Meteor server, which is derived from our Ionic app's config:

4.6 Add server's tsconfig file api/tsconfig.json
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
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "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"
    ]
  },
  "exclude": [
    "node_modules"
  ],
  "compileOnSave": false,
  "atom": {
    "rewriteTsconfig": false
  }
}

Since we're using TypeScript, let's change the main server file extension from .js to .ts:

api$ mv server/main.js server/main.ts

Now we will need to create a symbolic link to the declaration file located in src/declarations.d.ts. This way we can share external TypeScript declarations in both client and server. To create the desired symbolic link, simply type the following command in the command line:

api$ ln -s ../src/declarations.d.ts

The following dependencies are required to be installed so our server can function properly:

$ npm install --save babel-runtime
$ npm install --save meteor-node-stubs

Now we'll have to move our models interfaces to the api dir so the server will have access to them as well:

$ mv src/models.ts api/server/models.ts

This requires us to update its reference in the declarations file as well:

4.11 Update the models import path src/pages/chats/chats.ts
1
2
3
4
5
6
7
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import * as moment from 'moment';
import { Chat, MessageType } from 'api/models';
 
@Component({
  templateUrl: 'chats.html'

We will also install the meteor-rxjs package so we can define collections and data streams and TypeScript definitions for Meteor:

$ npm install --save meteor-rxjs
$ npm install --save-dev @types/meteor

To avoid duplicate identifier errors in Ionic we will have to remove meteor-typings from the root's tsconfig.json types array:

4.13 Remove meteor-typings from root's tsconfig.json types array tsconfig.json
20
21
22
23
24
25
    "stripInternal": true,
    "noImplicitAny": false,
    "types": [
      "@types/underscore"
    ]
  },

Collections

This collection is actually a reference to a MongoDB collection, and it is provided to us by a Meteor package called Minimongo, and it shares almost the same API as a native MongoDB collection. In this tutorial we will be wrapping our collections using RxJS's Observables, which is available to us thanks to meteor-rxjs.

Our initial collections are gonna be the chats and messages collection, the one is going to store some chats-models, and the other is going to store messages-models:

4.14 Create chats collection api/server/collections/chats.ts
1
2
3
4
import { MongoObservable } from 'meteor-rxjs';
import { Chat } from '../models';
 
export const Chats = new MongoObservable.Collection<Chat>('chats');
4.15 Added messages collection api/server/collections/messages.ts
1
2
3
4
import { MongoObservable } from 'meteor-rxjs';
import { Message } from '../models';
 
export const Messages = new MongoObservable.Collection<Message>('messages');

We chose to create a dedicated module for each collection, because in the near future there might be more logic added into each one of them. To make importation convenient, we will export all collections from a single file:

4.16 Added main export file api/server/collections/index.ts
1
2
export * from './chats';
export * from './messages';

Now instead of requiring each collection individually, we can just require them from the index.ts file.

Data fixtures

Since we have real collections now, and not dummy ones, we will need to fill them up with some data in case they are empty, so we can test our application properly. Let's create our data fixtures in the server:

4.17 Move stubs data to the server side api/server/main.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
64
65
66
67
68
69
70
71
import { Meteor } from 'meteor/meteor';
import { Chats } from './collections/chats';
import { Messages } from './collections/messages';
import * as moment from 'moment';
import { MessageType } from './models';
 
Meteor.startup(() => {
  if (Chats.find({}).cursor.count() === 0) {
    let chatId;
 
    chatId = Chats.collection.insert({
      title: 'Ethan Gonzalez',
      picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'You on your way?',
      createdAt: moment().subtract(1, 'hours').toDate(),
      type: MessageType.TEXT
    });
 
    chatId = Chats.collection.insert({
      title: 'Bryan Wallace',
      picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'Hey, it\'s me',
      createdAt: moment().subtract(2, 'hours').toDate(),
      type: MessageType.TEXT
    });
 
    chatId = Chats.collection.insert({
      title: 'Avery Stewart',
      picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'I should buy a boat',
      createdAt: moment().subtract(1, 'days').toDate(),
      type: MessageType.TEXT
    });
 
    chatId = Chats.collection.insert({
      title: 'Katie Peterson',
      picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'Look at my mukluks!',
      createdAt: moment().subtract(4, 'days').toDate(),
      type: MessageType.TEXT
    });
 
    chatId = Chats.collection.insert({
      title: 'Ray Edwards',
      picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg'
    });
 
    Messages.collection.insert({
      chatId: chatId,
      content: 'This is wicked good ice cream.',
      createdAt: moment().subtract(2, 'weeks').toDate(),
      type: MessageType.TEXT
    });
  }
});

This behavior is not recommended and should be removed once we're ready for production. A conditioned environment variable may also be a great solution

Note how we use the .collection property to get the actual Mongo.Collection instance. In the Meteor server we want to avoid the usage of observables since it uses fibers. More information about fibers can be found here.

Preparing the Meteor client

In order to connect to the Meteor server, we need a client which is capable of doing so. To create a Meteor client, we will use a bundler called meteor-client-bundler. This bundler, bundles all the necessary Meteor client script files into a single module. This is very useful when we want to interact with Atmosphere packages and integrate them into our client. To install meteor-client-bundler, run the following command:

$ sudo npm install -g meteor-client-bundler

Now we'll add a bundling script to the package.json as followed:

4.18 Created a script for generating the Meteor client bundle package.json
13
14
15
16
17
18
19
20
    "build": "ionic-app-scripts build",
    "lint": "ionic-app-scripts lint",
    "ionic:build": "ionic-app-scripts build",
    "ionic:serve": "ionic-app-scripts serve",
    "meteor-client:bundle": "meteor-client bundle -s api"
  },
  "dependencies": {
    "@angular/common": "4.1.2",

We will also need to install the tmp package:

$ sudo npm install --save-dev tmp

To execute the bundler, simply run:

$ npm run meteor-client:bundle

This will generate a file called meteor-client.js under the node_modules dir, which needs to be imported in our application like so:

4.20 Import meteor client bundle src/app/main.ts
1
2
3
4
5
import 'meteor-client';
 
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 
import { AppModule } from './app.module';

By default, the client will assume that the server is running at localhost:3000. If you'd like to change that, you can simply specify a --url option in the NPM script. Further information can be found here.

The client we've just imported gives us the ability to interact with the server. Let's replace the local chats-data with a data which is fetched from the Meteor server:

4.21 Use server side data src/pages/chats/chats.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
import { Component, OnInit } from '@angular/core';
import { Chats, Messages } from 'api/collections';
import { Chat } from 'api/models';
import { Observable } from 'rxjs';
 
@Component({
  templateUrl: 'chats.html'
})
export class ChatsPage implements OnInit {
  chats;
 
  constructor() {
  }
 
  ngOnInit() {
    this.chats = Chats
      .find({})
      .mergeMap((chats: Chat[]) =>
        Observable.combineLatest(
          ...chats.map((chat: Chat) =>
            Messages
              .find({chatId: chat._id})
              .startWith(null)
              .map(messages => {
                if (messages) chat.lastMessage = messages[0];
                return chat;
              })
          )
        )
      ).zone();
  }
 
  removeChat(chat: Chat): void {

And re-implement the removeChat method using the actual Meteor collection:

4.22 Implement remove chat with the Collection src/pages/chats/chats.ts
31
32
33
34
35
36
37
  }
 
  removeChat(chat: Chat): void {
    Chats.remove({_id: chat._id}).subscribe(() => {
    });
  }
}