6
\$\begingroup\$

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;
})());
\$\endgroup\$
8
  • \$\begingroup\$ Sounds like you could use inheritance here, rather than redefining a core framework method. It could make upgrading AngularJS in the future a real pain. \$\endgroup\$ Commented Oct 31, 2014 at 15:01
  • \$\begingroup\$ Trouble with upgrades is indeed once of my fears. However, it sounds like upgrading to angular2.0 is going to be a pain whatever I do. Also, angularjs already provides mechanisms to decorate framework objects. \$\endgroup\$
    – Gil Elad
    Commented Oct 31, 2014 at 15:21
  • \$\begingroup\$ How would you implement a solution using inheritance without requiring special code on each controller definition? \$\endgroup\$
    – Gil Elad
    Commented Oct 31, 2014 at 15:24
  • \$\begingroup\$ You can't, no matter which language you happen to be using. This is a case where "Don't Repeat Yourself" is superseded by the need to not rewrite a framework, which incurs more maintenance headaches than a few lines of repeated code. Just call MyBaseController.call(this, $resource, $location); in each of your controller constructor functions. Use FooController.prototype = Object.create(MyBaseController.prototype) to set up the inheritance chain. \$\endgroup\$ Commented Oct 31, 2014 at 15:58
  • \$\begingroup\$ I added my previous comment plus a code example as an answer. \$\endgroup\$ Commented Oct 31, 2014 at 16:06

2 Answers 2

3
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

You must strike a balance between "Don't Repeat Yourself" and rewriting a part of a third party framework. Monkey patching AngularJS can (and probably will) make upgrading the framework in the future much harder. Instead, this is a valid use case for inheritance:

function BaseController($resource, $location) {
    this.$resource = $resource;
    this.$location = $location;
}

function MyController($resource, $location) {
    BaseController.call(this, $resource, $location);

    // ...
}

MyController.prototype = Object.create(BaseController.prototype);

MyController.prototype.constructor = MyController;
MyController.prototype.foo = function() {
    this.$resource...
};

While every controller constructor will need to accept the $resource and $location arguments, the setting of those values are at least centralized in the BaseController constructor, which is what you want.

You have common dependencies across your application, and as far as I know, AngularJS only performs constructor injection. It would be nice if they could do property or setter injection as well, which would clean up your constructor code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.