Fork me on GitHub

WhatsApp Clone with Meteor and Ionic CLI

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 layout and some dummy data, let’s create a Meteor server and connect to it to make our app real.

First download Meteor from the Meteor site: https://www.meteor.com/

Now let’s create a new Meteor server inside our project.

Open the command line in our app’s root folder and type:

$ meteor create api

We just created a live and ready example Meteor app inside an api folder.

As you can see Meteor provides with an example app. Since non of it is relevant to us, you can go ahead and delete:

$ cd api
$ rm .gitignore
$ rm package.json
$ rm -rf node_modules
$ rm -rf client
$ rm -rf server

By now you probably noticed that Meteor uses npm, just like the our Ionic project. Since we don't want our client and server to be seperated and require a duplicated installation for each package we decide to add, we'll need to find a way to make them share the same resource.

We will acheive that by symbolic linking the node_modules dir:

$ cd api
$ ln -s ../node_modules

NOTE: Our symbolic link needs to be relative, otherwise it won't work on other machines cloning the project.

Don't forget to reinstall Meteor's node dependencies after we deleted the node_modules dir:

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

Our package.json should look like this:

3.3 Install api dependencies package.json
19
20
21
22
23
24
25
    "lodash.camelcase": "^4.1.1",
    "lodash.upperfirst": "^4.2.0",
    "script-loader": "^0.7.0",
    "meteor-node-stubs": "^0.2.3",
    "moment": "^2.13.0",
    "webpack": "^1.13.0"
  },

Now we are ready to write some server code.

Let’s define two data collections, one for our Chats and one for their Messages.

We will define them inside a dir called server in the api, since code written under this dir will be bundled only for server side by Meteor's build system. We have no control of it and therefore we can't change this layout. This is one of Meteor's disadvantages, that it's not configurable, so we will have to fit ourselves into this build strategy.

Let's go ahead and create the collections.js file:

3.4 Add messages and chats collections to api api/server/collections.js
1
2
3
4
import { Mongo } from 'meteor/mongo';
 
export const Chats = new Mongo.Collection('chats');
export const Messages = new Mongo.Collection('messages');

Now we will update our webpack.config.js to handle some server logic:

3.5 Update webpack config to handle api code webpack.config.js
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
  resolve: {
    extensions: ['', '.js'],
    alias: {
      lib: __dirname + '/www/lib',
      api: __dirname + '/api/server'
    }
  }
};
 
function resolveExternals(context, request, callback) {
  return meteorPack(request, callback) ||
         cordovaPlugin(request, callback) ||
         callback();
}
 
function meteorPack(request, callback) {
  var match = request.match(/^meteor\/(.+)$/);
  var pack = match && match[1];
 
  if (pack) {
    callback(null, 'Package["' + pack + '"]' );
    return true;
  }
}
 
function cordovaPlugin(request, callback) {
  var match = request.match(/^cordova\/(.+)$/);
  var plugin = match && match[1];

We simply added an alias for the api/server folder and a custom handler for resolving Meteor packages. This gives us the effect of combining client side code with server side code, something that is already built-in in Meteor's cli, only this time we created it.

Now that the server side is connected to the client side, we will also need to watch for changes over there and re-build our client code accordingly.

To do so, we will have to update the watched paths in the gulpfile.js:

3.6 Update watch paths in gulp file gulpfile.js
11
12
13
14
15
16
17
var webpackConfig = require('./webpack.config');
 
var paths = {
  webpack: ['./src/**/*.js', '!./www/lib/**/*', './api/server/**/*.js'],
  sass: ['./scss/**/*.scss']
};
 

Let’s bring Meteor's powerful client side tools that will help us easily sync to the Meteor server in real time.

Navigate the command line into your project’s root folder and type:

$ npm install meteor-client-side --save
$ npm install angular-meteor --save

Notice that we also installed angular-meteor package which will help us bring Meteor's benefits into an Angular project.

Our package.json should look like so:

3.7 Install meteor client dependencies package.json
4
5
6
7
8
9
10
 
19
20
21
22
23
24
25
26
27
28
  "description": "whatsapp: An Ionic project",
  "dependencies": {
    "angular-ecmascript": "0.0.3",
    "angular-meteor": "^1.3.11",
    "babel": "^6.5.2",
    "babel-core": "^6.7.6",
    "babel-loader": "^6.2.4",
...some lines skipped...
    "gulp-sass": "^2.0.4",
    "lodash.camelcase": "^4.1.1",
    "lodash.upperfirst": "^4.2.0",
    "meteor-client-side": "^1.3.4",
    "meteor-node-stubs": "^0.2.3",
    "moment": "^2.13.0",
    "script-loader": "^0.7.0",
    "webpack": "^1.13.0"
  },
  "devDependencies": {

Don't forget to import the packages we've just installed in the index.js file:

3.8 Import meteor client dependencies in index js src/index.js
7
8
9
10
11
12
13
import 'script!lib/angular-ui-router/release/angular-ui-router';
import 'script!lib/ionic/js/ionic';
import 'script!lib/ionic/js/ionic-angular';
import 'script!meteor-client-side/dist/meteor-client-side.bundle';
import 'script!angular-meteor/dist/angular-meteor.bundle';
// app
import './app';

We will also need to load angular-meteor into our app as a module dependency, since that's how Angular's module system works:

3.9 Add angular-meteor to app dependencies src/app.js
11
12
13
14
15
16
17
const App = 'whatsapp';
 
Angular.module(App, [
  'angular-meteor',
  'ionic'
]);
 

Now instead of mocking a static data in the controller, we can mock it in the server.

Create a file named bootstrap.js inside the api/server dir and place the following initialization code inside:

3.10 Mock initial data in chats and messages collections api/server/bootstrap.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
59
60
61
62
63
64
65
66
import Moment from 'moment';
import { Meteor } from 'meteor/meteor';
import { Chats, Messages } from './collections';
 
Meteor.startup(function() {
  if (Chats.find().count() !== 0) return;
 
  Messages.remove({});
 
  const messages = [
    {
      text: 'You on your way?',
      timestamp: Moment().subtract(1, 'hours').toDate()
    },
    {
      text: 'Hey, it\'s me',
      timestamp: Moment().subtract(2, 'hours').toDate()
    },
    {
      text: 'I should buy a boat',
      timestamp: Moment().subtract(1, 'days').toDate()
    },
    {
      text: 'Look at my mukluks!',
      timestamp: Moment().subtract(4, 'days').toDate()
    },
    {
      text: 'This is wicked good ice cream.',
      timestamp: Moment().subtract(2, 'weeks').toDate()
    }
  ];
 
  messages.forEach((m) => {
    Messages.insert(m);
  });
 
  const chats = [
    {
      name: 'Ethan Gonzalez',
      picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg'
    },
    {
      name: 'Bryan Wallace',
      picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg'
    },
    {
      name: 'Avery Stewart',
      picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg'
    },
    {
      name: 'Katie Peterson',
      picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg'
    },
    {
      name: 'Ray Edwards',
      picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg'
    }
  ];
 
  chats.forEach((chat) => {
    const message = Messages.findOne({ chatId: { $exists: false } });
    chat.lastMessage = message;
    const chatId = Chats.insert(chat);
    Messages.update(message._id, { $set: { chatId } });
  });
});

The code is pretty easy and self explanatory.

Let’s bind the collections to our ChatsCtrl.

We will use Scope.helpers(), each key will be available on the template and will be updated when it changes. Read more about helpers in our API.

3.11 Fetch data from chats collection in chats controller src/controllers/chats.controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Chats } from 'api/collections';
import { Controller } from 'angular-ecmascript/module-helpers';
 
export default class ChatsCtrl extends Controller {
  constructor() {
    super(...arguments);
 
    this.helpers({
      data() {
        return Chats.find();
      }
    });
  }
 
  remove(chat) {
    this.data.remove(chat._id);
  }
}
 

NOTE: These are exactly the same collections as the server's. Adding meteor-client-side to our project has created a Minimongo on our client side. Minimongo is a client side cache with exactly the same API as Mongo's API. Minimongo will take care of syncing the data automatically with the server.

NOTE: meteor-client-side will try to connect to localhost:3000 by default. To change it, simply set a global object named __meteor_runtime_config__ with a property called DDP_DEFAULT_CONNECTION_URL and set whatever server url you'd like to connect to.

TIP: You can have a static separate front end app that works with a Meteor server. you can use Meteor as a back end server to any front end app without changing anything in your app structure or build process.

Now our app with all its clients is synced with our server in real time!

To test it, you can open another browser, or another window in incognito mode, open another client side by side and delete a chat (by swiping the chat to the left and clicking delete).

See the chat is being deleted and updated in all the connected client in real time!

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