The code side of life

Automating component registration in AngularJS

Introduction

user

Shlomi Assaf


AngularJS TypeScript

Automating component registration in AngularJS

Posted by Shlomi Assaf on .
Featured

AngularJS TypeScript

Automating component registration in AngularJS

Posted by Shlomi Assaf on .

Bootstrapping an AngularJS application involves a lot of registration, we have to register every component we create so we can use it later with our precious friend, mister DI.

The manual way

The process looks the same for every component: Using the angular application module, call the appropriate registrar and supply a name and a type (function).

var app = angular.module('myApp', []);
app.service('myService', function() {
    /* SOME SERVICE LOGIC HERE */
});

Simple, works but of course, bad practice. Ask John Papa why.

angular
    .module('myApp', [])
    .service('myService', myService);

function myService() { 
    /* SOME SERVICE LOGIC HERE */
}

This is much better.

How to register?

If we break the last code sample to pieces, we find that there are 2 basic code blocks:

  • Component registration.
  • Component implementation.

When defining & implementing a component we register it on the spot, usually in the same file, every time for every component. What if we can register all components at once, in one location, potentially avoiding the use of the 'angular' object completely and de-coupling angular from the process?

If you think about it, a component is a single unit of work. In a certain aspect, it shouldn't care about the technology just do the work.

This is how myService.js will look like, no angular registration:

function myService() { 
    /* SOME SERVICE LOGIC HERE */
}

What about automatic registration?

Using namespace's and OOP techniques (implemented in JS) we can create a system that register's component automatically, here is a short description how:

  • Group components by type. (Service, Controller, etc.., this is actually namespacing)
  • Describe a factory for every component. (also support JS minify)
  • Go over every item in a group and register it. (introspection)

Quite simple.

Yep, TypeScript!

Using TypeScript we can achieve this with little effort as it supports Namespace's and Class's for JavaScript, however it can be done with pure JavaScript of course.

Wait? Namespace's? What??? Well, namespace like behavior is the right term. The implementation in TypeScript converted to pure JS is actually simple nested JS object's.

My OCD needs a Namespace

The purpose here is to organize components into groups, naming needs to follow common sense, no other rules. I usually assign the first namespace to be the application name, so if my program is called webapp this is the name of the 1st namespace. The 2nd namespace usually describe the group's content, so webapp.servies holds all of the services in the application.

Remember myService from our previous examples? lets try it with TypeScript:

module webapp.services {
        export class myService {    
        }
}

So, the reference to myService will be webapp.services.myService. Remember that we created a Class thus the reference is to a constructor function.

The 4 lines of code above are translated to 12 pure JavaScript lines of code, emulating namespace's, OOP and more while doing it using best practices for JavaScript:

var webapp;
(function (webapp) {
    var services;
    (function (services) {
        var myService = (function () {
            function myService() {
            }
            return myService;
        })();
        services.myService = myService;
    })(services = webapp.services || (webapp.services = {}));
})(webapp || (webapp = {}));

Here is a JSON representation of a made up namespace tree:

// webapp =
{
    "services": {
        "myService": function() {},
        "hisService": function() {},
        "herService": function() {}
    },
    "directives": {
        "myDirective": function() {},
        "hisDirective": function() {},
        "herDirective": function() {}
    }
    "utils": {
        "stringUtil": {},
        "numericUtil": {}
   }
}

Consider the tree structure above, iterating webapp.services will give us the name (key) and constructor (value) for every service in our application. Exactly what we need to register a service with angular.

Dress code, please

Using namespace's we now have the information needed to register components, this is true if all components are the same but they are not.

  • Services & Directives are Class's, constructor function's, angular invoke them using the new keyword.
  • Controllers are the same but we don't need to register router-linked controllers.
  • Filters use simple functions.
  • You get the idea...

Also, the current DI engine resolve injections via parameter names, we need to make sure they stay the same after a minify/uglify process.

The solution is a factory function pinned to every component as metadata.
In TypeScript terms, we assign metadata to a Class via Static members.
In JavaScript terms, every function is an object so it can also hold members.

module webapp.services {
        export class myService {

                public static factory() {
                        return [
                                '$http',
                                ($http) => new myService($http)
                        ];
                }

                constructor(private $http) {

                }
        }
}

If you want to see a pure JavaScript version, please use the TypeScript Playground.

The static factory method is later used to create components where each component is responsible to its own instantiate process (if any...) and stay valid after minification.

It is also possible to use the $inject static member to declare injectables.

Here is another example, registering a filter component where angular excepts a function not a constructor.

module webapp.filters {
        export class myFilter {

                public static factory() {
                        return [                
                                () => new myFilter().filterAction
                        ];
                }

                public filterAction(value: string) { 
                        return value + " is filtered.";
                }

                constructor() {     
                }
        }
}

I'm all tidy, lets do it

By now, we have everything we need to register the components in our application.

Usually, a Class should represent the bootstrap process but for simplicity I will show a simple function that does the registration.

function registerComponents(components, registrar) {
    for (var name in components) {
        var component = components[name];
        if (component.factory && typeof component.factory === "function") {
                registrar(name, component.factory());
        }
    }
}

The function need's 2 parameters:

  • components: An object holding components. for example: myapp.services
  • registrar: A function that register components, for example: angular.module('myApp').service

The logic is simple, go over all components in the object, do some validation and apply some logic. If good do the registration.

A TypeScript bootstrap class might look like this:

module webapp {
      export class Loader {
            public app: ng.IModule;

            registerComponents(components: any, registrar: (string, Array) => any) {
          for (var name in components) {
            var component = components[name];
            if (typeof component.factory === "function") {
          registrar(name, component.factory());
        }
      }
    }

            init(appName: string, externalModules: Array<ng.IModule>) { 
                  this.app = angular.module(appName, externalModules);
                  this.registerComponents(webapp.services, this.app.service);         
                  this.registerComponents(webapp.directivs, this.app.directive);
                  this.registerComponents(webapp.filters, this.app.filter);
                  // And more if needed.
    }

            constructor() {     
                  this.init('myApp', []);
            }
      }
}

Using the angular DefinitelyTyped TypeScript definition.

The load order of files does not matter, as long as you instantiate the Loader after all files are loaded, I use an inline JavaScript execution script in my `index.html':

<script type="text/javascript">new webapp.Loader();</script>

Extending functionality

The registration function in the example above is simple, it has no logic and minimal validation.
It can be extended to support any schema your team wishes to implement, perform multiple validations and enforce complex interfaces.

For example, a Controller can be loaded via Router or it needs to be loaded manually since it has no direct route, if this is the case you can implement a schema to load controllers if certain conditions are met.

A single place for registering components can be used for many purposes, dynamic file loading, logging, eager loading and more... In the process your components are real Class's now and does not care who the registrar is.

Benefits using this approach are:

  • Systematic code schema across the project.
  • Clear self-explained code structure.
  • Single point of registration.
  • Decoupling (It will make it easier to port to Angular 2.0 or even other frameworks)

At last...

I`v been using this approach for almost 2 years, the downside is that after such a long time you forget how to manually register a component in angular :)

If your familiar with Angular 2.0 you'v probably noticed that metadata is heavily used to describe components, instead of grouping components by types it marks them using metadata. TypeScript 1.5 enables this via Attributes which essentially translate to static Class members or function members in pure JavaScript. While the Angular 2.0 implementation is much more complex and tied to the dependency injector the approach is not so different :)

If you have any notes, found any errors or have nice implementation ideas to this approach, please comment below.

Thank you!

user

Shlomi Assaf