Build an Angular Todo App with a Node Backend

Today we’ll be making a simple todo application based of the TodoMVC http://todomvc.com/architecture-examples/angularjs/#/ project, using the MEAN (Mongo, Express, Angular, Node) Stack. We will:

  • Create a restful API
  • Store todos in a MongoDB
  • Use AngularJS as a frontend
  • Interface with our API through ng-resource
  • Use polling to sync todos across clients

What we’re building

This is the working app. The full source code is available here: https://github.com/DaftMonk/mean-todomvc

Getting Started

If you haven’t used the angular-fullstack generator before, have a look at a previous article. You’ll also need to have MongoDB installed to follow along.

Lets set up a new project. In a new directory, run yo angular-fullstack. Then use the following configuration:

[?] Would you like to use Sass (with Compass)? No
[?] Would you like to include Twitter Bootstrap? No
[?] Which modules would you like to include? angular-resource.js, angular-route.js
[?] Would you like to include MongoDB with Mongoose? Yes
[?] Would you like to include a Passport authentication boilerplate? No

When thats done, run grunt serve. If everything is working, you should see a welcome message with a list of some of the packages your project is using. It won’t have much default styling because we’re not using bootstrap, don’t worry, we’ll take care of that later.

Starting with a clean slate

Let’s just clear away some stuff we won’t need. Delete the following files

lib/config/dummydata.js
lib/models/thing.js
lib/controllers/api.js

Edit server.js and remove require('./lib/config/dummydata');.

We’ll also want to delete references to the api controller from lib/routes.

// lib/routes

...

api = require('./controllers/api'), // remove

...

app.get('/api/awesomeThings', api.awesomeThings); // remove

The Todo API

Lets start by creating the schema for our Todo model.

// lib/models/todo.js

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

/**
 * Todo Schema
 */
var TodoSchema = new Schema({
  title: String,
  completed: Boolean,
  createdAt: Date,  
  updatedAt: Date,
});

// keep track of when todos are updated and created
TodoSchema.pre('save', function(next, done){
  if (this.isNew) {
    this.createdAt = Date.now();
  }
  this.updatedAt = Date.now();
  next();
});

mongoose.model('Todo', TodoSchema);

We’re keeping track of when our todos are created and updated so that we can sort by that later.

Now lets go ahead and make the todo controller. The todo controller is where all our CRUD actions will take place.

// lib/controllers/todos.js

var mongoose = require('mongoose'),
  Todo = mongoose.model('Todo');

/**
 * Find todo by id and store it in the request
 */
exports.todo = function(req, res, next, id) {
  Todo.findById(id, function(err, todo) {
    if (err) return next(err);
    if (!todo) return next(new Error('Failed to load todo ' + id));
    req.todo = todo;
    next();
  });
};

/**
 * List of todos
 */
exports.query = function(req, res) {
  Todo.find().sort('-createdAt').exec(function(err, todos) {
    if (err) return res.json(500, err);
    res.json(todos);
  });
};

/**
 * Show a todo
 */
exports.show = function(req, res) {
  res.json(req.todo);
};

/**
 * Create a todo
 */
exports.create = function(req, res) {
  var todo = new Todo(req.body);

  todo.save(function(err) {
    if (err) return res.json(500, err);
    res.json(todo);
  });
};

/**
 * Update a todo
 */
exports.update = function(req, res) {
  Todo.update({ _id: req.todo._id }, req.body, { }, function(err, updatedTodo) {
    if (err) return res.json(500, err);
    res.json(updatedTodo);
  });
};

/**
 * Remove a todo
 */
exports.remove = function(req, res) {
  var todo = req.todo;

  todo.remove(function(err) {
    if (err) return res.json(500, err);
    res.json(todo);    
  });
};

The CRUD operations are pretty self explanatory, they’re just using standard mongoose methods.

One thing that is more important to note is the todo method. We’ll talk more about this in a moment, but any request for a specific todoId will fire this method off first, which allows us to find the requested todo and store it in req.todo for any other request handlers to use.

Routes

Lets create the Express routes to handle our todo API calls.

// lib/routes.js

'use strict';

var index = require('./controllers'),
    todos = require('./controllers/todos');

...

  // Server API Routes
  app.param('todoId', todos.todo);

  app.post('/api/todos', todos.create);
  app.get('/api/todos', todos.query);
  app.get('/api/todos/:todoId', todos.show);
  app.put('/api/todos/:todoId', todos.update);
  app.del('/api/todos/:todoId', todos.remove);

...

app.param lets us execute the todos.todo method any time a todoId is present in the route path. This lets us set req.todo as we mentioned above.

Our api is done. If you go to localhost:8000/api/todos you’ll see a list of our todos, which is currently empty.

The client interface

Now that the server side is finished, lets install the todomvc-common bower package which contains all the styles we’ll need.

bower install --save todomvc-common

Most bower packages will specify a main file, which you can automatically link to your index file with grunt bower-install. However with this particular package, we have to link it manually.


...
    
    	
    
    
    

Now we’ll add the html for our main view. Jump into your views/partials/main.html and replace the contents with



Some of the things this will do:

  • Creates a form thats we use to add todos
  • Create an input to mark all todos as completed
  • Display all todos, and allow you to edit a todo by double clicking, or delete it by clicking x button
  • Auto focus the current edited todo, and allow you to cancel editing by pressing escape.
  • Allow you to filter by active/complete/all todos by changing the route

We’ll have to implement most of that in the controller, we’ll get to that last.

Custom directives

We need to create the two custom directives we’re using in the main view, so lets scaffold them.

yo angular-fullstack:directive todoFocus
yo angular-fullstack:directive todoEscape

The focus directive is used to focus the todo which is currently being edited

// app/scripts/directives/todofocus.js

...

  .directive('todoFocus', function todoFocus($timeout) {
    return function (scope, elem, attrs) {
      scope.$watch(attrs.todoFocus, function (newVal) {
        if (newVal) {
          $timeout(function () {
            elem[0].focus();
          }, 0, false);
        }
      });
    };
  });

And this directive is used to revert editing of the todo when the escape key is pressed

// app/scripts/directives/todoescape.js

...

  .directive('todoEscape', function () {
    var ESCAPE_KEY = 27;
    return function (scope, elem, attrs) {
      elem.bind('keydown', function (event) {
        if (event.keyCode === ESCAPE_KEY) {
          scope.$apply(attrs.todoEscape);
        }
      });
    };
  });

Todo resource

This factory will return a new $resource object configured for the todo api.

// app/scripts/services/todo.js

...

  .factory('Todo', function ($resource) {
    return $resource('api/todos/:todoId', {
      todoId: '@_id'
    }, {
      update: {
        method: 'PUT'
      }
    });
  });

The second argument of the $resource factory is an object that lets us set the default parameters.

Setting todoId to @_id will set todoId to the _id value of the current data object. This will make it easy for us to update and delete existing todos.

The $resource object comes out of the box with save, query, get, and delete methods, but as it’s lacking an update method we add our own which uses PUT,

App.js

We want to set the current filter based on the route, eg /active for the active filter. However, a route change normally will either take you to another controller, or reload the current controller all together. That doesn’t look so good because controller reload clears the todo array and has to download todos from the server again.

Fortunately, we can make an exception to reloading the current controller by using search params. Then we can make route changes like ?q=active without triggering a reload. Add the following configuration to our route.

// app/scripts/app.js

...
      .when('/', {
        templateUrl: 'partials/main',
        controller: 'MainCtrl',
        reloadOnSearch: false
      })
...

The Main Controller

The final bit that ties it all together is the main controller. Edit the current main controller and replace it with the following.

// app/scripts/controllers/main.js

...
  .controller('MainCtrl', function ($scope, $timeout, Todo, filterFilter, $location) {
    $scope.todos = [];
    $scope.newTodo = '';
    $scope.editedTodo = null;
    // set the filter status to the initial search query if it exists
    $scope.status = $location.search().q || '';

    // watch the todos array for changes and update the counts
    $scope.$watch('todos', function () {
      $scope.remainingCount = filterFilter($scope.todos, { completed: false }).length;
      $scope.completedCount = $scope.todos.length - $scope.remainingCount;
      $scope.allChecked = !$scope.remainingCount;
    }, true);

    // monitor the current location for changes and adjust the filter accordingly
    $scope.$on('$locationChangeSuccess', function () {
      var status = $scope.status = $location.search().q || '';
      $scope.statusFilter = (status === 'active') ?
        { completed: false } : (status === 'completed') ?
        { completed: true } : null;
    });

    // create a new todo locally save it remotely
    $scope.addTodo = function () {
      var todoTitle = $scope.newTodo.trim();
      if (!todoTitle.length) {
        return;
      }

      var newTodo = new Todo({
        title: todoTitle,
        completed: false
      });
      newTodo.$save();
      $scope.todos.unshift(newTodo);
      $scope.newTodo = '';
    };

    // remove todo locally and remotely
    $scope.removeTodo = function (id) {
      $scope.todos[id].$remove();
      $scope.todos.splice(id, 1);
    };

    // begin editing a todo, save the original in case of cancel
    $scope.editTodo = function (id) {
      $scope.editedTodo = $scope.todos[id];
      $scope.originalTodo = angular.extend({}, $scope.editedTodo);
    };

    // update when done editing, or if title is erased remove the todo
    $scope.doneEditing = function (id) {
      $scope.editedTodo = null;
      var title = $scope.todos[id].title.trim();
      if (title) {
        $scope.todos[id].$update();
      } else {
        $scope.removeTodo(id);
      }
    };

    // revert the edited todo back to what it was
    $scope.revertEditing = function (id) {
      $scope.todos[id] = $scope.originalTodo;
      $scope.doneEditing(id);
    };

    // toggle todo completed, and update remotely
    $scope.toggleCompleted = function (id) {
      var todo = $scope.todos[id];
      todo.completed = !todo.completed;
      todo.$update();
    };

    // remove completed todos locally and from server
    $scope.clearCompletedTodos = function () {
      var remainingTodos = [];
      angular.forEach($scope.todos, function (todo) {
        if (todo.completed) {
          todo.$remove();
        } else {
          remainingTodos.push(todo);
        }
      });
      $scope.todos = remainingTodos;
    };

    // mark all as completed or not, then update remotely
    $scope.markAll = function (allCompleted) {
      angular.forEach($scope.todos, function (todo) {
        todo.completed = !allCompleted;
        todo.$update();
      });
    };

    // Poll server to regularly update todos
    (function refreshTodos() {
      Todo.query(function(response) {
        // Update todos if a todo is not being edited
        if($scope.editedTodo === null) {
          $scope.todos = response;
        }
        $scope.promise = $timeout(refreshTodos, 5000);
      });
    })();

    // when the controller is destroyed, cancel the polling
    $scope.$on('destroy', function(){
      $timeout.cancel($scope.promise);
    });
  });

We use the $resource methods, which are attached to each of the todo objects in our todo array, to keep everything synced up on the server side.

And that’s it for everything! You should now be able to launch the server, and edit todos in two different screens and have them sync up every 5 seconds when they poll the server for changes.

Conclusion

We’ve built a fully working full stack todo application that lets us do CRUD operations on a todo list! An overview of what we’ve built and accomplished.

  • A restful API with express
  • A single page app that requires no refresh
  • Used mongoDB as our database through mongoose
  • Used $resource to interface with our API

Going forward you may also find times you want to use socket.io, rather than polling or simple api requests. Have a look at my MEAN chat app to get some ideas for ways you could set that up: https://github.com/DaftMonk/mean-chat

Share Button
  • Oscar Agreda

    Outstanding !!!
    in just one article (and with the help of the awesome agular-fullstack) you have reduced hours and hours of pain into a beautiful experience with MEAN.
    Thanks

    • tylerhenkel

      Thanks! I’m happy to help.

  • Arthur Fanti

    Really awesome and well explained! Thanks for the article, it will help me a lot!

  • Christian Huening

    Great tutorial, thank you very much!

    However I get an error when trying to update a todo. The todos.update function returns the error “Mod on _id is not allowed” from MongoDB. I found a post on Stackoverflow about this, but am not entirely sure whether that’s the best way: http://stackoverflow.com/questions/17250496/update-a-record-where-id-id-with-mongoose

    Can you help?

    best regards,
    christian

    • tylerhenkel

      Hi Christian,

      I changed the todo controllers update method in the tutorial so it uses model.update, but it looks like you might still have this problem because the _id is part of the req.body that we’re using to update the todo with. I would fix it the same way as those stackoverflow answers. e.g.

      var newTodo = req.body;
      delete newTodo._id;

      Todo.update({ _id: req.todo._id }, newTodo, { }, function(err, updatedTodo) {
      if (err) return res.json(500, err);
      res.json(updatedTodo);
      });

      But I haven’t been getting this error, so I wonder if it’s related to the mongoDB version you’re using. Do you know what version it is?

      • Christian Huening

        Hi Tyler, thanks for your answer! I already tried both alternatives and sadly none works, but I found the solution, read on.

        My mongodb Version is 2.4.9 installed via brew on a mac with 10.9.2. Got the same issue on Windows with the same MongoDB Version.

        In fact the update() method in MongoDB got changed from 2.2 to 2.4. Look here: http://docs.mongodb.org/manual/reference/method/db.collection.update/

        I checked with the mongoose reference and it seems that we need to use the Model.update() method differently, since it’s not returning the result (http://mongoosejs.com/docs/api.html#model_Model.update) (anymore?).

        So here’s what works for me:
        exports.update = function(req, res){

        var newTodo = req.body;
        var id = newTodo._id;
        delete newTodo._id;

        Todo.update({_id : id}, newTodo, { }, function (err, numberAffected, raw) {
        if (err) return res.json(500, err);
        newTodo._id = id;
        res.json(newTodo);
        });
        };

        Thanks again! Even the error I got helped me to understand my way around the angular-fullstack ;-)

        regards,
        Christian

        • Matt Cowski

          That fixed it for me too.
          Alternatively, using the included lodash, other MEAN stacks use:

          var newTodo = req.todo;

          newTodo = _.extend(newTodo, req.body);

          newTodo.save(function (err){

          if (err) return res.json(500, err);

          res.json(newTodo);

          });

    • Matt Cowski

      I stumbled upon the same issue, but was wondering how you get “Mod on _id is not allowed”? I only see

      PUT /api/todos/532ce23c5321c4d48b34e7b6 500 but no actual message. Are you running any other plugins other than the fullstack gen + this tutorial?

      • Christian Huening

        Hi Matt,
        I am using WebStorm as an IDE and its debugger lets me look into the actual messages. I guess the different browsers dev extensions will also allow you to do that.

  • Matt Cowski

    Ok, so since you had us include _ lodash, I figure you probably considered to do the update method instead like this:

    var newTodo = req.todo;
    newTodo = _.extend(newTodo, req.body);
    newTodo.save(function (err){
    if (err) return res.json(500, err);
    res.json(newTodo);
    });

    any reason why not?

    • tylerhenkel

      Actually that is how I was doing doing it before, but I updated the tutorial recently (forgot to remove it from the module dependencies in the tutorial but i’ll update that.)

      Both ways work, but I just felt the mongodb method is superior because its the more standard way of doing it, and for different models using _.extend which is only a shallow merge might not have the desired results.

      • Matt Cowski

        Thanks. That’s the answer I was hoping for :)

  • Silver Troy

    Tyler, if you could update this tutorials.
    let individuals know that if they get “Todo Provider unknown”
    they have to insert

    into the index.html at the bottom with the other controllers and directives.
    Also if you get an error running grunt serve that says “todomvc-common was not injected in your file. Please go take a look in ‘appbower-componentstodomvc-common”
    you need to add in
    appbower-componentstodomvc-common.bower.json add in
    “main”: “./base.js”,

    hope this helps someone

  • Juraj Krivda

    Awesome tutorial. I have one problem with function $scope.addTodo. I cannot push newTodo to the Array $scope.todos using .unshift(newTodo); because newTodo contains Resource {title: “test”, completed: false, $get: function, $save: function, $query: function…}. I am getting this using console.log(newTodo) .Can you help me?

  • JesseBreuer

    I am having issues with update. The answers in the comments do not seem to work.

    Using the following in lib/controllers/todos.js

    exports.update = function(req, res){
    var newTodo = req.body;
    var id = newTodo._id;
    delete newTodo._id;

    }

    I get no error in the console and it appears to be updated in the client until I refresh and see that the updates did not write to the database.

    If I use the lodash code:

    exports.update = function(req, res){
    var newTodo = req.todo;
    newTodo = _.extend(newTodo, req.body);
    newTodo.save(function (err){
    if (err) return res.json(500, err);
    res.json(newTodo);
    });
    }

    I get a message in the console saying “ReferenceError: _ is not defined” which I assume means I dint have lodash included?

    btw I skipped: “bower install –save todomvc-common” because I wanted to use my own styling. Would that make a difference?

  • Yu Yu

    Great tutorial Tyler.

    I have some questions let’s say on removeTodo or toggleCompleted function, since data is updated locally then remotely; what would happen if the remote call failed which left data updated locally only?

  • Tyler Pflueger

    I’m currently trying to go through this tutorial, but the latest yo scaffolding changes it compared to your code. I was wondering if you could help explain or maybe if its pretty similar.

  • Ram Anand

    Hi Tyker,
    It seems that the scaffolding fir angular-fullstack has changed and i was wondering if you would be able to update the article to match the new angular-fullstack.

  • http://peterwarbo.com/ peterwarbo

    Tyler! Thanks for this! I’m wondering there are some differences regarding this setup and how your generator-angular-fullstack creates server endpoints. I wonder is this approach preferred over the approach in the generator?