HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Monkey patching angularjs controllers to have instance functions with injectables

Submitted by: @import:stackexchange-codereview··
0
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

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:

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.