Toby Vervaart

Feb 24, 2014

Build a real-time chat application with Meteor

Important: The application described in this article is only compatible with Meteor 0.8.

I’ve been enjoying learning Meteor and since I generally learn by doing I decided to have a go at building a real-time chat application. This article will take you through what I’ve learned along the way and should explain how to set up and complete a full application in Meteor.

You can find the finished app at http://meteorchat.herokuapp.com/ and the source code at (https://github.com/hellotoby/meteor-chat). If I’ve made any mistakes or any code can be optimised, please feel free to fork the code and issue a pull request.

What we’ll be building

This will be a basic application with only three templates.

The finished chat room

Setting up the project

So let’s start by creating a new project. Browse to the directory where you’d like the project to live and from the command line run meteor create <project name>.

Meteor will put some very basic code in the directory. It’s safe just to delete all this code and set up a custom structure which will help us structure the application a little more cleanly (as described in my previous post Rapid prototyping with Meteor).

We’ll use the following structure for this project:

/client
    /client/stylesheets
    /client/views
/collections
/lib
/server

It’s important to note that meteor handles some of these folders in a special way. Files in the client folder are only available to the client. Similarly, files in the server folder are only available on the server and are inaccessible by the client. Finally, the lib folder is special as it’s contents are loaded first by Meteor.

Removing insecure packages

Next we’ll go ahead and remove Meteor’s insecure packages. By default Meteor comes with the autopublish package installed. This package sends all data to the client, which can include data you wouldn’t want the client to normally have access to. What this does is allow you to get up and running quickly without having to worry about publications and subscriptions. Similarly the Meteor insecure package let’s you do any kind of database insert, update or delete from anywhere in your application. In a production environment this can be very destructive.

In my opinion it’s better to get rid of these packages immediately and start coding as if the application was in production. This will help you to get a better understanding of how publications, subscriptions and allowed database methods work, and won’t leave you scratching your head if you remove these packages later and then your app stops working.

To remove the packages run the following command. meteor remove autopublish insecure

Installing required packages

Next we’ll install the packages we need for this application. The application has the following requirements:

So let’s go ahead and install the packages. Firstly the Meteor standard package for Twitter authentication.

meteor add accounts-twitter

Then the Meteorite packages.

mrt add bootstrap-3 iron-router iron-router-auth accounts-ui-bootstrap-3 livestamp-hs houston

Tip: You can list all the packages that your application is currently using by running meteor list --using

Defining routes and building the UI

The next step in my workflow is always to define the routes for the application and build non-reactive UI templates before adding reactivity later.

Routes

Let’s start by creating a file named router.js inside our /lib folder. (Remember the lib folder is always loaded first by Meteor). This file will, unsurprisingly, contain all our routes.

Firstly we’ll add some boilerplate configuration for the router.

Router.configure({
    layoutTemplate: 'layout'/*,
    notFoundTemplate: 'notFound',
    loadingTemplate: 'loading'*/
});
/lib/router.js

In this case I’m not going to use the notFoundTemplate or the loadingTemplate.

Next we need to define a route for each of the pages we intend to build. We need a homepage, a list of chat rooms and a chat room page.

Router.map(function() {
    /**
    * Homepage
    * Path: /
    * Redirects user on login.
    */
    this.route('home', {
        path            : '/',
        template        : 'home',
        redirectOnLogin : true
    });
    /**
    * loginRedirectRoute
    * Path: none
    * Sends user to rooms list on login.
    */
    this.route('loginRedirectRoute', {
        action          : function() {
            Router.go('/rooms');
        }
    });
    /**
    * Room
    * Path: /room/[a-zA-Z0-9]
    * Default chat room page.
    */
    this.route('room', {
        path            : '/room/:_id',
        template        : 'room',
        loginRequired   : 'home'
    });
    /**
    * Room
    * Path: /rooms
    * Lists all available chat rooms.
    */
    this.route('rooms', {
        path            : '/rooms',
        template        : 'roomList',
        loginRequired   : 'home'
    });
});
/lib/router.js

For each route we specify a path, a template and the loginRequired parameter. loginRequired is a paramater provided by the iron-router-auth package and lets us restrict a route based on whether the user is logged in or not, if they aren’t logged in then we provide a route name to redirect them to.

Also provided by iron-router-auth is the loginRedirectRoute which lets us specify where we want to send the user after they login to the application.

Templates

Main

The main template is very basic. First create a file in /views called main.html. In this file put the following:

<head>
    <title>MeteorChat!</title>
</head>
/client/views/main.html

Since Meteor handles our doctype and other head information, and the default layout used by Iron Router handles our templating, this is all that’s required for our main file.

Default layout

The default layout is used by Iron Router as the basis for all templates. Iron Router uses the special handlebars tag {{yield}} to specify where all your other templates will be loaded relative to the default template. Create the file /client/views/default.html and add the following code.

<template name="layout">
    <header class="main">
        <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
            <div class="container">
                <a href="#" class="navbar-brand">MeteorChat!</a>
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    {{"{{#if currentUser"}}}}
                    <ul class="nav navbar-nav navbar-left">
                        <li><a href="{{pathFor 'rooms'}}" title="Videos">Chat Rooms</a></li>
                    </ul>
                    {{"{{/if"}}}}
                    <ul class="nav navbar-nav navbar-right">
                        {{"{{loginButtons"}}}}
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <section class="main">
        <!-- Our Iron Router templates will be rendered here -->
        {{"{{yield"}}}}
    </section>
</template>
/client/views/default.html

Ignore the handlebars tags in this template for the time being. These are for user authentication and will be explained further on in this article.

Home

Next we’ll create a template for our homepage. Nothing much exists on this page except for some text instructing the user how to log in.

<template name="home">
    <div class="container">
        <h1>MeteorChat!</h1>
        <p>Welcome to MeteorChat!.</p>
        <p>Please login with your Twitter account.</p>
    </div>
</template>
/client/views/home.html

Room List

Now for the room list. This template will contain a list of the chat rooms and a form to create a new chat room. There’s some logic in this template which iterates over each room in the rooms object.

<template name="roomList">
    <div class="container">
        <div class="row">
            <ul class="list-group">
                {{"{{#each rooms"}}}}
                     <li class="list-group-item">
                        <a href="{{"{{pathFor 'room'"}}}}" title="{{"{{name"}}}}" data-id="{{"{{_id"}}}}">{{"{{name"}}}}</a>
                        <span class="badge">0 users</span>
                    </li>
                {{"{{/each"}}}}
            </ul>
        </div>
        <hr />
        <div class="row">
            <div class="form-group">
                <label>Create a new room</label>
                <input class="form-control room-name" type="text" placeholder="Room name" required />
            </div>
            <div class="form-group clearfix">
                <input type="submit" class="create-room btn btn-primary pull-right" value="Create" />
            </div>
        </div>
    </div>
</template>
/client/views/roomlist.html

Chat Room

The final template is for the chat room itself. It contains some logic to iterate over each message for the room and also the users who are in the room.

<template name="room">
    <div class="container">
        <div class="row">
            <div class="messages-container">
                <div class="messages">
                    {{"{{#if messages"}}}}
                        {{"{{#each messages"}}}}
                            <p>
                                <span class="username">{{"{{user"}}}}:</span>
                                {{"{{content"}}}}
                                <span class="timestamp">&ndash; <span data-livestamp="{{"{{creation_date"}}}}"></span></span>
                            </p>
                        {{"{{/each"}}}}
                    {{"{{/if"}}}}
                </div>
                <ul class="user-list">
                    {{"{{#if roomusers"}}}}
                        {{"{{#each roomusers"}}}}
                            {{"{{#if away"}}}}
                            <li class="away">{{"{{user"}}}} <span>&ndash; away</span></li>
                            {{"{{else"}}}}
                            <li>{{"{{user"}}}}</li>
                            {{"{{/if"}}}}
                        {{"{{/each"}}}}
                    {{"{{/if"}}}}
                </ul>
            </div>
        </div>
        <div class="row">
            <div class="add-message">
                <p class="pull-left">
                    Logged in as: <b>{{username}}</b>
                </p>
                <p class="pull-right">
                    <input type="checkbox" class="away-toggle" />
                    <label>Set away</label>
                </p>
                <div class="form-group">
                    <textarea class="form-control message" placeholder="Write a message"></textarea>
                </div>
                <div class="form-group clearfix">
                    <input type="submit" class="btn btn-primary pull-right send-message" style="margin-left: 10px;" value="Send message" />
                    <a href="{{pathFor 'rooms'}}" class="btn btn-danger pull-right">Disconnect</a>
                </div>
            </div>
        </div>
    </div>
</template>
/client/views/room.html

User authentication

Now that we’ve configured our routes the next thing to do is to add user authentication. If you try browsing to one of the routes for example /rooms you’ll find that you’re unable to get there without logging in. This is because of the Iron Router Auth setting in our router which explicitly requires a logged in user.

Luckily adding user authentication is really easy. You probably noticed the handlebars tag {{loginButtons}} in our default template. This tag tells the accounts-ui package to display the login buttons here. There’s a nifty helper to guide you through the process of setting up Twitter OAuth authentication. Just follow the instructions on setting up a new Twitter application, add your api keys and away you go.

Adding collections

So now we have our basic templates and routes configured and our user authentication sorted out, it’s time to think about our database collections.

We’ll need three collections for this application, rooms which will contain the data for each chat room, messages which will contain all our messages and roomusers which will contain an up to date list of the users in each chat room.

The data model for our collections looks something like this.

Rooms                   Messages                RoomUsers
- roomId                - user                  - room
- name                  - room                  - user
- creation_date         - content               - away
                        - creation_date

Creating each collection is very easy. In the /collections folder create three files, messages.js, rooms.js and roomusers.js.

Each file should have the corresponding code.

Messages = new Meteor.Collection('messages');
/collections/messages.js
Rooms = new Meteor.Collection('rooms');
/collections/rooms.js
RoomUsers = new Meteor.Collection('roomusers');
/collections/roomusers.js

And that’s it. When we need to perform an operation to any of these collections we’ll refer to them by their global variable name.

Publications and subscriptions

Since we removed the Meteor autopublish and insecure packages at the beginning of the project we won’t be able to perform any operations on our collections. In order to give ourselves access to the collections we need specifically publish the datasets and also define which operations are allowed.

Server side subscriptions

To allow users to subscribe to our data, we need to publish it. Create a new file in /server/subscriptions.js and add the following code.

/**
* All chat rooms are public. Publish all of them.
*/
Meteor.publish('allRooms', function() {
    return Rooms.find();
});
/**
* Publish messages by room id.
*/
Meteor.publish('roomMessages', function(roomId) {
    return Messages.find({ room : roomId });
});
/**
* Publish room users by room id.
*/
Meteor.publish('roomUsers', function(roomId) {
    return RoomUsers.find({ room : roomId });
});
/server/subscriptions.js

Next we need to define what operations are allowed for our collections. In the same file add the following.

/**
* Allow messages to be added.
*/
Messages.allow({
    'insert' : function() {
        return true;
    }
});
/**
* Allow chat rooms to be added.
*/
Rooms.allow({
    'insert' : function() {
        return true;
    }
});
/**
* Allow room users to be added, updated and removed.
*/
RoomUsers.allow({
    'insert' : function() {
        return true;
    },
    'remove' : function() {
        return true;
    },
    'update' : function() {
        return true;
    }
});
/server/subscriptions.js

Client side subscriptions

Since we only want to send and update information relative to the chat room the user is currently in, we need to specifically subscribe the user to that data when they enter the chat room. This is done through Iron Router’s waitOn method.

this.route('room', {
    path            : '/room/:_id',
    template        : 'room',
    loginRequired   : 'home',
    waitOn          : function() {
        return Meteor.subscribe('roomMessages', this.params._id);
    },
    data            : function() {
        var roomMessages    = Messages.find({ room : this.params._id }, {sort : {creation_date : 'desc'}});
        return {
            messages    : roomMessages,
        }
    },
    action          : function() {
        // Set the Session
        Session.set('roomId', this.params._id);
        // Insert the current user into the room
        var username      = Session.get('userName');
        var roomid        = this.params._id;
        roomuser = {
            user            : username,
            room            : roomid,
            away            : false
        };
        var roomuserid = RoomUsers.insert(roomuser);
        Session.set('userRoomId', roomuserid);
        Meteor.subscribe('roomUsers', this.params._id);
        // Render the view
        this.render();
    },
    unload          : function() {
        // Remove the user from the list of users.
        var roomUserId = Session.get('userRoomId');
        RoomUsers.remove({ _id : roomUserId });
        Session.set('roomId', null);
    }
});
/lib/router.js

As you can see the waitOn method allows us to subscribe to the messages for only the chat room the user selects.

Retrieving data

So far, we’ve built our templates, configured the routes for those templates, set up our collections and configured the publication of data. But how do we put that into our templates?

In this application we’ll send all our data through to our templates via Iron Router, which is the easiest and cleanest way I’ve found so far.

To send data through to the templates we need to use the data method of the route. See below for an example.

this.route('rooms', {
    path            : '/rooms',
    template        : 'roomList',
    loginRequired   : 'home',
    action          : function() {
        var username = Meteor.user().profile.name;
        Session.set('userName', username);
        this.render();
    },
    data            : function() {
        var roomsList = Rooms.find({}, {sort : {creation_date : 'desc'}});
        return {
            rooms : roomsList
        }
    }
});
/lib/router.js

Here you can see that from within the router we search for the list of rooms and send them through to our template.

Also note the action method. This allows us to set up some session data prior to rendering the view.

Deployment

Meteor.com

Deploying to meteor.com is ridiculously easy. Just run the command meteor deploy <projectname>.meteor.com

Heroku

Assuming you already have Heroku Toolbelt installed, you can run the following commands to build your app on Heroku.

First create your new application on Heroku using the buildpack from oortcloud.

heroku create <project-name> --stack cedar --buildpack https://github.com/oortcloud/heroku-buildpack-meteorite.git

Next you need to set the ROOT_URL environment variable.

heroku config:add ROOT_URL=http://your.domain.com

Then just git push heroku master as normal.

Note: You’ll need a verified account as the buildpack needs to configure a MongoHQ sandbox account.

Things not covered

So far I’ve talked about routes, templates, subscriptions, publications and deployment, but there have been a few things I’ve glossed over to avoid making a novel out of this article. The main piece of functionality that I haven’t described in too much detail is session management. I use sessions in this project mainly to remember which user is in which room and it should be reasonably self-explanatory in the source code.

Bugs

There is one noticable bug in the application, and it’s to do with displaying the users in the chat room. If a user decides to leave the chat room by closing the browser, then currently the application has now way of knowing that the user has left and will display their name forever. I aim to correct this bug in a future release by using the Presence package to make the necessary checks to see if a user is online or not.

So, that’s it. I hope this article helps you get a better understanding of how a basic Meteor application is built. If I can improve on anything or if there are any mistakes please send me a message, @hellotoby.