In the spirit of Google's angular.js Style Guide, I set out to define my angularjs controllers as classes, complete with instance functions. I ran into trouble, however, when trying to access injectable services (like $resource) from inside one of those instance functions.
I wanted a way to avoid having to write
function MyController ($resource, $location) {
this.$resource = $resource;
this.$location = $location;
...
}
in every controller constructor.
What I ended up with is This hack. The fiddle demonstrates how two separate instances of MyController
can access the injectable service $window
and invoke this.$window.console.log()
from the instance function MyController.prototype.inc()
, triggered by clicking a button.
$window
is automatically available as an object member this.$window
without having to explicitly assign it in the constructor.
The mechanism is a sort of set-and-forget hack, monkey-patching some built-in angularjs functions.
I would appreciate any suggestions on how to improve it, and also if you think using this pattern ("controllers-as-classes-with-instance-functions") is in fact any better than the usual "put-all-the-logic-in-the-constructor" approach.
This is the code:
/**
* "decoject" = "decorate" + "inject"
* decorate the global "angular" object with a new function
* which receives a controller constructor function, along
* with its injectable dependencies, and decorates the controller's
* prototype with those injectable dependencies, so that they
* will be available to the controller's instance functions.
*/
angular.decoject = function (ctl, deps) {
if (ctl.prototype.decojected) {
return;
}
var decorations = {'decojected': true};
var injs = _.zip (angular.injector ().annotate (ctl), deps);
// injs is now an array of key-value sub-arrays, of the form
// [ ['$scope', $scope], ['$location', $location], ... ]
injs.forEach (function (inj) {
// Add each injectable to decorations object
var name = inj[0];
var obj = inj[1];
if (ctl.prototype[name] === undefined) {
decorations[name] = obj;
}
});
angular.extend (ctl.prototype, decorations);
}
/**
* Monkey patch "angular.module()", in order to
* monkey patch "angular.module ().controller()", in order to
* monkey patch controller constructor functions
* so that they invoke "angular.decoject()" on controller instantiation.
*/
angular.origModule = angular.module;
angular.module = function () {
var mod = angular.origModule.apply (this, arguments);
if (!mod.origController) {
// monkey patch "module()" function
mod.origController = mod.controller;
mod.controller = function () { // monkey patch "controller()" function
var name = arguments[0];
var ctor = arguments[1];
// wrap the original constructor with our own.
// method adopted from http://stackoverflow.com/questions/10101508/how-do-i-wrap-a-constructor
function CtorWrapper () {
angular.decoject (CtorWrapper, arguments);
ctor.apply (this, arguments);
}
CtorWrapper.prototype = Object.create (ctor.prototype);
// annotate our wrapper, so that injector know to pass required injectables
CtorWrapper['$inject'] = angular.injector ().annotate (ctor);
return this.origController (name, CtorWrapper);
};
}
return mod;
};
angular.module ('myapp', [])
.controller ('MyController', (function () {
// Use IIFE to define controllers with instance functions.
// Idea adopted from http://www.bennadel.com/blog/2421-creating-angularjs-controllers-with-instance-methods.htm
function MyController ($scope, $window) {
$scope.ctrl = this;
this.val = 0;
}
MyController.prototype.inc = function () {
// demonstrate use of instance function in controller
// with injectable "$window" on "this"
this.val++;
this.$window.console.log ('got here: ' + this.val);
}
return MyController;
})());
MyBaseController.call(this, $resource, $location);
in each of your controller constructor functions. UseFooController.prototype = Object.create(MyBaseController.prototype)
to set up the inheritance chain. \$\endgroup\$