Creating your to-do app

Now that you have a Flatiron "hello world" example running, you need to extend it so that our to-do application takes shape. For this you will need to create and change some files. If you ever get lost, you can always refer to the chapter's source code. Also, for your reference, there is a complete list of the project files included at the end of this chapter.

As in any real application, you will need a reliable way to persist data. Here we will use CouchDB, the open-source and document-oriented database. You can either choose to install CouchDB locally or use a service over the Internet, such as Iris Couch.

If you choose to install CouchDB on your local development machine, you can head out and visit http://couchdb.apache.org/, click on Download and follow the instructions.

If you prefer to simply use CouchDB over the Internet, you can head out to http://www.iriscouch.com/, click on the Sign Up Now button and fill the registration form. You should have a running CouchDB instance in a matter of seconds.

Setting up the database

To access a CouchDB database from Node we will use a library called nano, which you will add to the dependencies section of your package.json file:

Now you can install this missing dependency by running the following command at the root of your application:

This installs nano inside the node_modules folder, making it available for help while building this app.

To actually connect to the database, you need to define the CouchDB server URL. If you're running CouchDB locally, the URL should be similar to ht tp://127.0.0.1:5984. If you are running CouchDB in Iris Couch or a similar service, your URL will be similar to https://mytodoappcouchdb.iriscouch.com.

In any of these cases, if you need to access using a username and a password, you should encode these in the URL, http://username:password@mytodoappco uchdb.iriscouch.com

This URL should now be entered into a configuration file under config/config.json, under the couchdb key:

Next, encapsulate the access to the database by providing a simple module under lib/couchdb.js:

This module will be used to get a CouchDB server object instead of repeating the config and nano dance several times throughout the code.

As many websites do nowadays, we will be using the Twitter Bootstrap framework to help us in getting the website look and feel minimal yet presentable. For that you will head out to the Bootstrap website http://twitter.github.com/bootstrap/ and click on the Download Bootstrap button:

Application layout

You will get a zip file, which you should expand into the local public folder, ending up with these files:

$ tree public/
public/
├── css
│   ├── bootstrap-responsive.css
│   ├── bootstrap-responsive.min.css
│   ├── bootstrap.css
│   └── bootstrap.min.css
├── img
│   ├── glyphicons-halflings-white.png
│   └── glyphicons-halflings.png
└── js
    ├── bootstrap.js
    └── bootstrap.min.js

You will also need to add jQuery into the mix since Bootstrap depends on it. Download jQuery from http://jquery.com and name it public/js/jquery.min.js.

Now that we have Bootstrap and jQuery installed, it's time to create the frontend of our application.

First we will set up the layout HTML template, which defines the outer structure for all the pages. For hosting all the templates, we will have a directory named templates, containing the following under templates/layout.html:

This template loads the CSS and scripts and contains the placeholders for the messages and main section.

We also need a small module that gets the main content and some other options and applies them to this template. We'll place it inside templates/layout.js:

In Node.js, a module is simply a JavaScript file that is intended to be used by other modules. All the variables inside a module are private; if the module author wishes to expose a value or a function to the outside world, it modifies or sets the special variable in module.exports.

In our case, this module exports a function that gets the markup for the main page content, the page title, and some options such as the info or the error message and applies it to the layout template.

We also need to place the following markup file under templates/alert.html:

Now we're ready to start implementing some of the requirements.

This app will be offering users a personal to-do list. Before they can access it, they need to be signed up in the system. For that you need to define some URLs that the user will use to fetch our user sign-up form and submit it.

Now you will be changing the app.js file. This file contains a set of initialization procedures, including this block:

This block is routing all the HTTP requests having a / URL and where the HTTP method is GET to the given function. This function will then be invoked for every request with these two characteristics in which case you are replying, {"hello":"world"}, which the user will see printed on the browser.

Now we need to remove this routing and add some routes that allow a user to register himself.

For that, create a folder named routes where you will place all the routing modules. The first one is routes/users.js and will contain the following code:

var fs      = require('fs'),
    couchdb = require('../lib/couchdb'),
    dbName  = 'users',
    db      = couchdb.use(dbName),
    Plates  = require('plates'),
    layout  = require('../templates/layout');

var templates = {
  'new' : fs.readFileSync(__dirname +
    '/../templates/users/new.html', 'utf8'),
  'show': fs.readFileSync(__dirname +
    '/../templates/users/show.html', 'utf8')
};

function insert(doc, key, callback) {
  var tried = 0, lastError;

  (function doInsert() {
    tried ++;
    if (tried >= 2) {
      return callback(lastError);
    }

    db.insert(doc, key, function(err) {
      if (err) {
        lastError = err;
        if (err.status_code === 404) {
          couchdb.db.create(dbName, function(err) {
            if (err) {
              return callback(err);
            }
            doInsert();
          });
        } else {
          return callback(err);
        }
      }
      callback.apply({}, arguments);
    });
  }());
}

function render(user) {
  var map = Plates.Map();
  map.where('id').is('email').use('email').as('value');
  map.where('id').is('password').use('password').as('value');
  return Plates.bind(templates['new'], user || {}, map);
}

module.exports = function() {
  this.get('/new', function() {
    this.res.writeHead(200, {'Content-Type': 'text/html'});
    this.res.end(layout(render(), 'New User'));
  });

  this.post('/', function() {
    
    var res = this.res,
        user = this.req.body;

    if (! user.email || ! user.password) {
      return this.res.end(layout(templates['new'],
        'New User', {error: 'Incomplete User Data'}));
    }

    insert(user, this.req.body.email, function(err) {
      if (err) {
        if (err.status_code === 409) {
          return res.end(layout(render(user), 'New User', {
            error: 'We already have a user with that email address.'}));
        }
        console.error(err.trace);
        res.writeHead(500, {'Content-Type': 'text/html'});
        return res.end(err.message);
      }
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(layout(templates['show'], 'Registration Complete'));
    });
  });

};

This new module exports a function that will bind two new routes GET /new and POST /. These routes will later be appended to the /users namespace, which means that they will get activated when the server receives a GET request to /users/new and a POST request to /users.

On the GET /new route, we will present a template that contains a user form. Place it under templates/users/new.html:

We will also need to create a Thank you for registering template, which you need to place in templates/users/show.html:

In the POST / route handler, we'll do some simple validation and insert the user document into the CouchDB database by calling the function named insert. This function tries to insert the user document and makes use of some clever error handling. If the error is a "404 Not Found", it means that the users database hasn't been created, and we take the opportunity to create it and automatically repeat the user document insertion.

You're also catching the 409 Conflict HTTP status code, which CouchDB will return if we try to insert a document with a key that already exists. Since we're using the user e-mail as the document key, we inform the user that such a username already exists.

Now we need to attach these new routes to the /users/ URL namespace by updating and adding a line right before app.start(3000) in the file app.js:

Now you can start your application by typing in the command line:

This starts the server. Then open a web browser and hit http://localhost:3000/users/new. You will get a user form:

User registration

Submit an e-mail and a password and you will get a confirmation screen:

User registration

This screen will present you with a link to the /session/new URL, which doesn't exist yet.

Now you're ready to implement the login screens.

To be able to keep a session, your HTTP server needs to be able to do two things: parse cookies and store session data. For this we use two modules, namely, flatware-cookie-parser and flatware-session, which you should add to the package.json manifest:

Now, install the missing dependencies:

Next, add these middleware components to your server in the file app.js:

We also need to create a routes/session.js module to handle the new session routes:

Next we need to add a view template under templates/session/new.html that contains the login form:

Next, stop the server if it's still running (by pressing Ctrl + C) and start it again:

Point your browser to http://localhost:3000/session/new and insert the e-mail and password of a user you already have registered:

Logging in and session management

If the loginsucceeds, you will be redirected to the /todos URL, which the server does not respond to yet.

Next we're going to make the to-do list work.

For displaying the to-do list, we're going to use a table. It would be nice to sort the to-do items by using drag-and-drop. An easy way to enable this is by using jQuery UI. For this feature alone you don't need the full jQuery UI library, you can download a custom-built one by pointing your browser to http://jqueryui.com/download, deselecting every option except the Sortable option in the Interactions element, and clicking on the Download button. Unzip the resulting file and copy the jquery-ui-1.8.23.custom.min.js file into public/js.

The to-do list

We need to refer this script in the templates.html or layout.html file:

You should also add a file under public/js/todos.js that will contain some frontend interactive code.

Now we need to respond to the /todos URL by firstly including the new routing in the app.js file:

Then we need to place the new to-do routes module under routes/todos.js:

var fs      = require('fs'),
    couchdb = require('../lib/couchdb'),
    dbName  = 'todos',
    db      = couchdb.use(dbName),
    Plates  = require('plates'),
    layout  = require('../templates/layout'),
    loggedIn = require('../middleware/logged_in')();

var templates = {
  index : fs.readFileSync(__dirname +
    '/../templates/todos/index.html', 'utf8'),
  'new' : fs.readFileSync(__dirname +
    '/../templates/todos/new.html', 'utf8')
};

function insert(email, todo, callback) {
  var tries = 0,
      lastError;

  (function doInsert() {
    tries ++;
    if (tries >= 3) return callback(lastError);

    db.get(email, function(err, todos) {
      if (err && err.status_code !== 404) return callback(err);

      if (! todos) todos = {todos: []};
      todos.todos.unshift(todo);

      db.insert(todos, email, function(err) {
        if (err) {
          if (err.status_code === 404) {
            lastError = err;
            // database does not exist, need to create it
            couchdb.db.create(dbName, function(err) {
              if (err) {
                return callback(err);
              }
              doInsert();
            });
            return;
          }
          return callback(err);
        }
        return callback();
      });
    });
  })();
  
}

module.exports = function() {

  this.get('/', [loggedIn, function() {

    var res = this.res;

    db.get(this.req.session.user.email, function(err, todos) {

      if (err && err.status_code !== 404) {
        res.writeHead(500);
        return res.end(err.stack);
      }

      if (! todos) todos = {todos: []};
      todos = todos.todos;

      todos.forEach(function(todo, idx) {
        if (todo) todo.pos = idx + 1;
      });

      var map = Plates.Map();
      map.className('todo').to('todo');
      map.className('pos').to('pos');
      map.className('what').to('what');
      map.where('name').is('pos').use('pos').as('value');

      var main = Plates.bind(templates.index, {todo: todos}, map);
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(layout(main, 'To-Dos'));

    });

  }]);

  
  this.get('/new', [loggedIn, function() {

    this.res.writeHead(200, {'Content-Type': 'text/html'});
    this.res.end(layout(templates['new'], 'New To-Do'));
  }]);

  
  this.post('/', [loggedIn, function() {
    
    var req  = this.req,
        res  = this.res,
        todo = this.req.body
    ;

    if (! todo.what) {
      res.writeHead(200, {'Content-Type': 'text/html'});
      return res.end(layout(templates['new'], 'New To-Do',
        {error: 'Please fill in the To-Do description'}));
    }

    todo.created_at = Date.now();

    insert(req.session.user.email, todo, function(err) {
      
      if (err) {
        res.writeHead(500);
        return res.end(err.stack);
      }
      
      res.writeHead(303, {Location: '/todos'});
      res.end();
    });
  
  }]);


  this.post('/sort', [loggedIn, function() {

    var res = this.res,
        order = this.req.body.order && this.req.body.order.split(','),
        newOrder = []
        ;
    
    db.get(this.req.session.user.email, function(err, todosDoc) {
      if (err) {
        res.writeHead(500);
        return res.end(err.stack);
      }

      var todos = todosDoc.todos;

      if (order.length !== todos.length) {
        res.writeHead(409);
        return res.end('Conflict');
      }

      order.forEach(function(order) {
        newOrder.push(todos[parseInt(order, 10) - 1]);
      });

      todosDoc.todos = newOrder;

      db.insert(todosDoc, function(err) {
        if (err) {
          res.writeHead(500);
          return res.end(err.stack);
        }
        res.writeHead(200);
        res.end();
      });

    });
  }]);

  
  this.post('/delete', [loggedIn, function() {

    var req = this.req,
        res = this.res,
        pos = parseInt(req.body.pos, 10)
        ;

    db.get(this.req.session.user.email, function(err, todosDoc) {
      if (err) {
        res.writeHead(500);
        return res.end(err.stack);
      }

      var todos = todosDoc.todos;
      todosDoc.todos = todos.slice(0, pos - 1).concat(todos.slice(pos));

      db.insert(todosDoc, function(err) {
        if (err) {
          res.writeHead(500);
          return res.end(err.stack);
        }
        res.writeHead(303, {Location: '/todos'});
        res.end();
      });

    });

  }]);

};

This module responds to the to-do index (GET /todos), fetching and presenting all the to-do items for the logged-in user. Place the following template under templates/todos/index.html:

Another new route is GET /todos/new, presenting the user a form for creating a new to-do item. This route makes use of a new template placed in templates/todos/new.html:

The POST /todos route creates a new to-do item by calling the local insert function, which handles the error for when the database does not exist, creating it as needed and retrying the insert function later.

The index template depends on the existence of a client-side script placed under public/js/todos.js:

This file activates and handles the drag-and-drop item, making an AJAX call to the /todos/sort URL with the new order of the to-do items.

The Delete button on each item is also handled in the todos.js routing module by loading the user to-do items, removing the item at the given position and storing the items back.

To make this work, we need to provide a routing middleware under middleware/logged_in.js. This middleware component is responsible for protecting some routes and, when the user is not logged in, redirecting the user to the login screen instead of executing that route:

Finally, stop the server if it's still running (by hitting Ctrl + C) and start it up again:

Point your browser to http://localhost:3000/session/new , and enter the e-mail and password of the user you already have registered. You will then be redirected to the to-do list of the user, which will start off empty.

The to-do list

You can now click on the New To-Do button, obtaining the following form:

The to-do list

Insert some text and click on the Create button. The to-do item will be inserted in the database and the updated to-do list will be presented:

The to-do list

You can insert as many to-do items as you like. Once you've had enough, you can try to reorder them by dragging-and-dropping the table rows.

The to-do list

You can also click on the Delete button to remove a specific to-do item.