Bringing the simplicity of Attributes/Annotations into Angular 1.X

in Front-End Web Technologies

dec

One of the more interesting features of Angular 2 is its improved dependency injection mechanism

Unlike Angular 1.X you don’t need to specify the dependencies manually in order to preserve minification capabilities

Angular 2 is smart enough to use the type information of the dependency and inject the correct implementation at run time

Can we do the same with Angular 1.X ? Sure, keep reading …

First lets describe the way we define a controller

import {MyApp} from '../App';
import {Controller} from '../Common/Decorators';
import {Contact, ContactService} from '../Services/ContactService';
 
@Controller(MyApp, "HomeCtrl")
class HomeCtrl {
    contacts: Contact[];
 
    constructor(contactService: ContactService) {
        contactService.getAll().then(contacts => {
            this.contacts = contacts;
        });
    }
}

And the service dependency

import {MyApp} from '../App';
import {Service, Inject} from '../Common/Decorators';
 
@Service(MyApp, "contactService")
export class ContactService {
    constructor(@Inject("$http") private $http: ng.IHttpService) {
    }
 
    getAll(): ng.IPromise<Contact[]> {
        return this.$http.get("/contacts.json").then(response => response.data);
    }
}
 
export interface Contact {
    id: number;
    name: string;
}
 

Second we need to define the Controller and Service decorators (A.K.A attribute/annotation)

export function Controller(module: ng.IModule, name: string) {
    return function (target: Function) {
        Reflect.defineMetadata("controller", target, { controllerName: name });
 
        module.controller(name, target);
 
        set$inject(target);
    }
}
 
export function Service(module: ng.IModule, name: string) {
    return function (target: Function) {
        Reflect.defineMetadata("service", { serviceName: name }, target);
 
        module.service(name, target);
 
        set$inject(target);
    }
}

For more information on ES7 decorators read hereReflect.defineMetadata is part of the ES7 reflection API. I am using this polyfill

The interesting implementation reside inside the set$inject function which fills Angular’s $inject metadata with all correct dependencies

function set$inject(target) {
    var paramtypes: any[] = Reflect.getMetadata("design:paramtypes", target);
 
    var $inject = [
    ];
 
    paramtypes.forEach((type, index) => {
 
        var parameterName = null;
        var metadata;
 
        if (metadata = Reflect.getMetadata("service", type)) {
            parameterName = metadata.serviceName;
        }
        else {
            throw new Error("Failed to resolve dependency: " + type);
        }
 
        $inject.push(parameterName);
    });
 
    target.$inject = $inject;
}

We use Reflect.getMetadata API and extract all parameter types from the constructor. For each parameter type we check if it is a registered service and extract its name

We can extend that solution and support Angular 1.X components and other injectables. A nice component declaration may look like below

import {MyApp} from '../App';
import {Component, Inject} from '../Common/Decorators';
 
@Component(MyApp, "clock", {
    templateUrl: "/Scripts/Components/Clock.html",
})
class ClockComponent {
    time: Date;
 
    constructor( @Inject("$interval") $interval) {
        this.time = new Date();
 
        $interval(() => {
            this.time = new Date();
        }, 1000);
    }
}

If you are interested with the full solution then contact me through my e-mail ori.calvo@gmail.com

 

Contact us
You might also like
Share: