Reflections on our first AngularJS App

22 Aug 2013 posted by Jaz Lalli

Since we started using AngularJS a few months ago, we’ve read lots of great articles from lots of different people. Many talk in-depth about technical features of the framework, whilst others talk about personal journeys and lessons learnt during building an application. Given that this is our first foray into building an app using AngularJS, this post falls squarely in the second category.

Around June this year, we decided to give the totallymoney.com mortgages site a much needed revamp. At the time, it was a pretty basic site consisting of a simple tabbed-table of mortgage products, grouped by type, with links through to the providers site. Given the complexity of the mortgage market (so I’m told by everyone that’s bought a house), there was obviously an awful lot more we could do to help consumers find the right mortgage. Ultimately, our grand aim would be to allow people to sift through all mortgages on the market, collect details of their personal circumstances, and narrow down to the one product that best suits them. The first step in that journey would be something altogether different, but we got cracking in earnest.

It was fairly obvious from there that we’d be building a (jargon alert) ‘rich front-end app’, and so we chose to use AngularJS to build it. The reasons for that aren’t particularly meaningful, it was more a case that a few members of the team had tried it out in their own time, and they’d been pretty impressed.

The ability to go from zero to productive using AngularJS is certainly one of the major attractions to it. Understanding how to get started, and implementing basic, common features can be acheived in a matter of hours. In not too long, we were able to make some API calls using $http, get some filtering implemented, and a modal form for submitting data. Quite quickly though, we found ourselves in a position where certain components had grown quite large, and figuring out what was going on in those parts wasn’t obvious at first glance. As well as that, we were expecting greater flexibility from the AngularJS router that turned out not to be possible.

AngularJS Router

ng-router wasn’t quite what we were expecting. We had expected a route to be composed of multiple partial views, so that visual components of the application could be broken out into smaller, more manageable parts. On the product table for example, we wanted a number of partial views and corresponding controllers for the sidebar, the same for a table row, and a parent view and controller wrapped around the lot. This would mean that we could compose the app by introducing discreet features and their components, rather that it all being intertwined. However, with the Angular router, it turns out that this isn’t possible.

Instead we decided to use ui-router from the AngularUI project. The stated aim of that module is to relate an AngularJS route to the concept of a state. It supports nested, parallel and named views, all of which allow you to modularize your app more effectively. How you actually implement it is very similar to ng-router, instead of $route and $routeProvider, you use ui-routers $state and $stateProvider, as shown below.

app.config(['$stateProvider', function($stateProvider) {
    $stateProvider
        // inherited state
        .state('table', {
            templateUrl: 'mortgages/home'
        })
        .state('table.mortgages', {
            url: '/table/?type&purpose&loanRequired&propertyValue&termLength&mortgageType',
            views: {
                // named, parallel views
                'sidebar': {
                    templateUrl: 'app/partials/table/sidebar.html'
                },
                'results': {
                    templateUrl: 'app/partials/table/table.html',
                    controller: 'TableCtrl'
                }
            },
            // you can use resolve in exactly the same way as with ng-router
            resolve: {
                Lenders: ['MortgageLenderService', function(MortgageLenderService) {
                    return MortgageLenderService.promise;
                }]
            }
        });
}]);

Services, services, services

For the product table, we ended up with the main controller trying to do far too much. It used $http to obtain API data, initialized view data, handled clicks, and watched and reacted to model properties by calling update functions on itself. In addition, there was one filter function to rule them all, which had to be explicitly called on certain events. Value changes on some of the filters also required another HTTP call to get fresh data, and then was sorting on top of that. All told, it amounted to 500+ LOC.

The first step in breaking things apart was to create partial views with discreet controllers for them. We then moved the code across to each relevant controller which enabled us to see the common parts that needed to share data. For those parts, we needed services! Over time, we’ve found that it’s services that are one of the most powerful and useful features in AngularJS. They allow you to maintain order within a medium-large sized app, by creating owners for your data. For any given distinct chunk of data, we created a service, which could be shared across controllers. The services encompass the HTTP calls for data retrieval, and logic controlling initialization of the data based on state parameters. They also enable you to use the resolve feature during routing by exposing the promise returned by $http methods (as shown above).

Testing

The upshot of breaking things into smaller components of course, is testability, and hence maintainability. As always, we had the best intentions to unit test as we went along, but given that we were learning the tools as we went, most of our time went into understanding the framework rather than testing code. However, since carrying out the refactoring, testing has become much easier, and far less daunting, which means we’re gradually increasing our coverage.

We’re using the Karma test runner, and writing tests using Jasmine, and so far it’s been pretty smooth going. We’ve found the key to writing effective tests is in the setup of mock dependencies and data, and having had similar such experiences writing C#, the concepts have been easy to pick up. The less familiar part has been in writing tests and mocking things in JavaScript. We’re gradually wrapping our head around things, and for the time being, the snippet below is fairly typical of what a test suite looks like.

describe('CompareMortgagesPopUpCtrl Unit Tests', function () {
    var target,
        scope,
        compareService;

    beforeEach(module('remortgages'));

    beforeEach(inject(function ($injector) {
        var $rootScope = $injector.get('$rootScope');
        scope = $rootScope.$new();
        
        compareService = $injector.get('CompareService');
        spyOn(compareService, 'mortgagesToCompare').andReturn([{ProductId: 1}, {ProductId: 2}]);
        
        var $controller = $injector.get('$controller');
        target = $controller('CompareMortgagesPopUpCtrl', {
            '$scope': scope,
            'CompareService': compareService
        });
    }));

    it('should create a valid CompareMortgagesPopUpCtrl', function () {
        expect(target).toBeDefined();
        expect(target).not.toBe(null);
    });
    
    it('should get mortgages from the compare service', function () {
        expect(compareService.mortgagesToCompare).toHaveBeenCalled();
    });
    
    it('should put the mortgages for comparison onto the scope', function () {
        expect(scope.mortgagesToCompare).toEqual(compareService.mortgagesToCompare());
    });
});

Conclusions

Building an application for production in AngularJS has been really fun. At no point have we felt like we’re having to wrestle with it in order to do what we want. A major plus is the fact that it uses dependency injection, something that is familiar to us given our background in C#.

The most beneficial feature of it from our point of view has been services. We haven’t needed to write too many directives as yet, so we haven’t pushed the envelope in that respect, but for what we’re doing, being able to utilize services has been a godsend. We’re looking forward to doing more with it, and perhaps trying it out new projects in the future.