Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular InjectionToken throws 'No provider for InjectionToken'

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

like image 475
Paradoxis Avatar asked Nov 15 '18 10:11

Paradoxis


People also ask

What is Nullinjector () in angular?

Angular Creates the Module Injector tree when the Application starts. At the top of the Module Injector tree, Angular creates an instance of Null Injector . The Null Injector always throws an error unless we decorate the dependency with the Optional decorator.

When should I use InjectionToken?

Use an InjectionToken whenever the type you are injecting is not reified (does not have a runtime representation) such as when injecting an interface, callable type, array or parameterized type. InjectionToken is parameterized on T which is the type of object which will be returned by the Injector .

What is provider in angular?

A provider is an instruction to the Dependency Injection system on how to obtain a value for a dependency. Most of the time, these dependencies are services that you create and provide. For the final sample application using the provider that this page describes, see the live example / download example .

What is injectable ({ providedIn root })?

The service itself is a class that the CLI generated and that's decorated with @Injectable. By default, this decorator is configured with a providedIn property, which creates a provider for the service. In this case, providedIn: 'root' specifies that the service should be provided in the root injector.


2 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(...);
  }
}
like image 16
Paradoxis Avatar answered Oct 19 '22 14:10

Paradoxis


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.

like image 3
DontThinkJustGo Avatar answered Oct 19 '22 13:10

DontThinkJustGo