Votr Part 5: AngularJS and CRUD Using RESTful APIs

December 17, 2013
Written by

carter-rabasa-seattle-shirt-lo-res (1)

This is the fifth and final part in a series of blog posts about building a real-time SMS and voice voting application using Node.js and TwiML. I began working on this application during some downtime at a Startup Weekend event back in the summer of 2012. It was both an excuse to learn Node and build a reusable app for something that my team is often asked to provide at events (SMS voting). Let’s take a moment and recap where we’ve been.

In part one, we created the Node application and captured incoming votes over SMS and stored them in a CouchDB. I chose to use Express as the web framework and Cloudant and my CouchDB provider.

In part two, we created a real-time visualization of the voting using Socket.io and Highcharts. As votes came in a bar chart depicting the current state of the vote would update in realtime. For my purposes (live SMS voting at events) this was my MVP product. But at larger events I ran into some nasty scaling issues.

In part three, we tweaked our app to scale to thousands of votes per second and millions of total votes. This was accomplished by being smarter about how we stored our documents and leveraging CouchDB’s map/reduce capabilities.

In part four we began adding an web front-end for admins using AngularJS. We added AngularJS to our project and implemented a login and logout flow. We walked through the process of using CouchDB’s cookie-based authentication scheme to bootstrap a simple log-in process for our app.

The last thing for us to do to complete this admin portion of the application is to use AngularJS to create a simple CRUD interface for the events in our CouchDB. We will focus on the AngularJS code and templates that we’re going to write to edit our event documents and finish with a brief run-through of the server-side code.

Listing Event Documents

If you go back and look at the configuration for our app in votr.js you’ll see that the root path (/) is associated with the event-list.html template and the EventListCtrl controller. Remember, this is the root path relative to where we mounted AngularJS. In our case we mounted our app at /admin. When the controller is instantiated, it issues a query to the EventService. The EventService is just like the SessionService; it’s a thin wrapper around a $resource object that points to the /api/events/:id endpoint on our Node app.

app.controller('EventListCtrl', function($scope, $location, EventService) {
  EventService.query(function(events){
    $scope.events = events;
  });

Running this query with no parameters will return all of the events in the database, which will then be sent to the template for rendering.

list of events

Below is the HTML code for the template and it’s fairly simple. We use the ngRepeat directive to iterate over the collection of events.

List of Events

Event
{{event.name}}

You might notice that I’m using Bootstrap to handle the styling and UX touches. You can certainly use your UX framework of choice, but I appreciated having access to a clean set of styles, icons and easy to use modal capabilities.

Creating a New Event

On the list page, the user has the option of creating a new event. When they click “Create New Event”, the method editEvent is called passing in the parameter “new”:

$scope.editEvent = function(event) {
    $scope.opts = ['on', 'off'];

    if (event === 'new') {
      $scope.newEvent = true;
      $scope.event = {name: '', shortname: '', phonenumber: '', state: '', voteoptions: [{id:1, name: ''}]};
    }
    else {
      $scope.newEvent = false;
      $scope.event = event;
    }
  };

You can see that if the event parameter is “new” we set a “newEvent” flag to true and attach an empty event object to our $scope. You’ll also notice we attach an array called opts to our scope, we’ll discuss that in a second.

new event

This template, while being fairly concise, offers quite a bit in the way of functionality. The user can:

  1. Enter data for a new event document
  2. Edit an existing event document
  3. Add an unlimited number of voting options
  4. Remove voting options
  5. Delete the event
  6. Save the event

In the case of create a new event document, the ngModel directive will bind the form fields to the associated attributes of the empty event object we created. When a user clicks “Add Option”, we simply add a new object (with an incremented id) to the vote options array:

$scope.addVoteOption = function() {
    $scope.event.voteoptions.push({id: $scope.event.voteoptions.length+1, name: null});
  };

When the user clicks the trash can icon next to an option, we remove that object from the array and then reset the id values for all of the vote options.

$scope.removeVoteOption = function(vo) {
    $scope.event.voteoptions.splice(vo.id-1, 1);
    // need to make sure id values run from 1..x (web service constraint)
    $scope.event.voteoptions.forEach(function(vo, index) {
      vo.id = index+1;
    });
  };

When a user clicks the Save button the EventService is used to persist the event to the backend and the new event is then added to our in-memory list of events.

var newEvent = new EventService($scope.event);
newEvent.$save(function(){
  $scope.events.push(newEvent);
});

When a user clicks “Edit” for an event on the list page, the editEvent is called passing in the event object itself. Remember, all of the events have been pulled down from the database and are sitting in memory. There’s no need to make another call to the database to fetch the data for the selected event.

edit event

When the user decided to save this event, the following code is executed:

$scope.events.forEach(function(e) {
 if (e._id === $scope.event._id) {
   e.$save();
 }
});

We loop through all of the event objects in memory and invoke save on the one we’ve just edited. Since the $scope.events collection was fetched from the backend using ngResource, each item in the collection contains the logic necessary to persist itself back to the backend.

Deleting an Event

Deleting an event is relatively straightforward. Much like the save() method, we loop through our in-memory collection of events looking for the one we’d like to delete and invoke the $delete method. In addition to an id our service expect a document revision to be passed along.

$scope.delete = function() {
    $scope.events.forEach(function(e, index) {
      if (e._id == $scope.event._id) {
        $scope.event.$delete({id: $scope.event._id, rev: $scope.event._rev}, function() {
          $scope.events.splice(index, 1);
        });
      }
    });
  };

Once our event has been deleted we remove it from our in-memory collection.

Creating an Events API

Throughout this blog post we have assumed a simple RESTful API for event documents. Going back to our Node application, let’s quickly walk through our simple API:

Getting a List of Events

In app.js, let’s wire up the route /api/events to the getEventList function in the routes module:

app.get   ('/api/events',     routes.getEventList);

Remember, when we logged-in to the application we also logged-in to CouchDB. During the authentication step, CouchDB gave us a cookie to use for future requests to the database. We now pass this cookie value to the list method of the event module. If an error is returned we just chalk this up to an auth issue and return an HTTP 401 error.

getEventList = exports.getEventList = function(req, res) {
    events.list(req.cookies['AuthSession'], function(err, list) {
      if (err) {
        res.send(401, JSON.stringify({error: true}));
      }
      else {
        res.send(list);
      }
    });
  }

The list method gets a handle to the DB (using the cookie) and executes the event/list view.

list = exports.list = function(cookie, callback) {
      getDb(cookie).view('event', 'list', function(err, body) {
        if (err) {
          console.log(err);
          callback(err);
        }
        else {
          var events = _und.map(body.rows, function(row) {return row.value});
          callback(null, events);
        }
      });

Getting an Event

Now let’s wire up the route for fetching a specific event:

app.get   ('/api/events/:id', routes.getEventById);

In the event that an error occurs while trying to retrieve the specified document we simply return a 404 error to the client.

getEventById = exports.getEventById = function(req, res){
    events.findBy('all', {key: [req.params.id], reduce:false}, function(err, event) {
      if (err) {
        res.send(404, 'We could not locate your event');
      }
      else {
        res.send(JSON.stringify(event));
      }
    });
  }

Deleting an event is similarly simple. Notice that we’re wiring up the HTTP delete method.

app.delete('/api/events/:id', routes.destroyEvent);

destroyEvent = exports.destroyEvent = function(req, res) {
    events.destroy(req.cookies['AuthSession'], req.params.id, req.query.rev, function(err, body) {
      if (err) {
        console.log(err);
        res.send(500, JSON.stringify({error: true}));
      }
      else {
        res.send(200, "OK");
      }
    });
  }

destroy = exports.destroy = function(cookie, id, rev, callback) {
      getDb(cookie).destroy(id, rev, function(err, body) {
        callback(err, body);
      });
    }

Saving an Event

And finally, we need to be able to save an event.

app.post  ('/api/events',     routes.saveEvent);

If an error occurs, return a 500 error to the client.

saveEvent = exports.saveEvent = function(req, res) {
    events.save(req.cookies['AuthSession'], req.body, function(err, body) {
      if (err) {
        console.log(err);
        res.send(500, JSON.stringify({error: true}));
      }
      else { 
        // update the doc revision
        req.body._rev = body.rev;
        res.send(req.body);
      }
    });
  }

If we are creating a new event document, we need to generate a document id for it that matches the pattern “event:{shortname}”.

save = exports.save = function(cookie, event, callback) {
      if (!event._id) { event._id = 'event:' + event.shortname }
      getDb(cookie).insert(event, function(err, body) {
        callback(err, body);
      });
    }

And That’s All She Wrote

Feel free to play around with a live demo of this app, hosted on Nodejitsu:

http://twilio-votr-part5.nodejitsu.com (thenchathavertyoundishos / fPFEFIHIWjKsATJADG4qWfsI)

I have had a ton of fun working on this app and walking you all through the process of making. I started working on Votr back in July 2012, which blows my mind. Along the way I taught myself:

  1. Node.js
  2. Express and Mustache templating
  3. CouchDB and map/reduce functions
  4. Socket.io
  5. Highcharts
  6. AngularJS

I hope you enjoyed the series. All the code has been posted to Github.

Happy hacking! Make sure you check out our other guides and tutorials on the Twilio docs.