Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly use dependency injections in Angular2?

I have had mixed success with resolving issues with DI. I have read a few tutorials and I get the jist but after making some nested DIs with my custom services things have started to fall apart.

Could someone explain when to use useFactory instead of useClass? I have seen the ng2 docs and I have seen the examples but I cannot map them to my problems. Currently my bootstrapper looks like this:

bootstrap(
    App,
    [
        FORM_PROVIDERS,
        ROUTER_PROVIDERS,
        HTTP_PROVIDERS,
        provide(LocationStrategy, { useClass: PathLocationStrategy }),
        provide(RequestOptions, { useClass: DefaultRequestOptions }),
        provide(MsgService, { useClass: MsgService }),
        provide(HttpAdvanced, { useFactory: (MsgService, HTTP_PROVIDERS) => new HttpAdvanced(MsgService, HTTP_PROVIDERS), deps: [MsgService, HTTP_PROVIDERS] }),
        provide(AuthService, { useFactory: (HttpAdvanced) => new AuthService(HttpAdvanced), deps: [HttpAdvanced, HTTP_PROVIDERS, MsgService] }),
        provide(FormBuilderAdvanced, { useFactory: (FormBuilder, HttpAdvanced) => new FormBuilderAdvanced(FormBuilder, HttpAdvanced), deps: [FormBuilder, HttpAdvanced] }),
        provide(MsgServiceInternal, { useClass: MsgServiceInternal })
    ]
);

And my latest issue is:

EXCEPTION: Error during instantiation of AuthService! (HeaderBar -> AuthService).
ORIGINAL EXCEPTION: TypeError: this.http.get is not a function

My dependencies work like

HttpAdvanced        -> Http(ng2), MsgService
MsgService          -> MsgServiceInternal
AuthService         -> HttpAdvanced
FormBuilderAdvanced -> FormBuilder(ng2), HttpAdvanced

1. Am I properly using provide / useClass / useFactory and how do I provide services that have other dependencies?

Also, I have at one place in my code:

static isUserInjector() {
    return (next, prev) => Injector.resolveAndCreate([AuthService, provide(HttpAdvanced, { useClass: HttpAdvanced })]).get(AuthService).isUser();
}

Because I want to have a function that I provide to

@CanActivate(AuthService.isEditorInjector())

but I can't use constructor injection because @CanActivate is outside of the class scope so I can't inject the service inside the controller and then reference like @CanActivate(this.authService.isEditor())

2. What would be a good solution for this?

SOME CODE:

@Component({
    selector: 'ShowStats',
    templateUrl: './dest/views/showStats/showStats.html',
    directives: [ COMMON_DIRECTIVES, UsersCount, AdminsList, GlobalWishlist, PopularTrack ]
})
export class ShowStats {
    authService : AuthService;

    constructor( authService : AuthService ){
        this.authService = authService;
    }
}

... next file ...

@Injectable()
export class HttpAdvanced {
    msgService: MsgService;
    http: Http;

    constructor(msgService: MsgService, http: Http) {
        this.msgService = msgService;
        this.http = http;
    }

    /*
     * This is for plain ol' GET requests .. with callback of course.
     */
    public get(url, callback) {
        return this.http.get(url).subscribe((res) => {
            let data = res.json().data;
            callback(data);
        }, this.msgService.httpErrorHandler);
    }
.... other code for HttpAdvanced

3. Does the order of importing files matter for DI? I think i noticed that since I have MsgService and MsgServiceInternal in the same file and MsgService depends on Internal that I had to put Internal before but I'm not 100% Is it the same of order of importing ?

4. So if I simply do:

bootstrap(
    App,
    [
        FORM_PROVIDERS,
        ROUTER_PROVIDERS,
        HTTP_PROVIDERS,
        provide(LocationStrategy, { useClass: PathLocationStrategy }),
        provide(RequestOptions, { useClass: DefaultRequestOptions }),
        MsgService,
        HttpAdvanced,
        AuthService,
        FormBuilderAdvanced,
        MsgServiceInternal
    ]
);

I get:

Cannot resolve all parameters for 'FormBuilderAdvanced'(?, ?). 
Make sure that all the parameters are decorated with Inject or have 
valid type annotations and that 'FormBuilderAdvanced' is decorated 
with Injectable.

I used to remove this error with the useFactory but I'm confused now. Does this mean the deps aren't injected because it can't see them or what?

The Form class:

export class FormBuilderAdvanced {
    http: HttpAdvanced;
    fb: FormBuilder;

    constructor(fb: FormBuilder, http: HttpAdvanced) {
        this.fb = fb;
        this.http = http;
    }

    create(controlNames: string[], submissionUrl: string, getter?: any) {
        return new Form(this.fb, this.http, controlNames, submissionUrl, getter);
    }
}
like image 976
ditoslav Avatar asked Jan 22 '16 09:01

ditoslav


2 Answers

You question doesn't provide enough information to know for sure but this is probably enough

bootstrap(
    App,
    [
        FORM_PROVIDERS,
        ROUTER_PROVIDERS,
        HTTP_PROVIDERS,
        provide(LocationStrategy, { useClass: PathLocationStrategy }),
        provide(RequestOptions, { useClass: DefaultRequestOptions }),
        // provide(MsgService, { useClass: MsgService }),
        MsgService, // is just fine when no special behavior is required
        // provide(HttpAdvanced, { useFactory: (MsgService, HTTP_PROVIDERS) => new HttpAdvanced(MsgService, HTTP_PROVIDERS), deps: [MsgService, HTTP_PROVIDERS] }),
        provide(Http, {useClass: HttpAdvanced});
        AuthService,
        provide(FormBuilder, { useClass: FormBuilderAdvanced}),
        MsgServiceInternal)
    ]
);

If you want to make a class available for injection, just add the type to the providers list (provide(AuthService) and just AuthService do the same). If you want inject a different class than the one requested, use useClass.

Example

@Injectable()
export class MyService {
  constructor(private http: Http) {
  }
}

If your providers contain

provide(Http, {useClass: HttpAdvanced});

then MyService (which has a dependency on Http) gets injected HttpAdvanced instead. Ensure you have this line after HTTP_PROVIDERS to override the default Http provider, contained in HTTP_PROVIDERS.

If DI can't resolve dependencies by itself because they aren't just other providers, then use useFactory.

As mentioned above,

Ensure you have this line after HTTP_PROVIDERS to override the default Http provider, contained in HTTP_PROVIDERS.

the order in the list of providers does matter. When the providers list contains multiple providers for a type then only the last one is used.

The order of the imports doesn't matter.

The order of dependent classes within one file does matter because classes are not hoisted.

Example

export class ClassA {
   // constructor(private b: ClassB) {} // ClassB is unknown here
   constructor(@Inject(forwardRef(() => DataService)) {
   }
}

export class ClassB {
}

If the code of ClassB is above ClassA, forwardRef() isn't necessary.

See also Class is not injectable if it is defined right after a component with meta annotation

like image 69
Günter Zöchbauer Avatar answered Sep 18 '22 10:09

Günter Zöchbauer


To complete a bit the Günter's great and clear answer, I would say that I would use useFactory to instantiate by myself classes associated with providers in the following use cases:

  • When you want to select the element to inject into the contructor of the element associated with the provider. For example, if you want to extend the HTTP class and provide explicitly a specific concrete class for parameters (here XHRBackend instead of ConnectionBackend). Here is a sample:

    bootstrap(AppComponent, [HTTP_PROVIDERS, ROUTER_PROVIDERS,
      new Provider(Http, {
        useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => new CustomHttp(backend, defaultOptions),
        deps: [XHRBackend, RequestOptions]
      })
    ]);
    

@Langley provides interesting hints in this question: Handling 401s globally with Angular.

  • Another cool usage of the useFactory is to instantiate a class from a third-party library into the context of Angular2 (Zones)

    bootstrap(App, [
      provide(Mousetrap, { useFactory: () => new Mousetrap() })
    ]);
    

@alexpods provides this very elegant solution in this question: View is not updated on change in Angular2.

Hope it helps you, Thierry

like image 42
Thierry Templier Avatar answered Sep 21 '22 10:09

Thierry Templier