0

I would like to implement prototypal inheritance in Angular where by base type is defined as an Angular value. The problem is setting my child type prototype. Suppose this simplified example:

File 1

angular.module("Test")
       .value("BaseController", BaseController);

BaseController.$inject = [...];

function BaseController(...) {
    ...
}

BaseController.prototype.func = function () {
    ...
};

File 2

angular.module("Test")
       .controller("ChildController", ChildController);

ChildController.$inject = ["BaseController", ...];

function ChildController(BaseController, ...) {
    BaseController.call(this, ...);

    // save reference for later
    ChildController.base = BaseController;
    ...
}

// ERROR - Clearly why
ChildController.prototype = Object.create(BaseController.prototype);

// ERROR - instance properties aren't available on this level
ChildController.prototype = Object.create(ChildController.base.prototype);

Inheritance

The problem is that prototype is being generated before constructor is being instantiated. But until constructor is being instantiated I have no possible reference of the angular-injected BaseController.

The only way I can see to solve this is to have my BaseController publically defined so I can access it even before angular injects it into my constructor. I don't like this as I can't have my code private inside function closures and I would also like to use angular's features as much as possible without having a mixture of usual Javascript against Angular code.

Main question

Is there any way that I could make prototypal inheritance work by having base types defined as values (or other) in angular?

Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404

3 Answers3

1

This solution is specifically for your approach. You can use the module's run block to assign the prototype. In File 2 add the following:

angular.module("Test").run(function(BaseController) {
  ChildController.prototype = Object.create(BaseController.prototype);
});

BaseController gets injected and is available for creating the prototype. Since this code runs before any controller gets instantiated you get your prototypal inheritance.

Also keep in mind that ChildController.$inject has to contain all of BaseController.$inject.

An alternative would be to attach BaseController to the module itself:

angular.module("Test").BaseController = BaseController;
...
ChildController.prototype = Object.create(angular.module("Test").BaseController.prototype);

The code would still be private and the constructor function is still only accessible through the module.

You can also look for alternatives to inheritance. Depending on the situation hierarchical controllers might be a viable solution.

<div ng-controller="BaseController"><%-- Handle Generic stuff --%>
  ...
  <div ng-controller="ChildController"><%-- Handle specific stuff --%>
a better oliver
  • 26,330
  • 2
  • 58
  • 66
  • This is actually quite nice except that there's one problem. If I have any prototype functionality on my child type it gets completely overwritten by this assignment operation. I should rather *extend* existing prototype, because this `run` is being executed after child type's prototype is already being set. And if I decide to extend I would then be just monkey patching my child type which makes this answer very very similar to the other one. The main difference being where prototype is being extended. – Robert Koritnik Apr 23 '15 at 06:26
  • Nobody keeps you from defining the whole prototype within the `run` block. An alternative would be to attach `BaseController` directly to the module. – a better oliver Apr 23 '15 at 07:12
  • That's something @Bergi also suggested, but I must admit that I don't know what you both mean by *injecting directly into module*? Because that surely is something I should consider... If it is what I understand it is then this would be the best possible solution to this problem. Can you please elaborate a bit on it? Maybe put your answer into two sections: *.run* and *injection into module* or something similar. – Robert Koritnik Apr 23 '15 at 09:10
  • 1
    @RobertKoritnik I don't mean injection. I updated the answer. – a better oliver Apr 23 '15 at 09:49
  • Well that's not something I was looking for, but it surely works. If nothing else it makes these types (properties of a specific module) local to module so I don't have to publish them in global scope. At least that. Not sure if this hack is something I'd like to implement but it surely is a nice trick. – Robert Koritnik Apr 24 '15 at 08:10
  • Check [my answer](http://stackoverflow.com/a/29859217/75642) and see how I've finally implemented it. It works actually as prototypal inheritance in browsers supporting `setPrototypeOf` and satisfactorily in older ones that don't (IE of course). - Generally though, if there's some common Angular functionality it should be extracted in its own service and used within others that need it. Nice and easy, but doesn't seem to make sense to inject service functions to controllers that should inherit common functions. – Robert Koritnik Apr 24 '15 at 23:37
0

If you are trying to extend controllers to create a mixin, then prototypal inheritance is not what you want to do.

For an example of extending controller behavior checkout this SO Question: angular-extending-controller

Community
  • 1
  • 1
Enzey
  • 5,254
  • 1
  • 18
  • 20
0

Important info

  1. This solution is implemented specifically for Angular-related code that's using actual Javascript prototypes to define controllers/services and not just anonymous functions as shown in everyday examples on the web - this means following John Papa's styleguide and extending it even further as he doesn't use real prototypes

  2. All constructors must use explicit DI annotations using $inject static property. This limitation can be somewhat worked around by

    • using Angular injector's annotate (if annotations are provided inline - array) or
    • changing .inherits signature to include all base type's constructor parameters in correct order as its own parameters i.e.
      Child.inherits(Base, baseInjection1, baseInjection2, ...)

Setting proper prototypal inheritance in Angular

I've come up with a rather simple and most of all generic way to set type inheritance of my Angular controllers. Well this inheritance goes even beyond that and could be used with any Angular asset that uses type constructors (controllers, services, etc.)

Resulting solution changes original files' contents to this superbly simplistic form:

File1

angular.module("Test")
       .value("BaseController", BaseController);

BaseController.$inject = [...];

function BaseController(...) {
    ...
}

BaseController.prototype.func = function () {
    ...
};

File2

angular.module("Test")
       .controller("ChildController", ChildController);

ChildController.$inject = ["BaseController", ...];

function ChildController(BaseController, ...) {
    // ALL MAGIC IS HERE!
    ChildController.inherits(BaseController, arguments);
    ...
}

So all we have to do is make one call in our child type's constructor and provide base type (that's injected for us by Angular DI into constructor) and child type's constructor parameters (so inheritance can use them when running base type's constructor).

Implementing .inherit functionality

To make things generic I've added this function to Function.prototype object so it becomes available to all functions (or better yet constructors). This is how it's implemented:

Function.prototype.inherits = function inherits(BaseType, constructorInjections) {
    /// <summary>Sets type's inheritance to base type.</summary>
    /// <param name="BaseType" type="Function" optional="false">Base type to set for this child type.</param>
    /// <param name="constructorInjections" type="Array" optional="true">Child type constructor injection instances.</param>

    // return if both angular types don't use explicit DI
    if (!this.$inject || !BaseType.$inject) return;

    // DRY
    if (this.prototype.__proto__ === BaseType.prototype || Object.getPrototypeOf(this.prototype) === BaseType.prototype) return;

    // #region construct base object instance

    // make a quick-search dictionary of child constructor injections
    for (var i = 0, l = this.$inject.length, dic = {}; i < l; i++)
    {
        dic[this.$inject[i]] = i;
    }

    // create base type's constructor injections array
    for (var i = 0, l = BaseType.$inject.length, baseParams = []; i < l; i++)
    {
        baseParams.push(constructorInjections[dic[BaseType.$inject[i]]]);
    }

    // get base type's constructed instance
    var baseInstance = BaseType.apply(baseInstance = {}, baseParams) || baseInstance;

    // #endregion

    // #region set type inheritance chain
    if (Object.setPrototypeOf)
    {
        Object.setPrototypeOf(this.prototype, BaseType.prototype);
    }
    else
    {
        // doesn't do the same thing, but it should work just as well
        angular.extend(this.prototype, BaseType.prototype, { __proto__: BaseType.prototype });
    }
    // #endregion

    // #region add base class generated instance to prototype
    for (var i = 0, keys = Object.keys(baseInstance), l = keys.length; i < l; i++)
    {
        this.prototype[keys[i]] = baseInstance[keys[i]];
    }
    // #endregion
};
Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404