Two Approaches to AngularJS Controller Inheritance
It is exceedingly rare to find yourself building a web application that has no need for shared code or inherited hierarchies of functionality. Sub-pages in a site section with shared headers, for example, or a collection of distinct widgets that all work on the same underlying principles. As ever Don't Repeat Yourself is the guiding principle, and in AngularJS the easiest ways to manage this sort of situation are as follows:
- Move shared functionality into services that are injected as dependencies where needed.
- Identify features that appear on multiple pages and turn them into directives, so that the managing code is written only once.
- Create a hierarchy of inherited controllers and place common functionality in ancestor controllers.
These are all valid approaches, and each tends to be more or less useful than the others in any particular circumstance. They can be mixed and matched in any given application. Here, however, I'll restrict myself to looking at controller inheritance.
GitHub Repository
Before diving in, I should mention that I've made available a GitHub repository containing notes and example code to show controller inheritance via the methods outlined in this post.
Controller Inheritance via $injector
In AngularJS it is trivial to request any defined dependency at any point via the $injector service. This is easy to abuse in ways that will make your life difficult later, but it also has its uses. Here is how we can use it to arrange controllers into an inheritance hierarchy, starting with the definition of a generic parent controller:
function ParentController($scope, myService) { // In this form of inheritance, properties are set in reverse order: child sets // first, then parent. So a property that is intended to be overridden has to // check for its own existence. // // e.g. Only create this function if it hasn't already been set by the child. this.decorateScope = this.decorateScope || function () { $scope.decorator = myService.getDecorator() + 2; } // Setting up an initialize function is another necessary part of enabling child // controllers to override features of the parent. All setup is delayed to the // end of instantiation, allowing individual functions and properties to be // overridden first. this.initialize = this.initialize || function () { this.decorateScope(); } // This will be invoked last of all when a child controller is instantiated. initialize(); }
Next we define a child controller that inherits from the parent above:
function ChildController($injector, $scope, myService) { // Override the parent function while still allowing further children to override // it. this.decorateScope = this.decorateScope || function () { $scope.decorator = 44; } // This is the magic by which this controller inherits from the ParentController. // Essentially the ParentController function is invoked on "this" and is passed // dependencies directly. // // Note that this means a child controller must have all of the dependencies // required by the parent. $injector.invoke(ParentController, this, { $scope: $scope, myService: myService }); }
It is actually possible to skip passing any dependencies other than $scope from the child to the parent controller. Everything will work until you start to write test code in which you are injecting mock dependencies. Any dependency that is not explicitly passed into $injector.invoke() will be instantiated as the real thing, not the mock specified in the test case. This is obviously very inconvenient and will completely sabotage your tests - so always pass in all of the dependencies explicitly.
This method of inheritance supports convenient mixins, as a mixin works in exactly the same way as inheritance:
function ChildController($injector, $scope, myService, otherService) { // Any number of controllers can be invoked in this way. They will apply // their properties and overrides in the order they are invoked. So if you // want a mixin to override the parent, it has to come first. $injector.invoke(MixinController, this, { $scope: $scope, otherService: otherService }); $injector.invoke(ParentController, this, { $scope: $scope, myService: myService }); }
Note that in this scheme of inheritance you can't define functionality in controller constructor prototypes. That will be ignored by $injector.invoke(), which is in essence just a function call bound to "this". You must define functions inside the constructor body, as shown above.
Controller Prototypical Inheritance
Standard issue Javascript prototypical inheritance works well for AngularJS controllers, provided you are prepared to stick to an ordering scheme for controller function arguments. To start you'll want to define an inheritance function somewhere convenient:
/** * A clone of the Node.js util.inherits() function. This will require * browser support for the ES5 Object.create() method. * * @param {Function} ctor * The child constructor. * @param {Function} superCtor * The parent constructor. */ angular.inherits = function (ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false } }); };
Set up a parent controller as a straightforward constructor with prototype methods:
// Define a parent controller. function ParentController($scope, myService) { this.$scope = $scope; this.myService = myService; this.initialize(); } /** * Decorate the scope. */ ParentController.prototype.decorateScope = function () { this.$scope.decorator = myService.getDecorator() + 2; } /** * Initialize the controller for use. */ ParentController.prototype.initialize = function () { this.decorateScope(); }
Use standard prototypical inheritance to define the child controller:
function ChildController($scope, myService) { // No need to explicitly pass the injected dependencies, provided they // are ordered consistently. ChildController.super_.apply(this, arguments); } angular.inherits(ChildController, ParentController); /** * Override the parent function. * @see ParentController#decorateScope */ ChildController.prototype.decorateScope = function () { this.$scope.decorator = 44; }
Mixins are less graceful, however. One approach is to call the mixin constructor and copy over its prototype functions:
function ChildController($scope, myService, otherService) { // By using mixins, you give up the ability to structure arguments to avoid // specifying them explicitly. // Invoke the parent constructor. ChildController.super_.call(this, $scope, myService); // Invoke the mixin constructor. MixinController.call(this, $scope, otherService); } // Define the inheritance. angular.inherits(ChildController, ParentController); // Add the mixin prototype functionality to the ChildController prototype, // provided it doesn't already exist - i.e. is overridden. angular.forEach(MixinController.prototype, function (value, name) { if (ChildController.prototype[name] === undefined) { ChildController.prototype[name] = value; } });