4

I'm using angular cli and angular 4. I've made some classes from which my components inherit behavivour! However during development their constructores grow and in the current phase I need to be able to allow other to use them whithout the need of knowing their constructors!

So after seeing Petr sugestion in dependency injection I tried, but had to make some adjustments due to errors the compiler was giving, I suppose due to diferences betwween angular 4 and angular 5!

This is my code:

Service Locator:

import {ReflectiveInjector} from "@angular/core";

export class ServiceLocator {
  static injector: ReflectiveInjector;
}

module which imports the serviceLocator:

ServiceLocator.injector = ReflectiveInjector.resolveAndCreate(
      Object.keys(services).map(key => ({
        provide: services[key].provide,
        useClass: services[key].provide,
        deps: services[key].deps
      }))
    );

serviceList:

import {SysFwDatesService} from '../sysDates/sysFwDates.service';
import {MatSnackBar} from '@angular/material';
import {SysFwFormSpecBuilder} from '../uiValidation/SysFwFormSpecBuilder';
import {SysFwHttpApi} from '../http/SysFwHttpApi';


export const services: {[key: string]: {provide: any, deps: any[], useClass?: any}} = {
 'SysFwDatesService': {
    provide: SysFwDatesService,
    deps: []
  },
  'MatSnackBar': {
    provide: MatSnackBar,
    deps: []
  },
  'SysFwFormSpecBuilder': {
    provide: SysFwFormSpecBuilder,
    deps: []
  },
  'SysFwHttpApi': {
    provide: SysFwHttpApi,
    deps: []
  }
}

It seems to be working, however it seems to have lost the other providers and to be expecting all providers to be passed this way!

This is the error I'm getting:

No provider for HttpClient! (SysFwDatesService -> SysFwHttpApi -> HttpClient)

Do I need to put everything in the servicesList? What am I doing wrong?

Before I use the injector it all worked fine!

Thanks for your help!

programtreasures
  • 4,250
  • 1
  • 10
  • 29
davidmr
  • 125
  • 1
  • 11

2 Answers2

1

HttpClient is defined in HttpClientModule and has a hierarchy of dependencies that is not very easy to list by hand as single providers array. ReflectiveInjector and Injector don't support Angular modules, and it's impractical to parse a module to get a hierarchy providers manually - this is what Angular already does internally.

The module that initializes service locator should import dependency modules, too, and resulting injector should inherit from module injector:

@NgModule({ imports: [HttpClientModule], ... })
export class AppModule {
  constructor(injector: Injector) {
    ServiceLocator.injector = ReflectiveInjector.resolveAndCreate(
      Object.keys(services).map(key => ({
        provide: services[key].provide,
        useClass: services[key].provide,
        deps: services[key].deps
      })),
      injector
    );
  }
}

By the way, services object keys aren't used for anything and are redundant. services likely should be an array.

I would strongly advise against using this home-grown service locator in real-world application. It is a proof of concept that does what it does, but also a hack that isn't idiomatic to the framework and may have a lot of negative consequences that may not be obvious at this moment, e.g. lazy loaded modules.

The recommendation is to use the means offered by the framework wherever possible, this way the application has most chances to be trouble-free in future.

It is:

@Injectable() // needed for JIT compilation
abstract class Injective {
  constructor(protected injector: Injector) {}
}

@Injectable() // potentially needed for AOT compilation
class FooService extends Injective {
  baz = this.injector.get(Baz);
}

@Component(...)
class BarComponent extends Injective {
  foo = this.injector.get(FooService);

  // just passes injector to parent constructor if there's a need for own constructor
  constructor(injector: Injector, public baz: Baz) {
    super(injector);
    // ...
  }
}

Notice that to be on safe side, @Injectable() is needed on both parent and child classes (this may vary between Angular and Angular CLI versions).

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Thanks! I'm going to try this! However I need some clarifications... What's the purpose of the abstract class Injective? From what I understood my base class should inherit from it! Is that it? My child classes are components should I make @Injectable() @component(...) class child extends parent(){}? Is that it? – davidmr Feb 20 '18 at 11:19
  • Injective is supposed to be base class (that's why it's abstract). If you have another base class in mind that should contain something else, then yes, Base should inherit from Injective. `@Component` and `@Directive` are already injectable, so you need `@Injectable()` only on service and base classes. – Estus Flask Feb 20 '18 at 11:29
  • Passing the injector in the resolveAndCreate call did the trick! Thank you very much! – davidmr Feb 20 '18 at 13:32
  • You're welcome. But again, if you didn't do that before, I wouldn't suggest to do that for anything but experimenting. ServiceLocator recipe is faulty by design and will go badly sooner or later. Not so convenient if there's already a hundred classes that rely on it. – Estus Flask Feb 20 '18 at 13:41
  • But... Is there any other way to avoid my coworkers from having to know the constructors of the classes they are extending? I need to simplify our future work and wont/need to make the classes in a way they only need to put in their constructor their specific services and inherit the others. Also it would solve the problem of having to refactor all the subclasses if another service is added to the parent class! – davidmr Feb 20 '18 at 16:36
  • It only requires `injector` to be injected to constructor. It looks like a fair trade-off to me. And I'm quite sure it's the only future-proof way to reach the injector in cases like this one. The downside of injector.get is that it isn't implicitly typed, while constructors are. The refactoring you're describing doesn't happen too often and isn't really costly (you will get type errors, so bugs won't be left unnoticed). I'd personally go with constructors, they are much less exotic, also naturally typed. – Estus Flask Feb 20 '18 at 19:11
  • Another concern is that injector.get() isn't fully functional.Some things like `@Self` or `@Attribute` aren't available and still need constructor for them. – Estus Flask Feb 21 '18 at 01:57
0

have you tried explicitly specifying HttpClient as a dependency if SysFwHttpApi ? Like

'SysFwHttpApi': { provide: SysFwHttpApi, deps: [HttpClient] }

erosb
  • 2,943
  • 15
  • 22
  • Yes! It gives the same error! I also tried to add it to the list 'HttpClient': { provide: HttpClient, deps: [] } This solves, but gives error in another service and so on! – davidmr Feb 19 '18 at 17:47
  • Did you import the `HttpClientModule` into your AppModule? – erosb Feb 19 '18 at 18:07
  • No! I did it in the SysFwHttpApiModule – davidmr Feb 19 '18 at 18:37
  • Already tried to import it in the baseClasses module. Didn't work. – davidmr Feb 19 '18 at 18:51
  • It should be imported into the module in which you declare the service with the ^above declaration. – erosb Feb 19 '18 at 18:54
  • @erosb This won't work because this injector doesn't inherit from module injector. This is one of the drawbacks of the recipe used in OP. – Estus Flask Feb 19 '18 at 20:20
  • @erosb That's why I imported it in the SysHttpApi! It's in the SysHttpApi that the service in question is used! Do I need to imported it in the child classes as well? Should I import it also in the module of the injector? – davidmr Feb 20 '18 at 10:16
  • Dependencies 'deps' didn't work for me either. I'm not clear what it is supposed to do. – Joe Feb 25 '18 at 15:58