Demo | Code

During the creation of Angular Cats!, it didn’t take me long to realize that I was straying from best practices and the intentions of Angular’s creators. I dug myself a bit of technical debt in the name of expediency, and have now come to pay the piper. Over the past few days I’ve been refactoring major portions of the app, and wound up with almost an entirely new codebase. Large sections are still intact (e.g. the translator service), and functionality is basically the same, but a few key differences are present.

The largest change by far is the ability to deep link. Every cat has its own URL, and can be bookmarked.

To get the deep-link ball rolling, the following code was added to app.js:

.config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/cats/:page', {templateUrl: 'partials/catList.html', controller: CatListController});
    $routeProvider.when('/cat/:catId/:page', {templateUrl: 'partials/catDetail.html', controller: CatDetailController});
    $routeProvider.otherwise({redirectTo: '/cats/1'});
}])

The routes allow a user to go to a specific ‘page’ of cats in the cat list, or navigate to the details of any given cat – provided you know its ID. I also store the page number in the URL of the cat detail page so the user can navigate back to the page previously viewed when clicking ‘Back’ on the cat detail page.

In order to navigate around the application, the controllers now use the $location module, and will change the path according to the behavior of the user. The code below shows how the handler for clicking a cat to view details has changed.

Before (use events to change state):

// CatListController
$scope.getDetail = function() {
  eventBroadcast.broadcast('catClicked',{cat:this.cat});
};

//MainController
$scope.$on('catClicked',function() {
  switchView();
});
...
function switchView() {
  $scope.viewList = !$scope.viewList;
  $scope.viewDetail = !$scope.viewDetail;
}

After (use location to change state):

// CatListController
$scope.getDetail = function (catId) {
  $location.path('/cat/' + catId + "/" + $routeParams.page);
};

The CatsService module was completely overhauled to include a getCat() method in addition to getCats(), along with a few other useful methods. Oh, and this version actually has the ability to contact the live Petfinder.com API! Using a config constant, the app can switch between grabbing cat data from a .json file on disk, or reaching out to the live Petfinder API (via a PHP proxy on my own server). While building the app, I usually relied on a static json file for data, but it’s nice to be able to contact a live service to return fresh data.

The returned data is also stored in the CatService module. This data is basically the catCollection property that I previously stuck in $rootScope. Storing application data and state information on $rootScope just didn’t seem right, so I created some properties and accessor methods (get/set) on CatService, and used that instead. Worked out quite nicely.

Below is the new CatService module. Notice that it’s quite a bit larger that the previous module. This time around, the service had a few private variables and functions that the service uses for itself, and the CatService object is returned by the factory and injected into the controllers. The properties of the CatService object are exposed to the controllers and can be used wherever CatService is injected.

The first major feature of this updated service is the remoteSvc resource. Instead of just grabbing a .json file from disk, it will contact the proxy.php service and grab live data returned from the Petfinder API. The query object passed into the $resource function demonstrates how to override default $resource methods. This really isn’t necessary in my case, but I did it just for kicks.

The line var service = (ENDPOINT === "LOCAL") ? localSvc : remoteSvc; will look at the ‘ENDPOINT’ constant set in the application config (see app.js), and determine whether to use the local json file, or server resource to gather the cat data. It’s a quick and dirty approach, and in a larger system it would be better to abstract this into a proper config file, with two separate service modules, but it works pretty well for a small app with one or two services.

The CatService object has some getters and setters for accessing the selected cat, and the selected cat’s index value in the cat collection array. And, of course, there are the getCats and getCat methods, which gather the necessary data for the list and detail views (respectively).

angular.module('catService', ['ngResource','CatServiceHelper'])
.factory('CatsService', function($resource,$filter,$routeParams,CatServiceHelper,ENDPOINT){
    /**
     * Private vars
     */

    // Set the max number of cats to retrieve from the Petfinder API
    var numberOfCatsToGet = 200;

    // Set up the $resource injectable to use the Petfinder API. Some custom options are used in the $resource.query method
    var remoteSvc = $resource('/jsCats/ngCats/proxy.php', {count:numberOfCatsToGet,offset:0},
                { query: {
                      method:'GET',
                      params:{action:'shelter.getPets',count:numberOfCatsToGet, offset:0},
                      isArray:true
                     }
                });

    // Use the cats.json file as the $resource, if necessary
    var localSvc = $resource('cats.json',{});

    // Decide which $resource to use as the actual service
    var service = (ENDPOINT === "LOCAL") ? localSvc : remoteSvc;

    // Private properties & methods used to store data shared between controllers
    var _cats = [];
    var _activeCatIndex = -1;
    var _setActiveCat = function _setActiveCat(catId,callback) {
                          var cat = CatsService.findCatInCollection(catId);
                          if (cat) {
                            _activeCatIndex = _cats.indexOf(cat);
                            callback(cat);
                          }
                        };

    /**
     * CatService Object returned by the factory function.
     * Contains instance methods that can be used by the controllers.
     */

    var CatsService = {
     
      /**
       * ### Accessor Methods
       */
     
     
      // Controllers can read the cat collection, but cannot change it without calling a service method.
      getCatCollection : function getCatCollection() {
        return _cats;
      },

      getActiveCatIndex : function getActiveCatIndex() {
        return _activeCatIndex;
      },

      setActiveCatIndex : function setActiveCatIndex(idx) {
        if (angular.isNumber(idx) && idx <= _cats.length) {
          _activeCatIndex = idx;
        } else {
          _activeCatIndex = -1;
          throw("activeCatIndex must be a number, and cannot exceed the length of the cat collection.");
        }
      },

      /**
       * ### Service Methods
       */


      // This method retrieves all cat data from the service and stores it in _cats
      getCats : function getCats(callback){
        var self = this;
        service.query(function (data) {
          var rawCatData = [];
          if ( data && data.length > 0 && data[1].petfinder.pets.pet && data[1].petfinder.pets.pet.length > 0 ) {
            rawCatData = data[1].petfinder.pets.pet;
          }
          angular.forEach(rawCatData,function(item){
            _cats.push( CatServiceHelper.translateCat(item) );
          });
          if (angular.isFunction(callback)) callback(_cats);
        });
      },

      // This method retrieves data for one cat.  It will also call getCats if the user
      // navigates directly to a catDetail page. This is necessary for the Back and
      // Next/Prev buttons to work correctly.
      getCat : function(catId, callback) {
        var self = this;
        if( _cats.length ) {
          _setActiveCat(catId,callback);
        } else {
          this.getCats(function (results) {
              _setActiveCat(catId,callback);
          });
        }
      },

      // This uses an AngularJS filter function to extract a cat from the cat collection by the cat's id
      findCatInCollection : function findCatInCollection(catId) {
        var cat = $filter('filter')(_cats, function (item) {
                return item.id.toString() === catId;
              });
        return cat[0] || 0;
      },

      // Use the default $resource.query method
      query : service.query,
     
      // Use the default $resource.get method
      get : service.get
   
    };

    // The factory function returns CatsService, which is injected into controllers.
    return CatsService;
});