patternjavascriptMinor
Monkey patching angularjs controllers to have instance functions with injectables
Viewed 0 times
angularjswithcontrollersmonkeyinstanceinjectablesfunctionspatchinghave
Problem
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
in every controller constructor.
What I ended up with is This hack. The fiddle demonstrates how two separate instances of
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
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
Solution
I took to heart your recommendation about staying away from native angular.js code. I didn't use inheritance, though. What I eventually did is create an angular service function which is invoked from inside the controller constructor function.
The code looks like this:
Usage would look like:
using
The code looks like this:
angular.module ('myServicesModule')
.factory ('injecorate', function ($injector) {
function _attachScope (ctl, scope) {
if (scope) {
ctl.$scope = scope;
}
}
function _attachServices (ctl, deps) {
if (ctl.injecorated) {
return;
}
var proto = Object.getPrototypeOf (ctl);
var depNames = _.isArray (ctl.$inject) ?
ctl.$inject :
$injector.annotate (proto.constructor);
var services = _.object (depNames, deps);
services = _.omit (services, '$scope');
_.defaults (proto, services);
proto.injecorated = true;
}
/**
* "injecorate" = "inject and decorate"
* this function receives a controller instance, along
* with its injectable dependencies, and its `$scope` object, and decorates the controller's
* prototype with those injectable dependencies, so that they
* will be accessible to the controller's instance functions as, e.g. `this.$location`.
* @param {function} ctl controller constructor function
* @param {arguments} deps the controller's arguments, as passed by the $injector service.
* @param {$scope} scope This controller instances's `$scope` object, as injected into the constructor.
*/
return function (ctl, deps, scope) {
_attachScope (ctl, scope);
_attachServices (ctl, deps);
};
});Usage would look like:
angular.module ('myModule')
.controller ('MyController', (function () {
/*@ngInject*/
function MyController ($scope, $state, $http, injecorate) {
injecorate (this, arguments, scope);
}
MyController.prototype.myFunction () {
// access injectables using e.g. "this.$scope"
}
return MyController;
}()));using
/@ngInject/ instructs ng-annotate to annotate the controller prior to minification.Code Snippets
angular.module ('myServicesModule')
.factory ('injecorate', function ($injector) {
function _attachScope (ctl, scope) {
if (scope) {
ctl.$scope = scope;
}
}
function _attachServices (ctl, deps) {
if (ctl.injecorated) {
return;
}
var proto = Object.getPrototypeOf (ctl);
var depNames = _.isArray (ctl.$inject) ?
ctl.$inject :
$injector.annotate (proto.constructor);
var services = _.object (depNames, deps);
services = _.omit (services, '$scope');
_.defaults (proto, services);
proto.injecorated = true;
}
/**
* "injecorate" = "inject and decorate"
* this function receives a controller instance, along
* with its injectable dependencies, and its `$scope` object, and decorates the controller's
* prototype with those injectable dependencies, so that they
* will be accessible to the controller's instance functions as, e.g. `this.$location`.
* @param {function} ctl controller constructor function
* @param {arguments} deps the controller's arguments, as passed by the $injector service.
* @param {$scope} scope This controller instances's `$scope` object, as injected into the constructor.
*/
return function (ctl, deps, scope) {
_attachScope (ctl, scope);
_attachServices (ctl, deps);
};
});angular.module ('myModule')
.controller ('MyController', (function () {
/*@ngInject*/
function MyController ($scope, $state, $http, injecorate) {
injecorate (this, arguments, scope);
}
MyController.prototype.myFunction () {
// access injectables using e.g. "this.$scope"
}
return MyController;
}()));Context
StackExchange Code Review Q#68479, answer score: 3
Revisions (0)
No revisions yet.