Making Skinny AngularJS Controllers

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

In AngularJS, controllers can sometimes become monolithic structures of mixed business and view logic, as it’s all too easy to house anything you need for the view inside the controller. It’s convenient, and it just plain works… until your application grows in complexity or needs unit tests.

So why do we want skinny controllers, anyway?

In short, separation of concerns. Ideally, everything from services, to controllers, to directives, and more should be skinny, and achieving this is very possible in AngularJS. Each part should have a single responsibility and the controller’s responsibility should be to communicate between services and the view; i.e. its main concern should be view-model logic.

AngularJS 2.0 will also eschew the notion of the general “controller” in AngularJS 1.x in favor of Components, which will contain their own View and Controller. By moving logic to services, your application will become more decentralized with simple, reusable services that any controller can consume. This level of organization will make the transition to 2.0 much smoother.

However, AngularJS 1.x isn’t going anywhere anytime fast, and it’s likely that it will still be used and supported for at least a few more years. Given that, let’s get the skinny on organizational strategies that we can use today.

The Controller

As an example, let’s make a simple goat database app, with the ability to view goats, search for certain goats, edit goats, and save those goats back to the database. Initially, here’s what a controller for such an app might look like:


angular.module('Goats') 
.controller('GoatsController', ['$scope', '$http', function($scope, $http) { 
    $scope.goat = null;
    $scope.editingGoat = false;
    $scope.goatQuery = null;
    $scope.goats = [];
    
    // Edit a goat database entry
    $scope.editGoat = function(goat) {
        $scope.goat = goat;
        $scope.editingGoat = true;
    };
    
    // Save a goat database entry
    $scope.saveGoat = function() {
        $http.post('/goats', $scope.goat)
            .then(function() {
                $http.get('/goats')
                    .then(function(response) {
                        $scope.goat = null;
                        $scope.goats = response.data;
                        $scope.editingGoat = false;
                    });
            });
    };
    
    // Discard an edit of a goat database entry
    $scope.cancelGoat = function() {
        $scope.editingGoat = false;
    };
    
    // Search the goat database
    $scope.searchGoats = _.debounce(function(query) {
        $http.get('/goats/search/' + query)
            .then(function(response) {
                $scope.goats = response.data;
            });
    }, 300);
    
    // Initially load the goats
    $http.get('/goats')
        .then(function(response) {
            $scope.goats = response.data;
        });
}]);

Immediately, you might notice that we are trying to do everything in the controller – $http calls, search logic, maintaining state, etc. This is a bad practice, and there are a few things we can do to shrink down this controller:

Let’s $scope out an alternative.

AngularJS 2.0 does away with $scope as everything is bound to plain ES6 classes, so you might as well get used to not using $scope. There are other good reasons to forego $scope, such as removing the temptation to overuse $watch, $apply, $on, $parent, and other methods of $scope that can bloat your controller. Chances are, if you are using one of those methods, it can be deferred to a service. Opt for the Controller as syntax whenever possible, and remember, you can still inject $scope if you need to use one of its methods.

Do yourself a service or three.

Looking at our controller, we can see that the saveGoat and searchGoats methods are using $http to get and post goats – actions that can (and should) be delegated to services. Our controller should be almost completely agnostic as to how data is retrieved and sent, and should only be concerned with the general action of sending and retrieving data to and from the service.

Here’s what a generic Goats service can look like:


angular.module('Goats')
.service('GoatsService', ['$http', function($http) {
    this.saveGoat = function(goat) {
        return $http.post('/goats', goat);
    };

    this.searchGoats = function(query) {
        return $http.get('/goats/search/' + query);
    };
    
    this.getGoats = function() {
        return $http.get('/goats');
    };
    
    this.getGoat = function(name) {
        return $http.get('/goat/' + name);
    };
}]);

Use UI-Router to state the obvious.

Separating your application into states will greatly improve the organization of your code, with a little bit of planning. We can infer three possible states of the application:

  • List of all the goats – the initial state
  • Search results, given a goat search query
  • Edit state for modifying a single goat

In each of these states, we can determine what needs to get resolved, ideally, before the state loads:

  • List – resolve all goats
  • Search – resolve filtered goats by search query
  • Edit – resolve single goat by ID

Using resolve in UI-router is powerful, because it allows you to provide your controller with data before it gets loaded. This saves you the burden of asynchronously making $http calls (and even service calls) inside your controller and promotes a separation of concerns.

The resolve object inside each state takes key-value pairs, where the key is the name of the resolved dependency that you can inject into your controller, and the value is a function that returns the value of your dependency, whether it’s a promise or any other type of value. Promises will get resolved before the controller is instantiated. You can read more about resolving dependencies in UI-Router’s wiki.

Here’s what our route configuration for the goat app can look like:


angular.module('Goats')
.config(['$stateProvider', function($stateProvider) {
    // List goats state
    $stateProvider
    .state('goats', {
        url: '/goats',
        resolve: {
            goats: ['goatsService',
                function(goatsService) {

                return goatsService.getGoats();
            }],
            goat: function() { return {}; }
        },
        templateUrl: '...',
        controller: 'goatsController',
        controllerAs: 'goats'
    })

    // Search goats state
    .state('search', {
        url: '/goats/search/:query',
        resolve: {
            goats: ['$stateParams', 'goatsService',
                function($stateParams, goatsService) {

                return goatsService.searchGoats($stateParams.query);
            }],
            goat: function() { return {}; }
        },
        templateUrl: '...',
        controller: 'goatsController',
        controllerAs: 'goats'
    })
    
    // Edit goat state
    .state('goat', {
        url: '/goats/:name',
        resolve: {
            goats: function() { return []; },
            goat: ['$stateParams', 'goatsService',
                function($stateParams, goatsService) {

                return goatsService.getGoat($stateParams.name);
            }]
        },
        templateUrl: '...',
        controller: 'goatsController',
        controllerAs: 'goats'
    });

In each state, we’re resolving both goats and goat, since we are using the same controller for all three states. When they aren’t relevant in the state, goats or goat is just resolved as an empty object. Ideally, you can further modularize your code by having a separate controller for each state, but for this example, we will use a single controller. These will be injected into our GoatsController, available for immediate use.

If you need a refresher on UI-Router, check out these helpful articles:

The Controller, refactored.

After employing these good practices — avoiding $scope, separating logic out into services, and taking advantage of UI-Router by separating our app out into states and utilizing resolves, our refactored controller benefits by being much more simplified, clearer, and easier to unit test. Take a look:


angular.module('Goats')
.controller('goatsController', ['$state', 'goatsService', 'goats', 'goat',
    function($state, goatsService, goats, goat) {

    this.goat = goat.data;

    this.goatQuery = $state.params.query;
    
    this.goats = goats.data;
    
    this.saveGoat = function() {
        goatsService.saveGoat(this.goat)
            .then(function() {
                $state.go('goats');
            });
    };
    
    this.searchGoats = function(query) {
        if (!query.length) return $state.go('goats');
    
        $state.go('search', {query: query});
    };
}]);

With the $state dependency injected, we can transition to states via controller methods. Remember that since each state has our previously defined resolves, we don’t need to worry about duties such as retrieving queried goats or getting the initial goat list inside the controller.

Conclusion

Aim to make your controllers skinny, as well as the rest of your application, by separating out view logic and business logic into controllers and services, respectively, and by taking advantage of routes and resolves.

David Khourshid

David Khourshid is a front-end web developer in Orlando, Florida. He is passionate about JavaScript, Sass, and cutting-edge front-end technologies. He is also a pianist and enjoys mathematics, and is constantly finding new ways to apply both math and music theory to web development.