Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular condition in type provider with AOT

I have an Angular project which I compile with AOT. I want to be able to register ClassProvider that is resolved dynamically according to configuration. Simplified code I use is this:

const isMock = Math.random() > 0.5;

@NgModule({
  // ...
  providers: [
    { provide: MyServiceBase, useClass: (isMock) ? MyServiceMock : MyService },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The problem is when I compile this with AOT I always get the same service. I would expect to get different service while hitting F5 (because of the randomness on the first line). When compiling without AOT it behaves as I expect.

Here is the whole code example on github: https://github.com/vdolek/angular-test/tree/aot-conditioned-provider-problem. It behaves differently with ng serve and ng serve --aot.

How can I achieve this? I know I could use FactoryProvider, but then I would have to duplicate the services dependencies (parameters of the factory function and deps property on the FactoryProvider).

like image 504
Martin Volek Avatar asked Feb 21 '18 14:02

Martin Volek


2 Answers

To achieve the dynamic nature of your requirement, you need to use factory providers, via the useFactory attribute.

I've forked your repository, and amended your app.module.ts as follows, to work in AOT.

Amend app.module.ts as follows

export let myServiceFactory = () => {

  const isMock = Math.random() > 0.5;

  return isMock ? new MyServiceMock() : new MyService();
}; 

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {provide: MyServiceBase, useFactory: myServiceFactory},
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

In the case that your service is dependent on other services, which, most likely it will, you can use the deps argument, to pass on the required dependencies.

Let's say that MyServiceBase is dependent on two services, MyService1 and MyService2... Your factory function will look as follows :

export let myServiceFactory = (service1:MyService1, service2:MyService2) => {

  const isMock = Math.random() > 0.5;

  return isMock ? new MyServiceMock(service1, service2) : new MyService(service1, service2);
}; 

and your providers decleration would look as follows

providers: [
    {
       provide: MyServiceBase, 
       useFactory: myServiceFactory,
       deps: [MyService1, MyService2]
    },
]

This guide contains further detail on the various ways of achieving dependency injection in Angular.

like image 93
JeanPaul A. Avatar answered Sep 27 '22 21:09

JeanPaul A.


I think as @jeanpaul-a said you don't have any choice other than to use factory. But managing dependencies could be not very clean. But what you could use is the Injector. I'll go with something like:

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, HelloComponent ],
  providers: [
    Dep1Service,
    Dep2Service,
    { provide: MyServiceBase, useFactory: createService, deps: [Injector] }
  ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

export function createService(injector: Injector) {
  const isMock = Math.random() > 0.5;
  if (mock) {
    return new MyService1(injector.get(Dep2Service));
  } else {
    return new MyService2(injector.get(Dep1Service));
  }
}

What you could also do is to set MyServiceBase as an interface and use InjectionToken. You will find a working example here (not your class name however).

like image 35
JEY Avatar answered Sep 27 '22 20:09

JEY