6

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);
    }
}
Luca Ritossa
  • 1,118
  • 11
  • 22
ditoslav
  • 4,563
  • 10
  • 47
  • 79
  • Perhaps you missed the `HeaderBar` dependency when defining the `AuthService` provider? – Thierry Templier Jan 22 '16 at 09:55
  • To **4**. When you want DI inject a class that has constructor parameters you need to add the `Injectable` annotation: `@Injectable() export class FormBuilderAdvanced {`. In your case there is another problem. DI can't resolve `controlNames: string[], submissionUrl: string, getter?: any`. There are no providers for `string[]` or `any`. What do you want to get passed to the constructor here? Maybe this is a case for `useValue` or `useFactory`. – Günter Zöchbauer Jan 22 '16 at 11:39
  • Aw man I put it everywhere but here. Can't believe it was this stupid – ditoslav Jan 22 '16 at 11:41
  • DI is quite confusing when you start using it. I had this 2 years ago when they introduced it in Angular.dart. We have covered the better part in this question I guess. One part not mentioned here is hierarchy. When to add providers to `bootstrap()` and when to a `@Component()` / `@Directive()` annotation. If you want to elaborate this as well check out for example the answer to this question http://stackoverflow.com/questions/34804298/whats-the-best-way-to-inject-one-service-into-another-in-angular-2-beta/34804459#34804459 or create a new one question. – Günter Zöchbauer Jan 22 '16 at 11:55

2 Answers2

6

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

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • I apologise for not enough info, I did not wan't to pollute with redundant code. The thing is that I'm not providing HttpAdvanced instead of usual Http, it's that I'm providing Http to HttpAdvanced like class HttpAdvanced { constructor(http: Http) { this.http = http; } } and I'm also providing HttpAdvnced to other components but not as Http but by itself. I'll add some code – ditoslav Jan 22 '16 at 10:43
  • No problem, I understand that this is an iterative process. In this case just replace `provide(Http, {useClass: HttpAdvanced});` by `HttpAdvanced`. – Günter Zöchbauer Jan 22 '16 at 11:05
  • When do I write `provide(HttpAdvanced, { useClass: HttpAdvanced })` and when do I just write `HttpAdvanced` like with MsgService in your example – ditoslav Jan 22 '16 at 11:20
  • They do both the same. `provide(X, { useClass: Y})` is only necessary if `X` and `Y` are not the same type and when you want a class requesting `X` get `Y` instead. This is for example for testing. Your service requests `Http` but for tests you do `provide(Http, { useClass: MockHttp })` then your service gets `MockHttp` instead of the requested `Http`? – Günter Zöchbauer Jan 22 '16 at 11:23
  • Added 4th question :/ – ditoslav Jan 22 '16 at 11:37
4

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

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360