Tuesday 13 May 2014

The magic of $resource

If you look at demos around using $resource in Angular then you'll see code something like this

app.factory("issuesResource", function ($resource) {
    return $resource('/api/issues/:id', {
        id: "@id"
    }, {
        update: {
            method: 'PUT'
        }
    });
});
so, what does this do/mean.

Breaking it down:

app.factory
creates a factory. Normally you would see something like this:
app.factory("myFactory", function(){
    return {
      doSomething: function(){}
    };
});
so when creating a $resource based service the code looks a little odd, it returns $resource(), so what does the call to $resource do?

$resource is "A factory which creates a resource object" according to the documentation, so what our code does is to take $resource (already a factory) and wrap it in our factory and then extend it. We extend it by telling it what the base URL is ('/api/issues' in this case), telling it where to get an 'id' value from when needed (more on this is a moment) and extending it with our own methods.

The last object passed to $resource

update: {
   method: 'PUT'
}
adds an 'update' method to $resource and causes $resource to use an HTTP PUT when this method is called.

The other parameter

{
    id: "@id"
}
is more interesting. According to the documentation then: "If the parameter value is prefixed with @ then the value of that parameter is extracted from the data object (useful for non-GET operations)." So this implies that the parameter is not used for GETs, and indeed this is so. I have a spec that looks like this:
it("should return an object when get is called", inject(function (issuesResource) {
    httpBackend.expectGET("/api/issues/1").respond(200, {id: 1});
    var issues = issuesResource.get({id: 1});
    httpBackend.flush();
    expect(issues.id).toBe(1);
}));
and this works whether the '@id' parameter is in place or not. For GETs it's the name of the placeholder in the URL that matters, so the 'id' in the '/api/issues/:id' has to match the 'id' in the call to
issuesResource.get({id: 1});
, however this is not true for POST, PUT or DELETE. In these cases the presence and name of the '@id' value is important.

So if we have a test for POST spec that looks like this:

it("should use the id passed in the object when posting", inject(function (issuesResource) {
    httpBackend.expectPOST("/api/issues/1").respond(200, {id: 1});
    var issues = issuesResource.save({id: 1});
    httpBackend.flush();
    expect(issues.id).toBe(1);
}));
then in the call to save the name of the property ('id') must match the name defined when creating the factory.

Angular application layout

Most of this I've got from John Papa's Pluralsight course on Breeze and Angular, some is my own, I wanted to write it as a step-by-step otherwise I forget it:
  1. Install all the libraries using Bower

    Bower is a package manager for the web, using this makes it easier to manage dependencies and general stuff around packages such as updates. I install all the libraries I can find in Bower such as Angular (and it's sub libraries such as cookies and resources), jQuery, moment etc...

    I have a .bowerrc file that specifies the directory I put the packages into, mine go into public/bower

  2. Create an 'app' directory. This will contain all my application code and will be split into a set of subdirectories
    • The root contains the startup code, routing configuration, common configuration (such as events and the actual app loading code) and error handling code
    • common: contains shared code, such as logging
    • layout: contains the layout files used across the site (sidebars, navigation etc.)
    • [others]: one subdirectory per location, 'home', 'admin' etc
    • services: contains all the application's 'services' including the directives the application uses
  3. Each 'location' contains the HTML, controller and service needed for that functionality. This means that the 'authentication' section will have the layout (but not the directives), controller and service that authentication needs to work. This provides, at least, a location aware form of packaging. Should the controller and service be loaded into their own module, I'm not sure yet. On the one hand that seems like overkill, but on the other hand it may mean better isolation when testing
  4. Load all code as IIFEs. Each component is then self loading, it does all the steps necessary to set itself up within angular (such as registration). The code looks something like this:
    (function () {
        'use strict';
        var controllerId = "IndexController";
    
        angular.module('app').controller(controllerId, ["$scope", "common", 'someService', IndexController]);
    
        function IndexController($scope, common, someService) {
            var vm = this;
    
            activate();
    
            function activate() {
                common.activateController([], controllerId)
                    .then(function () {
                        log('Activated Main Page');
                    });
            }
    
        }
    }());
    
    plus all the other code the controller needs of course. Again this feels like a reasonable approach, there is a 'standard' activate method (that calls a common activateController method) that can be used to wire up events and/or data after the controller and its services have been fully loaded.

Modularisation is one of the things I'm still unsure about. It feels that putting everything into the 'app' module is wrong, it also feels wrong to put each functional area into its own module; maybe have the an 'app' module, a 'controllers' module and a 'services' module, still to be decided I think.