Angular InjectionToken throws 'No provider for InjectionToken'

Viewed 64073

I'm currently learning the new Angular framework, and I'm trying to make a dynamic search bar which accepts a service name as an argument in order for it to dynamically resolve a service to query the backend service with.

For this I'm using an Injector, and loading the the service during ngOnInit. This works fine when using a string based provider, however my IDE notes that it's deprecated and I should use an InjectionToken which I can't seem to wrap my head around.

I expected the following code to work, as removing all instances of InjectionToken and replacing them with the direct string literal works:

I tried looking at the following documentation, but I didn't quite understand it as I feel like I did exactly what it says, yet it keeps telling me it doesn't work: https://angular.io/guide/dependency-injection-providers

Could someone tell me what I'm doing wrong?
Thanks

Module declaration

// app.module.ts
@NgModule({
  declarations: [
    AppComponent,
    SearchBarComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [
    {
      provide: new InjectionToken<ISearchable>('CustomerService'), // <-- doesn't work;  'CustomerService' <-- works
      useValue: CustomerService
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Search bar component:

// search-bar.component.ts
@Component({
  selector: 'search-bar',
  templateUrl: './search-bar.component.html',
  styleUrls: ['./search-bar.component.sass']
})
export class SearchBarComponent implements OnInit {

  @Input()
  source: string;

  private searcher: ISearchable;

  constructor(private injector: Injector) {}

  ngOnInit() {
    // error: Error: No provider for InjectionToken CustomerService!
    let token = new InjectionToken<ISearchable>(this.source);
    this.searcher = this.injector.get<ISearchable>(token);

    // this works, but it's deprecated and will probably break in the future
    // this.searcher = this.injector.get(this.source);
    console.log(this.searcher);
  }
}

Using the search bar:

<!-- app.component.html -->
<div class="row justify-content-center mb-2">
  <div class="col-8">
    <search-bar title="Customers" source="CustomerService"></search-bar>
  </div>
</div>

Edit: Here's an example with the error:

https://stackblitz.com/edit/angular-3admbe

4 Answers

After asking on the official angular repository, it turns out to be a simple solution. Instead of passing the service name as a string, you'll have pass the tokens through the component into the view into another component.

Globally define the injection token

I did this alongside my service itself to make it easier to keep track of.

@Injectable()
export class CustomerService implements ISearchable { ... }

export const CUSTOMER_SERVICE = new InjectionToken<ISearchable>('CustomerService');

Register the injection token in your app providers

import {CUSTOMER_SERVICE, CustomerService} from "./services/customer/customer.service";


@NgModule({
  declarations: [ ... ],
  imports: [ ... ],
  providers: [
    {
      provide: CUSTOMER_SERVICE,  // That's the token we defined previously
      useClass: CustomerService,  // That's the actual service itself
    }
  ],
  bootstrap: [ ... ],
})
export class AppModule { }

Pass the token through the view to your other component

// In your component
import {CUSTOMER_SERVICE} from "./services/customer/customer.service";


@Component({
  selector: 'app-root',
  template: '<app-search-bar [source]="searcher"></app-search-bar>'
})
export class AppComponent
{
  searcher = CUSTOMER_SERVICE;
}

You can now import the service dynamically from your other component

@Component({
  selector: 'app-search-bar',
  templateUrl: './search-bar.component.html',
  styleUrls: ['./search-bar.component.sass'],
})
export class SearchBarComponent implements OnInit
{
  @Input()
  source: InjectionToken<ISearchable>;

  private searcher: ISearchable;

  constructor(private injector: Injector) {}

  ngOnInit()
  {
    this.searcher = this.injector.get<ISearchable>(this.source);
  }

  search(query: string)
  {
    this.searcher.search(query).subscribe(...);
  }
}

You are mixing it all up.

Tokens are made to be plain objects. By using a service with a dependency injection, you can't declare it as a token.

To create services on demand, you will have to use a factory. A factory is a function that will create instances of your class, with given properties.

In my example, I have added the endpoint as the given propety, but you can do whatever you want in your factory.

Stackblitz : https://stackblitz.com/edit/angular-dg1hut?file=src/app/search-bar/search-bar.component.ts

const factory = (http: HttpClient) => new CustomerService(http, '/api/v1/customer')
// ...
providers: [{
  provide: CustomerService,
  useFactory: factory,
  deps: [HttpClient]
}],
export class SearchBarComponent implements OnInit {
  @Input() source: string;
  constructor(private searcher: CustomerService) { }
  ngOnInit() { console.log(this.searcher); }
  search() { return this.searcher.search(''); }
}

For anyone still struggling with this, it may be as simple as restarting VS Code, as was the case for me. I'm not sure why, but that fixed it for me. Of course, I tried this first, because I was quite certain the error was wrong to begin with.

Something you can do us instead of implementing the service into the AppModule's providers' list, you could just add providedIn to root parameter to service's injectable annotation.

https://angular.io/guide/providers#providing-a-service

Example:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UserService {
}

But, there is one more method, your method. Services can be used into the module's providers' list without requiring any IncpetionToken, because are already incpected, so you can just add it to providers list.

https://angular.io/guide/providers#provider-scope

Example:

@NgModule({
  declarations: [
    AppComponent,
    SearchBarComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [
    CustomerService // <-- You don't need to create any token.
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Related