View the App | Get the Code

In order to broaden my horizons as a developer of web applications, I’ve been taking a good hard look at some of the popular Javascript MVC frameworks of the modern era.  I started by looking at backbone.js and was horribly confused, and was subsequently delighted when I investigated AngularJS.  The ‘two-way declarative bindings’ remind me a lot of Flex, and it felt very natural jumping into development with Angular.  In order to stretch my brain a bit, and really dig into the framework, I decided to start simple and convert an existing Flex project into an Angular app.  A perfect candidate for this is the House of Mews cat browser.  It’s a simple little widget that pulls pet data from the Petfinder.com API, displays a grid of thumbnails, and allows a user to browse details of adoptable pets.

Try it out!

To get started, I grabbed the angular-seed project from GitHub.  This project includes the latest version of Angular (1.0.2 as of this writing), as well as some tools to run a stand-alone server using node.js, and a unit-test runner.  All I really paid attention to was the contents of the ‘app’ folder. It has a nice directory structure, some starter files, and the library itself.

After clearing out all the cruft, I started with the app.js file in /app/js/. Using a single statement, I defined the main module for my app.

angular.module('catApp', [])

Then it’s a simple as adding the ngApp directive to the root of the index.html document. Oh yeah, I also added Twitter Bootstrap to the project as well. The grid system is just super, and I wanted to use the carousel they provide.

<!doctype html>
<html lang="en" ng-app="catApp">
<head>
  <meta charset="utf-8">
  <title>AngularJS Cats App</title>
  <link rel="stylesheet" href="css/bootstrap.min.css"/>
  <link rel="stylesheet" href="css/app.css"/>
</head>
<body>
  stuff goes here...
</body>
</html>

Before going any further with HTML and partial templates and such, I like to make sure I can grab the data first. I created a service module to gather all the cat data. Because the cat data is only updated once per week, I simply download a data file using the Petfinder API, and load the data directly from my site. This prevents hundereds (thousands?) of unnecessary calls to the Petfinder API, since the data rarely changes. I actually have a PHP script that runs every week to update the local data file.

The angular service module is really a factory that does two things: Uses Angular’s $resource object to load data from cats.json, and create a CatService object to be used throughout the application. For now, this object only has one method: getCats(). Rather than just pass raw data from cats.json around the application, getCats() will first transform all the raw data into a nice collection of sensible Cat objects. To see what I mean, observe the raw data for a single cat below:

"pet": {
    "options": {
        "option": [
            {
                "$t": "altered"
            },
            {
                "$t": "noKids"
            }
        ]
    },
    "breeds": {
        "breed": [
            {
                "$t": "Tortoiseshell"
            },
            {
                "$t": "Domestic Long Hair"
            }
        ]
    },
    "shelterPetId": {},
    "status": {
        "$t": "A"
    },
    "name": {
        "$t": "HARLOW"
    },
    "contact": {
        "email": {},
        "zip": {
            "$t": "38104"
        },
        "city": {},
        "fax": {},
        "address1": {
            "$t": "933 S. Cooper"
        },
        "phone": {},
        "state": {
            "$t": "TN"
        },
        "address2": {
            "$t": "Website:  www.houseofmews.com"
        }
    },
    "description": {
        "$t": "<div>I&#39;m the House of Mews superstar in disguise, Harlow!! As you can see I am a beautiful long-haired, tortoiseshell girl and I know it! I&#39;m actually a little shy, but I know I&#39;m gorgeous, so I just can&#39;t help strutting my stuff everywhere I go. I guess hearing everyone tell me how beautiful I am has even made me a little more outgoing. They don&#39;t even mind my crossed eyes (not that it bothers me). In fact, most people tell me they&#39;re just part of my charm! I like to play with toys on sticks. I need a little patience to get over my shyness, but I&#39;ll be well worth it in the end. Please don&#39;t overlook me just because I am a little shy. Come on, haven&#39;t you ever dreamed of living with a supermodel?? Here&#39;s your chance! Approx. DOB is 5-20-00. </div>"
    },
    "sex": {
        "$t": "F"
    },
    "age": {
        "$t": "Senior"
    },
    "size": {
        "$t": "S"
    },
    "mix": {
        "$t": "yes"
    },
    "shelterId": {
        "$t": "TN198"
    },
    "lastUpdate": {
        "$t": "2011-01-12T20:07:07Z"
    },
    "media": {
        "photos": {
            "photo": [
                {
                    "@size": "x",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-1-x.jpg",
                    "@id": "1"
                },
                {
                    "@size": "fpm",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-1-fpm.jpg",
                    "@id": "1"
                },
                {
                    "@size": "pn",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-1-pn.jpg",
                    "@id": "1"
                },
                {
                    "@size": "pnt",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-1-pnt.jpg",
                    "@id": "1"
                },
                {
                    "@size": "t",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-1-t.jpg",
                    "@id": "1"
                },
                {
                    "@size": "x",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-2-x.jpg",
                    "@id": "2"
                },
                {
                    "@size": "fpm",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-2-fpm.jpg",
                    "@id": "2"
                },
                {
                    "@size": "pn",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-2-pn.jpg",
                    "@id": "2"
                },
                {
                    "@size": "pnt",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-2-pnt.jpg",
                    "@id": "2"
                },
                {
                    "@size": "t",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-2-t.jpg",
                    "@id": "2"
                },
                {
                    "@size": "x",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-3-x.jpg",
                    "@id": "3"
                },
                {
                    "@size": "fpm",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-3-fpm.jpg",
                    "@id": "3"
                },
                {
                    "@size": "pn",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-3-pn.jpg",
                    "@id": "3"
                },
                {
                    "@size": "pnt",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-3-pnt.jpg",
                    "@id": "3"
                },
                {
                    "@size": "t",
                    "$t": "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-3-t.jpg",
                    "@id": "3"
                }
            ]
        }
    },
    "id": {
        "$t": "4169367"
    },
    "animal": {
        "$t": "Cat"
    }
}

Now look at this:

cat: {
age: "Senior",
breed: "Tortoiseshell Domestic Long Hair ",
description: "<div>I&#39;m the House of Mews superstar in disguise, Harlow!! As you can see I am a beautiful long-haired, tortoiseshell girl and I know it! I&#39;m actually a little shy, but I know I&#39;m gorgeous, so I just can&#39;t help strutting my stuff everywhere I go. I guess hearing everyone tell me how beautiful I am has even made me a little more outgoing. They don&#39;t even mind my crossed eyes (not that it bothers me). In fact, most people tell me they&#39;re just part of my charm! I like to play with toys on sticks. I need a little patience to get over my shyness, but I&#39;ll be well worth it in the end. Please don&#39;t overlook me just because I am a little shy. Come on, haven&#39;t you ever dreamed of living with a supermodel?? Here&#39;s your chance! Approx. DOB is 5-20-00. </div>",
id: 4169367,
name: "HARLOW",
options: "Spayed/Neutered, No Kids",
pics: [
    0: "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-1-x.jpg",
    1: "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-2-x.jpg",
    2: "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-3-x.jpg" ]
sex: "Female",
size: "Small",
thumbnail: "http://photos.petfinder.com/photos/US/TN/TN198/4169367/TN198.4169367-1-fpm.jpg" }

Much better, no? It’s a lot easier to work with these tidier objects in the controllers and templates, plus, some validation and manipulation makes it easier to handle cases where data is missing.

Here’s the barebones service module. Too see the full source code (including the ServiceHelper module which does all the data translation), take a look at the project on GitHub.

angular.module('catService', ['ngResource','CatServiceHelper'])
.factory('CatsService', function($resource,$filter,CatServiceHelper){

    // Define the remote service using Angular's $resource module.
    var service = $resource('cats.json',{});

    // CatsService is the object created by the CatsService factory.
    // This is a bit long-winded and could easily be shortened,
    // but I plan on adding some additional methods to CatService in the future.
    var CatsService = {
      // The getCats function calls $resource.query() to retrieve the remote data.
      // The data is then lightly validated and scrubbed using the translateCat()
      // function in the CatServiceHelper module
      getCats : function getCats(callback){
        service.query(function (data) {
          var cats = [];
          var catCollection = [];
          if ( data && data.length > 1 && data[2].pets.pet && data[2].pets.pet.length > 0 ) {
            cats = data[2].pets.pet;
          }
          angular.forEach(cats,function(item){
            catCollection.push( CatServiceHelper.translateCat(item) );
          });
          callback(catCollection);
        });
      }
    };
    return CatsService;
});

So that’s a good start. The application won’t really do anything, but it’s primed and ready to fetch lots of cat data!