Tuesday, February 9, 2016

Building AngularJS Applications using Inheritance

It might be argued that you do not want to use inheritance for controllers or other AngularJS constructs because controllers are tightly bound to their views and therefore are unique. But I have a certification-style application that uses eleven different views for the start page, individual question types, and the results. All of these views have similar underlying functionality to work with the question being presented. Applying the DRY principle requires that I do not want to duplicate that functionality in every controller.

Even though the examples at angularjs.org are full of closures I would like to avoid them as much as possible. Building inheritance chains with prototypes is more efficient. Singleton services where the closures are only defined once may jump out as place where these closures are technically not less efficient, but sticking to a single pattern of object creation across the application is also very important. If you need some help on how prototypes work then visit this blog post: JavaScript Inheritance Best Practices. In fact nested scopes use prototypal inheritance, so I should use the built-in scope inheritance, right?

All the time I see examples of using the prototype-chain of scopes to leverage what the parent of a nested controller provides. So I could have a top-level controller that defines the shared methods I need in its scope and then the nested views will inherit them in their scopes, of course as long as I do not use isolate scopes. But when I do that I am using a controller to set up what the nested scopes inherit and I need that controller to know what the nested views are going to need.

Having the super-type know what to provide to the nested views is exactly the opposite of how inheritance should work: the super-type should not know about the extended types. The dependency is in the other direction, the extended types need to define which super-type they inherit from! So scope inheritance is a bad implementation of inheritance for both data and methods. Using prototypal inheritance in controllers, services, and model types is a a much better approach.

Unfortunately the registration process in an AngularJS module does not encourage using prototypes. Provider, service, and controller registration require a constructor function. To register a constructor with a prototype definition means that the prototype must be defined before registration. There are three ways to approach this within the AngularJS framework.

Global Constructor Function-Objects

The first choice is the traditional way of building the constructors outside of AngularJS as global function-objects. That simply is not the best way; the window object gets cluttered up with all the definitions and it leads to conflicts between modules. In fact, that is why in the JavaScript community we have replaced creating global constructors with the modular frameworks CommonJS and AMD (the Asynchronous Module Definition).

Providers Type Injection

Another choice is to use AngularJS providers as the vehicle to modularize the constructors. Honestly I prefer the third approach, module loaders. I will explain that why in a moment, after I explain how we can use providers.

Using providers involves registering a provider with the type constructor as a property, and returning the same object at the config and run injection phases. Returning the same object is important, because we may want to access to the new type constructor at either level:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// questionControllerTypeProvider
//

(function () {
    
    var module = angular.module('typeInjector');
    
    module.provider('questionControllerType', [ function () {
        
        this.QuestionController = function ($scope, assessmentService) {
            
            this.$scope = $scope;
            this.assessmentService = assessmentService;
        };
        
        this.QuestionController.prototype = Object.assign(Object.create(Object.prototype), {
                
            getCurrentQuestion: function () {

                return this.assessmentService ? this.assessmentService.getCurrentQuestion() : null;
            },

            gsResponse: function (response) {

                return this.assessmentService.gsResponse(this.getCurrentQuestion(), response);
            }
        });
        
        this.$get = function () {
            
            return this;
        }
    }]);

}).call(this);

Wherever the provider is injected new instances of the constructor type can be created, or new types linked to the constructor prototype. To build the controller I will establish a controller provider that defines the controller constructor in its constructor, and then registers the new controller. Remember when you inject a provider into a provider before configuration append the word "Provider" to the end of the provider name: questionControllerType is injected as questionControllerTypeProvider.

You cannot use the module.controller method to register a controller inside the provider because of the "late" registration, they only way is to inject $controllerProvider to register the new controller. A similar thing happens with services, you must inject $provider to use the factory or service methods. Note: the forward reference on line 10 works because the JavaScript engine compiles the functions before it executes the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// multipleChoiceQuestionControllerTypeProvider
//

(function () {
    
    var app = angular.module('typeInjector');
    
    app.provider('multipleChoiceQuestionControllerType', [ '$controllerProvider', 'questionControllerTypeProvider', function ($controllerProvider, questionControllerProvider) {
        
        $controllerProvider.register('MultipleChoiceQuestionController', [ '$scope', 'assessmentService', MultipleChoiceQuestionController ]);
        
        function MultipleChoiceQuestionController($scope, assessmentService) {
            
            questionControllerProvider.QuestionController.call(this, $scope, assessmentService);
            this.$scope.response = [ ];
        };
        
        MultipleChoiceQuestionController.prototype = Object.assign(Object.create(questionControllerProvider.QuestionController.prototype), {
            
            save: function () {
                
                this.gsResponse(JSON.stringify(this.$scope.response));
            } 
        });
        
        this.$get = function () {
            
            return this;
        }
    }]);

}).call(this);

Note that I followed another best-practice: the controller methods are attached to the controller object and it is intended to be used with the "controller as" syntax in AngularJS. The data should not be attached to the controller; the best-practice choices are to put model data properties directly on $scope or use getter-setter methods defined in the controller. Putting references to the model data on $scope treats it as the "view-model," or the model that the view uses for rendering. That separates the specific data the view uses from the larger context of the model data the application uses.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html>
    <head>
        <title>Type-Injection Example</title>
        <script src="components/object-assign-shim/index.js"></script>
        <script src="components/angular/angular.js"></script>
        <script src="components/angular-route/angular-route.js"></script>
        <script src="modules/app.js"></script>
        <script src="modules/app-config.js"></script>
        <script src="types/questionTypeProvider.js"></script>
        <script src="services/assessmentServiceTypeProvider.js"></script>
        <script src="controllers/questionControllerTypeProvider.js"></script>
        <script src="controllers/multipleChoiceQuestionControllerTypeProvider.js"></script>
        <script src="modules/app-bootstrap.js"></script>
    </head>
    <body>
        <div data-ng-controller="MultipleChoiceQuestionController as vc">
            <p>{{ vc.getCurrentQuestion().text }}</p>
            <form>
                <span data-ng-repeat="choice in vc.getCurrentQuestion().choices">
                    <input type="checkbox" data-ng-model="response[$index]" data-ng-true-value="true" data-ng-false-value="false" /> {{ choice }}<br/></span><br/>
                <input type="button" value="Save" data-ng-click="vc.save()" />
            </form>
        </div>
    </body>
</html>

The reason that I do not like this form is that anything inheriting from a type, such as a controller, must be wrapped in the constructor of a separate provider registration. That is not what a provider is designed for; it is not intended to be used as a container to register another element. Doing so is convoluted, potentially confusing, and unexpected in the context of a provider.

Using these providers this way also complicates unit testing. The provider must be instantiated to register the controller.

Module Loaders

The best practice is to look outside of AngularJS and use a module loader to define modules that wrap all of the AngularJS definitions. CommonJS or AMD definitions provide a scope for all definitions, avoid conflicts at the global level, and do not clutter up the window object.

In this RequireJS example I use AMD modules to define the super-type of QuestionController and register the MultipleChoiceQuestionController. I still apply the best-practice I discussed earlier of using "controller as" and putting the view-model data directly on $scope.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// QuestionController
//

define([ 'services/assessmentService' ], function () {

    function QuestionController($scope, assessmentService) {

        this.$scope = $scope;
        this.assessmentService = assessmentService;
    };

    QuestionController.prototype = Object.assign(Object.create(Object.prototype), {

        getCurrentQuestion: function () {

            return this.assessmentService ? this.assessmentService.getCurrentQuestion() : null;
        },

        gsResponse: function (response) {

            return this.assessmentService.gsResponse(this.getCurrentQuestion(), response);
        }
    });
    
    return QuestionController;
});

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MultipleChoiceQuestionController
//

define(['modules/app', 'controllers/QuestionController', 'services/assessmentService' ], function (app, QuestionController) {

    app.controller('MultipleChoiceQuestionController', [ '$scope', 'assessmentService', MultipleChoiceQuestionController ]);

    function MultipleChoiceQuestionController($scope, assessmentService) {

        QuestionController.call(this, $scope, assessmentService);
        this.$scope.response = [ ];
    };

    MultipleChoiceQuestionController.prototype = Object.assign(Object.create(QuestionController.prototype), {

        save: function () {

            this.gsResponse(JSON.stringify(this.$scope.response));
        } 
    });
    
    return MultipleChoiceQuestionController;
});

A big advantage of using RequireJS is that the modules are dynamically loaded, so all I need to have in the HTML document is a link to RequireJS and define where the main script is.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
    <head>
        <title>RequireJS Prototypal Inheritance Example</title>
        <script src="components/requirejs/require.js" data-main="main.js"></script>
    </head>
    <body>
        <div data-ng-controller="MultipleChoiceQuestionController as vc">
            <p>{{ vc.getCurrentQuestion().text }}</p>
            <form>
                <span data-ng-repeat="choice in vc.getCurrentQuestion().choices">
                    <input type="checkbox" data-ng-model="response[$index]" data-ng-true-value="true" data-ng-false-value="false" /> {{ choice }}<br/></span><br/>
                <input type="button" value="Save" data-ng-click="vc.save()" />
            </form>
        </div>
    </body>
</html>

Best-Practice

Placing responsibility where it belongs is a fundamental principle of both object-oriented programming and modularization, and the responsibility of dynamically loading dependency modules is not under the AngularJS umbrella. Moving the definitions of types out of AngularJS and into a module loader such as RequireJS or Bowerify is the cleanest approach. It is easier to follow, eliminates extra definitions of AngularJS providers and the use of providers beyond their intended scope.

Choosing between a monolithic JavaScript file or dynamic loading of individual modules on demand goes beyond exploring the inheritance aspect of programming in the AngularJS framework. Keep in mind that the load-on-demand feature of RequireJS is an excellent tool during development, but it can still provide a monolithic file for deployment as Browserify does.

The full code for the working examples is available at: http://askjoelit.com/download/AngularJsPrototypalInheritance.zip. The two projects the zip file contains are wrapped with a Node-express application. Remember to use "npm install" and "bower install" to retrieve the dependencies before launching the servers with "npm start."

References

See the references page.

No comments:

Post a Comment