Reusable Angular controller methods

Jun 05, 2015 in AngularJS

Angular offers a great flexibility in the way you code. However probably weak part is that it doesn't really promote the idea of inheritance of components. It's not that simple actually although possible to extend basically everything, overwrite or decorate. In this post I will cover a simple and effective solution for controller inheritance.

Inheritance in simple terms is the pattern that makes possible to reuse certain parts of code between different components. The notion of inheritance is not necessarily class-based. So, being class-less language (at least until syntactic sugar brought by ES6 classes) Javascript still offers plenty of means for making code reusable. Let's cover one possible solution for extending Angular controllers.

Extending controllers

There are two approaches for defining controller methods. The first one uses controller $scope object to put methods as its properties. Thus scope object being exposed to view template also brings controller functions into template. The second approach which has become popular since Angular version 1.2 declares methods using controller as syntax, when methods are kept separated from scope, and are created as controller instance object (this) properties.

However it doesn't matter what way you prefer. The only thing that you need to understand is that in both cases you want to extend objects, e.g. populate one object with properties from another. You might have never thought of it, but this is one of the simplest inheritance patterns!

Let's consider the following snippet of code:

app.controller('OneController', function($scope) {
    $scope.name = 'One';
});

app.controller('TwoController', function($scope) {
    $scope.name = 'Two';
});

In above example there are two controllers. Say we want both controllers to have the same set of methods, how to make them inherit from some BaseController?

The answer is quite simple: if we want to extend object, let's use dedicated tools. Every framework offers some utility for mixin one object properties into another. In Angular there is angular.extend. We will use it then.

Now we need to decide where to put reusable methods for this pattern. I found it convenient to use custom service to store base controller methods. So it can look like this:

app.service('BaseController', function($routeParam) {
    this.setName = function(name) {
        this.name = name;
    };
});

Using service for controller methods might seem strange at first but it actually makes sense. First of all, BaseController configured this way is a singleton object which is good from performance perspective. Secondary, it can leverage from convenient dependency injection of other services, and base controller will need it for sure. And last but not least, such service-controller can be easily tested, mocked in tests.

Here is how actual extension routine will look:

app.controller('OneController', function($scope, BaseController) {
    angular.extend($scope, BaseController);
    $scope.name = 'One';
});

app.controller('TwoController', function($scope, BaseController) {
    angular.extend($scope, BaseController);
    $scope.name = 'Two';
});

That's really it. One single line of code copies methods from base object (instance of the BaseController) into current $scope.

One important detail. You might wonder why I put methods on this in BaseController? Well, since we are using service we can't inject $scope in it, because it can only be injected into controller connected to DOM node, and since in out setup BaseController is technically detached abstract object - it can't have $scope injection. Okay, but doesn't controller need scope object to put methods on it, etc? Well, I would say that using this to define an "abstract" scope-less object is an advantage. Indeed, it doesn't matter what this points to BaseController can be mixed into both $scope driven and controller-as controllers:

// OneController uses $scope to defined methods
app.controller('OneController', function($scope, BaseController) {
    angular.extend($scope, BaseController);
    $scope.name = 'One';
});

// TwoController uses controller-as to define methods
app.controller('TwoController', function($scope, BaseController) {
    angular.extend(this, BaseController);
    this.name = 'Two';
});

Disclaimer

We need to be careful with this pattern due to the fact that angular.extend performs shallow copy of the properties. It means that non-primitive types from base controller will get shared between extended controllers. This can lead to unintended behaviour. Just something to be aware of. However, I would argue that having non-method properties in base-controller is a good idea anyway, but if it's important then deep copying is needed, for example with jQuery.extend, etc.

Conclusion

We discussed simple patter for effective controller inheritance. This methods can be used in the situations when base controller is used to store reusable methods.

comments powered by Disqus