1

I'm using Webpack with angular 1.6.x and Typescript and I quit using angular DI in favor of ES6 imports. When I need some ng functions like $http, $resource and such I inject them directly using the angular.injector function through a decorator, like this:

// inject.ts
    import * as angular from 'angular';

    export function inject (...params: string[]) {

        function doCall ( param: string, klass: Function) {
            const injector = angular.injector([ 'ng' ]);
            const service = injector.get(param);
            try {
                klass.prototype[ param ] = service;
            } catch ( e ) {
                window.console.warn( e );
            }
        }

        // tslint:disable-next-line:ban-types
        return function ( klass: Function ) {
            params.forEach( ( param ) => {
                doCall( param, klass );
            } );
        };
    }

// posts.service.ts
import { inject } from './inject';
import { IPost, Post } from './post';

@inject('$http')
export class PostsService {
    public $http: angular.IHttpService;
    get (): Promise<IPost[]> {
        const posts: IPost[] = [];
        const promise = new Promise<IPost[]>( (resolve, reject) => {
            this.$http.get<IPost[]>('https://jsonplaceholder.typicode.com/posts')
            .then(( response ) => {
                response.data.forEach(item => {
                    posts.push( new Post(item) );
                });

                resolve( posts );
            });
        });

        return promise;
    }
}


// post.ts
export interface IPost {
    userId: number;
    id: number;
    title: string;
    body: string;
}
export class Post implements IPost {
    userId: number;
    id: number;
    title: string;
    body: string;

    constructor (item: IPost) {
        this.userId = item.userId;
        this.id = item.id;
        this.title = item.title;
        this.body = item.body;
    }
}


// controller.ts
import { IPost } from './post';
import { PostsService } from './posts.service';

export class Controller {
    public postService: PostsService;
    public posts: IPost[];
    constructor ( private $scope: angular.IScope ) {
        this.postService = new PostsService();
    }

    $onInit () {
        this.postService.get()
        .then((posts) => {
            this.posts = posts;
            this.$scope.$digest();
        });
    }
}

// index.ts
import * as angular from 'angular';

import { Controller } from './app/controller';

import './index.scss';

export const app: string = 'app';

angular
  .module(app, [])
  .controller('controller', Controller);


angular.bootstrap(document.body, [app]);

I don't know if it's in compliance with best practices or not, but it is working quite nicely so far.

I would like to hear your thoughts on the subject: is there any problem (performance, bad practice and such) using this approach?

2
  • Angular modules/DI and ES6 modules compliment each other. They aren't interchangeable. directly using the angular.injector - and it's terribly wrong, you will rarely need to use angular.injector ever, because it doesn't do what you expect it to do. Commented Sep 1, 2017 at 14:50
  • I feel like this is a good question, but out of place here as written. You have code which is completely functional, which is the first red flag, along with the statement "I would like to hear your thoughts on the subject" which is almost directly quoted in dont-ask. I do think there is merit in the question, but you may want to consider rewording it to be more about the hows and whys of this technique, for QA sake, rather than a discussion piece. Commented Sep 1, 2017 at 15:46

1 Answer 1

4

ES modules cannot replace Angular modules and DI. They compliment each other and keep the application modular and testable.

ES6 modules provide extra layer of extensibility, for example controller/service subclassing (something that doesn't look good with Angular modules and DI alone).

The recommended approach with ES6 or TypeScript is to do DI conventionally, with $inject annotation:

export class PostsService {
  static $inject = ['$http'];
  constructor(
    public $http: angular.IHttpService
  ) {}
  ...
}

It's also a good practice to have one module per file, this way the application stays modular and testable:

export default angular.module('app.posts', [])
  .service('posts', `PostsService)
  .name;`

Its default export is module name that can be imported in another module that directly depends on it:

import postsModule from '...';
...
export default angular.module('app.controller', [postsModule])
  .controller('controller', Controller)
  .name;`

Application injector cannot be normally reached from a decorator. Even if it's possible with a hack to make it work in production, it will be messed up in tests.

angular.injector creates new injector (i.e. application instance) and has very limited proper uses in production:

angular.injector(['ng']).get('$rootScope') !== angular.injector(['ng']).get('$rootScope');

It is often misused when a developer doesn't know how to get current $injector instance. It certainly shouldn't be used in a case like this one.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks. I did know about this approach you posted here, but just came up with this idea of using angular.injector to use angular private functions through a decorator, and didn't know if it was a good one or not. I think I'll stick with angular DI by now.
Been there, done that. No, it's not a good practice for sure. It's possible in theory to expose an injector during bootstrapping and use it in decorators, but this may break things because there may be several injectors at once. The style shown in the answer is generally desirable for TypeScript (it may differ for ES6).

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.