45

I have components B, C, D that inherit from class A.

I want to use a service in class A, but I don't know how to inject it, without injecting it from its children.

What I tried is a normal injection:

constructor(pageName: string = null, translate: TranslateService) {

But when I do super() to construct class A, it throws an error because I didn't supply a second parameter.

Is there a way to inject into a parent class using Angular 2?

The Angular version I am forced to use is: 2.2.1

Some example case: I have many pages, each can show a loader. Instead of injecting the loader every time, and manage the loader from every page, I want to do:

export class BaseComponent {
    constructor(private loader: LoadingService) {}
    
    showLoading() {
        this.loader.show();
    }
}

And then to inherit from the component itself:

@Component({
    selector: "page-login",
    providers: [UsersService],
    templateUrl: "login.html"
})
export class LoginPage extends BaseComponent {
    constructor(private usersService: UsersService) {
        super();
    }
}

Now LoginPage has a method showLoading from it's parent.

Dale K
  • 25,246
  • 15
  • 42
  • 71
Amit
  • 5,924
  • 7
  • 46
  • 94
  • This might work for you: http://stackoverflow.com/questions/37117698/angular2-use-imported-libs-from-base-class/37117732#37117732. – Michael Kang Feb 25 '17 at 22:44
  • @pixelbits Thanks, just found another workaround. I inject that TranslateService to the app, then I save that instance in a global variable which is a part of my data store (I know, not supposed to do like that) and it works very well. – Amit Feb 25 '17 at 22:48
  • I wouldn't call this a good pattern. And your current example doesn't reflect the question. There's no TranslateService. Instead there's showLoading method, I see no reason why it should be there at all. – Estus Flask Feb 25 '17 at 22:52
  • @estus Apologies, the example was to make it as short as possible. Why shouldn't the "showLoading" method be there? – Amit Feb 25 '17 at 22:53
  • I see no good use for this thing `showLoading() { this.loader.show(); }`, there may be better ways to do that. Since the question is XY problem (you're trying to do a thing which doesn't play well with the framework itself), explaining the case thoroughly helps to get the best solution. The accepted answer is a known workaround - and a hack. Its usage usually indicates that idiomatic solution wasn't found. There may or may not be negative effects of it (global injector can hurt testability, for instance). – Estus Flask Feb 25 '17 at 23:06
  • Well, u are correct that it is a hack. However, I very much believe in high encapsulation, for ease of change. My problems were as followed: show loader that can be swapped with ease, and to manage it across multiple load event, which contradicted one another before. Also, I needed to translate the same sentence key over and over, so couldn't change that in the future, and finally, I wanted to make an easily changeable alert module, as I am working on 3 platforms with the same code. I really don't think that reusing the same code over and over benefits u in any way – Amit Feb 25 '17 at 23:12

4 Answers4

99

You could solve this by using a service locator service. That will easily allow you to get any service and use it in your parent classes without having to inject them via their children (as this can be a pain).

So to use this, create a simple class locator.service.ts:

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

export class ServiceLocator {
    static injector: Injector;
}

Import this service in your app.module.ts:

import {ServiceLocator} from './locator.service';

Then in the constructor of your module file (app.module.ts?), init this thing so you can use it anywhere:

export class AppModule {
    constructor(private injector: Injector){    // Create global Service Injector.
        ServiceLocator.injector = this.injector;
    }
}

Now, to use it in your super class (your BaseComponent), simply import the ServiceLocator

import {ServiceLocator} from './locator.service';

and use it like:

export class BaseComponent {
    public loader;
    constructor() {
        this.loader = ServiceLocator.injector.get(LoadingService)
    }

    showLoading() {
        this.loader.show();
    }
}

Now you have injected your service in an extendable parent and it's usable in your child components without having to pass it in the super().

aliqandil
  • 1,673
  • 18
  • 28
MikeOne
  • 4,789
  • 2
  • 26
  • 23
  • 1
    Absolutely amazing! But I'm wondering, are there negative side effects to this approach? Like performance? Does it affect the singleton nature of services? – Abeer Sul Nov 15 '18 at 21:22
  • 2
    There shoudn’t be any. It’s using the actual Angular injector that internally is also used to do dependency injection. – MikeOne Nov 15 '18 at 21:39
  • This works for sure, and there's not argument and I'm using it, but isn't this an anti-pattern for dependency injection? – mtpultz Feb 25 '19 at 01:34
  • 2
    This is a recognised anti pattern. But this solution is worthy particularly as having a significant number of base constructor parameters can be annoying with derived classes and super in the typescript world. Especially for UI components where it is not advantageous to have services take UI dependencies. – davidcarr Apr 12 '19 at 12:49
  • I agree that if there is a chance of noticeably reducing boilerplate code for your team developers (importing "infrastructure" services that are needed only by some base class) with the costs of getting a bit dirty inside your infrastructure and glue code, then it is a forgivable compromise. – JustAMartin Jul 03 '19 at 15:30
  • 1
    Great solution! Thank you, I was struggling a lot with this. Please note that in order for this to work, the service has to be provided in the module where the injector is grabbed. So if you have several different modules, for instance shared modules, you need to create different ServiceLocator classes for every module and name the classes accordingly, ie ServiceLocatorShared, ServiceLocatorApp, etc.. – Fredrik_Borgstrom Sep 14 '19 at 17:32
  • Great answer, but how do you handle the spec file? I can't find a way to test the component using the `ServiceLocator`. – Marin Takanov Nov 30 '22 at 08:51
  • 2
    This answer has become a bit stale from Angular 14 upwards. Angular now has the inject function which is a much better solution. – MikeOne Nov 30 '22 at 18:27
7

For Angular 14 and above there is inject() function:

import {inject} from '@angular/core'

export class BaseComponent {
  public loader;
  
  constructor() {
    this.loader = inject(LoadingService)
  }

  showLoading() {
    this.loader.show();
  }
}
export class LoginPage extends BaseComponent {
  constructor(private usersService: UsersService) {
    super();
  }
}

This way LoadingService will be available in your both (parent & child) components and UsersService only in your child component.

If you want LoadingService to be available only in the BaseComponent, just set private loader.

Marin Takanov
  • 1,079
  • 3
  • 19
  • 36
2

Take a look at this code.

@Injectable({ providedIn: "root" })
export class MyService {
   constructor(...) {
      MyService.ms = this;
   }
   static ms: MyService;

This works because Angular creates singleton instances. The only requirement is that a component somewhere prior to using the variable ms, must have used MyService in the CTOR.

Like this:

@Component
export class MyComponent {
//this will set the static value
constructor(private MyService) { }
...
JWP
  • 6,672
  • 3
  • 50
  • 74
1

In my case in Angular 10 running 'ng serve --aot', I had to use:

export class BaseComponent {

    public loader;

    constructor() 
    {
        setTimeout(() => {
            this.loader = ServiceLocator.injector.get(LoadingService);
        }, 0);
    }

    showLoading() {
        this.loader.show();
    }
}

This because it seems that now the code in the service constructor is executed first and then the constructor in the module which assigns the Injector instance to the ServiceLocator service, leading to the ServiceLocator.injector to be undefined if the setTimeout() hack is not used. Any ideas on how to improve this?

Will de la Vega
  • 536
  • 1
  • 5
  • 17
  • 2
    You could try moving the line to an ngOnInit because that's executed right after the constructor – Tim Oct 27 '20 at 22:25