Codementor Events

Create a Forum Engine in 15 minutes using Meteor & AngularJS

Published Mar 01, 2016Last updated Jan 18, 2017
Create a Forum Engine in 15 minutes using Meteor & AngularJS

I'm often asked to create small forums for websites' community and support team.

So I decided to write a small tutorial on how to do it using Meteor and AngularJS.

First, let's install Meteor! You can skip this step if you already have Meteor installed.
Open your command prompt and paste this command:

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

Or if you use Windows, download the official Meteor installer.

Now let's create a new meteor app.

$ meteor create forum

Open the new forum directory and delete the automatically generated forum.css, forum.html and forum.js files.

$ cd forum
$ rm forum.css forum.html forum.js

We are going to set up Angular-Meteor by adding angular, and removing the
unnecessary blaze-html-templates, ecmascript, autopublish and insecure packages.
Since Meteor v1.2, you also have to add check manually to be able to perform basic input sanitization.
We want our application to be lightweight and secure.

$ meteor remove blaze-html-templates ecmascript autopublish insecure
$ meteor add angular check

Let's create two directories in the project root, client and views.
The first is a special Meteor directory that contains code used only on the client side.
Even though in Meteor you can write code executed both on the client and server side,
it's still a good idea to separate some business logic for security reasons.
We are going to put HTML templates for the layout, pages and AngularJS directives into the views folder.

$ mkdir client
$ mkdir views

Create a new index.html file in the project root and place this code inside.

<head>
  <title>Angular-Meteor Forum</title>
</head>
<body ng-app="forum">
  <div ng-include="'views/forum.html'"></div>
</body>

This will bootstrap the forum AngularJS module, which doesn't exist yet, and
include the views/forum.html template, which doesn't exist either. Let's fix this.

First, the AngularJS module. Create a new file client/app.js and define the module:

angular.module('forum', ['angular-meteor']);

Next, create the forum template views/forum.html. This is going to be the common layout used by all subpages.

<div class="forum">
  FORUM
</div>

At this point you can run the app.

$ meteor

Open a browser and navigate to http://localhost:3000 . You should see the text FORUM.
This is all great, but not very impressive yet.

Let's create the template views/pages/topics.html to display the forum topics.

<div class="page">
  <h1>Topics</h1>
  <div ng-repeat="topic in topics">
    <a ui-sref="topic({topicId: topic._id})">{{ topic.name }}</a>
  </div>
</div>

To see this template in action, we will install the de-facto routing solution in AngularJS, angular-ui-router, and set up some basic routes. Either exit meteor with Ctrl+C, or open another command prompt, and navigate to your apps root directory.

meteor add angularui:angular-ui-router

Our app.js is a great place to set up routing and controllers. However, you may want to put these into different files in a real-life application. If you do so, make sure to pay attention to the order of how Meteor loads files. Place the module definition into a file under client/lib/, otherwise it will be loaded after the
controller/directive/etc. definitions triggering an AngularJS exception.

Replace the contents of app.js with:

angular.module('forum', ['angular-meteor', 'ui.router'])
.config(function($urlRouterProvider, $stateProvider){

  // Set the default route 
  $urlRouterProvider
    .when('/', '/topics')
    .otherwise('/topics');

  // Add states
  $stateProvider.state('topics', {
    url: '/topics',
    templateUrl: 'views/pages/topics.html'
  });
})
.run(function($state){
  // We inject $state here to initialize ui.router 
})

Before you can see the topics list in the browser, views/forum.html needs a little tweaking.
An element with ui-view attribute is required to render the template associated with the current route.

<div class="forum">
  <div class="page-container" ui-view>
  </div>
</div>

Navigate to http://localhost:3000 now, and notice how it redirects instantly to the topics page.
However, we don't have any topics yet. Since we will probably want to add or remove topics later,
it's a good idea to store them in a database.

Meteor uses MongoDB for persistent storage.
You can define MongoDB collections with new Meteor.Collection(collectionName).
This should be present both on the client and server side, so let's create a directory called common, and place this into common/db.js:

Topics = new Meteor.Collection('Topics');

In Meteor, publication is the way you construct a set of data to send to a client.
You call Meteor.publish with the name of the dataset, and a callback function that may or may not have arguments and returns a MongoDB cursor. Meteor uses some magic behind the scenes to read changes from the MongoDB oplog (or polls the database if oplog is not set up properly) and updates the cursor when data is added, removed or modified.

A publication should be defined in a server-only file, so let's create a directory called server. Every JavaScript file you place here will run only on the server.
Let's create the file server/publications.js:

Meteor.publish('topics', function(){
  return Topics.find();
});

Meteor.publish('topic', function(id){
  check(id, String);
  return Topics.find({_id: id});
});

This will define two publications, one for all the topics, and one for a single topic requested by id. Also, let's insert some topics by default if the collection is empty. Put this in server/defaults.js:

if (Topics.find().count() === 0) {
  _.each(['General Discussion', 'Tutorials', 'Help'],
  function(topicName){
    Topics.insert({name: topicName});
  });
}

A client initiates a subscription which connects to a publication, and receives that data. Let's see how to do this using Angular-Meteor. In client/app.js, add a controller option to the definition of the topics state called TopicsContoller, and define the controller after .run():

// ...
  $stateProvider.state('topics', {
    url: '/topics',
    templateUrl: 'views/pages/topics.html',
    controller: 'TopicsContoller'
  });
// ...
.controller('TopicsContoller', function($scope){
  $scope.subscribe('topics');
  $scope.helpers({
    topics: function() {
      return Topics.find({}, {sort: {name:1}});
    }
  });
})

Here we subscribe to the topics publication, and add a helper called topics that simply returns a MongoDB cursor to the list of topics sorted by name. From the AngularJS perspective, this will be a simple array available in the $scope.topics variable.

View your forum in the browser. You should see the three topics under the heading. However, if you try to click on them, nothing happens. This is because we haven't defined our topic state yet. Let's do that and define the TopicController as well.

// ...
  $stateProvider.state('topic', {
    url: '/topic/:topicId',
    templateUrl: 'views/pages/topic.html',
    controller: 'TopicContoller'
  });
// ...
.controller('TopicContoller', function($scope, $stateParams){
  $scope.subscribe('topic', function(){ 
    return [$stateParams.topicId];
  });
  $scope.helpers({
    topic: function(){
      return Topics.findOne({_id: $stateParams.topicId});
    }
  });
})

It's pretty straight-forward, but note that we have to use a function to pass the id argument to the topic publication, and return a single topic with findOne instead of find.

The template views/pages/topic.html will be quite simple, but don't worry, we'll add the list of threads soon.

<div class="page">
  <h1>{{ topic.name }}</h1>
</div>

To use the forum, we have to be able to identify users.
Meteor has some useful packages for authentication:

  • accounts-base: This package implements the basic functions necessary for user accounts and lets other packages register login services.
  • accounts-password: A login service that enables secure password-based login.
  • dotansimha:accounts-ui-angular: AngularJS wrapper for Meteor's Account-UI package.

Let's install these:

$ meteor add accounts-base accounts-password dotansimha:accounts-ui-angular

To use dotansimha:accounts-ui-angular, your AngularJS module should list
accounts.ui as a dependency:

angular.module('forum', ['angular-meteor', 'ui.router', 'accounts.ui'])

Now we can add authentication, signup, forgot password and change password features with a single line. A good place would be in views/forum.html, since it's the root layout for all of our pages:

<div class="forum">
  <login-buttons></login-buttons>
  <div ui-view>
  </div>
</div>

Nice! It's time to create some threads. We are going to ...

  • define the collection in common/db.js:
Threads = new Meteor.Collection('Threads');
  • publish threads in server/publications.js:
Meteor.publish('threads', function(topicId){
  check(topicId, String);
  return Threads.find({topicId: topicId});
});

Meteor.publish('thread', function(id){
  check(id, String);
  return Threads.find({_id: id});
});
  • edit views/pages/topic.html to list threads of the topic and to add a form where users can create new threads.
<div class="page">
  <h1>{{ topic.name }}</h1>
  <ul>
    <li ng-repeat="thread in threads">
      <a ui-sref="thread({threadId: thread._id})">{{ thread.content }}</a>
      by {{ thread.author }} at {{ thread.createdAt | date }}
    </li>
  </ul>

  <h2>Create a new thread</h2>
  <form ng-submit="createThread(thread)">
    <input type="text" placeholder="Start discussion..." ng-model="thread.content">
    <button type="submit">Create</button>
  </form>
</div>
  • and finally, modify TopicController in client/app.js so that it subscribes to the list of threads that belong to the current topic and hanldes thread creation as well:
.controller('TopicContoller', function($scope, $stateParams, $meteor){
  $scope.subscribe('topic', function(){ return [$stateParams.topicId]; });
  $scope.subscribe('threads', function(){ return [$stateParams.topicId]; });
  $scope.helpers({
    topic: function() {
      return Topics.findOne({_id: $stateParams.topicId});
    },
    threads: function() {
      return Threads.find({topicId: $stateParams.topicId});
    }
  });
  $scope.createThread = function(thread){
    $meteor.call("createThread", $stateParams.topicId, thread.content).then(function(){
      thread.content = '';
    }).catch(function(){
      alert("An error occured while creating the thread!");
    });
  };
})

See the injected $meteor service? We use it to call the server-side createThread
Meteor method which is yet to be created.

Paste this into server/methods.js:

Meteor.methods({
  createThread: function(topicId, content){
    check(topicId, String);
    check(content, String);
    var user = Meteor.user();
    if (!user) {
      throw new Meteor.Error("You are not logged in!");
    }
    if (!content){
      throw new Meteor.Error("Content is required!");
    }
    var thread = {
      author: user.emails[0].address,
      createdAt: new Date(),
      topicId: topicId,
      content: content
    };
    return Threads.insert(thread);
  }       
});

Meteor.methods expects an object, where keys are method names, and values are method definitions. Methods may have arguments and can return anything that can be serialized to JSON. Meteor cursors are not serializable, so avoid returningcollection.find(...) as it will crash your app.

Let's summarize where we are right now. We can:

  • List topics
  • Open topics to list threads
  • Create threads
  • Sign up/in/out, reset forgotten password and change password

What's left? We have to:

  • Define the Posts collection and publish it just like we did with Threads
  • Create a route for a single thread to list its posts just like we did with topic
  • Create a form to be able to post on a thread just like we did with topic
  • Write a method to handle post creation just like we did with createThread

Are you ready for some practice? Don't worry, you can always check the example in the Github repository.

Discover and read more posts from Tibor Fulop
get started
post commentsBe the first to share your opinion
Saakshi Challa
5 years ago

Hi Tibor,
I am a beginner and trying this for a ‘girls who code’ project. I am following the steps above, but keep getting stopped with this error:
What am I doing wrong? Your help will be greatly appreciated!

C:\Users\saaks\forum>meteor add angular check
C:\Users\saaks\AppData\Local.meteor\packages\meteor-tool\1.8.1\mt-os.windows.x86_32\dev_bundle\lib\node_modules\meteor-promise\promise_server.js:218
throw error;
^

Error: No metadata files found for isopack at: /C/Users/saaks/AppData/Local/.meteor/packages/pbastowski_angular-babel/1.0.9
at Isopack.loadUnibuildsFromPath (C:\tools\isobuild\isopack.js:843:13)
at .each (C:\tools\packaging\tropohouse.js:521:21)
at Array.forEach (<anonymous>)
at Function.
.each.
.forEach (C:\Users\saaks\AppData\Local.meteor\packages\meteor-tool\1.8.1\mt-os.windows.x86_32\dev_bundle\lib\node_modules\underscore\underscore.js:79:11)
at buildmessage.enterJob (C:\tools\packaging\tropohouse.js:520:13)
at Object.enterJob (C:\tools\utils\buildmessage.js:388:12)
at C:\tools\packaging\tropohouse.js:515:22
at Object.enterJob (C:\tools\utils\buildmessage.js:388:12)
at Object.download (C:\tools\packaging\tropohouse.js:427:20)
at C:\tools\packaging\tropohouse.js:600:22
at Object.enterJob (C:\tools\utils\buildmessage.js:388:12)
at exports.Tropohouse.downloadPackagesMissingFromMap (C:\tools\packaging\tropohouse.js:597:20)
at C:\tools\project-context.js:836:25
at Object.enterJob (C:\tools\utils\buildmessage.js:388:12)
at C:\tools\project-context.js:835:20
at C:\tools\packaging\catalog\catalog.js:100:5
at Object.capture (C:\tools\utils\buildmessage.js:283:5)
at Object.catalog.runAndRetryWithRefreshIfHelpful (C:\tools\packaging\catalog\catalog.js:99:31)
at ProjectContext._downloadMissingPackages (C:\tools\project-context.js:834:13)
at C:\tools\project-context.js:300:9
at Object.enterJob (C:\tools\utils\buildmessage.js:388:12)
at ProjectContext._completeStagesThrough (C:\tools\project-context.js:290:18)
at Profile.run (C:\tools\project-context.js:282:12)
at Function.run (C:\tools\tool-env\profile.js:490:12)
at ProjectContext.prepareProjectForBuild (C:\tools\project-context.js:281:13)
at C:\tools\cli\commands-packages.js:2160:20
at Object.capture (C:\tools\utils\buildmessage.js:283:5)
at Command.func (C:\tools\cli\commands-packages.js:2159:27)
at C:\tools\cli\main.js:1531:15

Vladislav Pshenychka
7 years ago

Hi Tidor. Good article. Read also: https://artjoker.net/blog/angularjs-vs-reactjs/ about Angular vs React: Which One is Better?

Ellina Bereza
7 years ago

Thanks for your article, super useful!
Best wishes from ww.erminesoft.com team.

Mariela Atausinchi
6 years ago

it’s good

Show more replies