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?

8
  • I think you should inject the BaseController in your ChildController module, not in the constructor? Commented Apr 22, 2015 at 13:46
  • @Bergi: What do you mean? Enlighten me? Do you mean to have a kind of dependent module with base types and provide it as my module requirement/dependency? How would I get it on the module level then? Commented Apr 22, 2015 at 13:51
  • You could set the prototype within a run block. Commented Apr 22, 2015 at 13:54
  • @zeroflagL please write an answer with a working example of your proposed solution. Commented Apr 22, 2015 at 14:08
  • is using ES6 an option for you? it would be easir to think about inheritance Commented Apr 22, 2015 at 14:56

3 Answers 3

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 --%>
Sign up to request clarification or add additional context in comments.

6 Comments

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.
Nobody keeps you from defining the whole prototype within the run block. An alternative would be to attach BaseController directly to the module.
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.
@RobertKoritnik I don't mean injection. I updated the answer.
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.
|
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

Comments

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
};

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.