"Can't resolve all parameters for service: (?)" when I try to use service from library in Angular 10

14.3k views Asked by At

In my library I have a service with this code:

import { Inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DataInjectorModule } from '../../data-injector.module';

// @dynamic
@Injectable()
export class RemoteDataService<T> {
  @Inject('env') private environment: any = {};
  public type: new () => T;
  
  constructor(
    model: T,
  ) {
    this.http = DataInjectorModule.InjectorInstance.get(HttpClient);
  }

  // ...
}

The data-injector.module (existing reason is only avoid circular dependency):

import { NgModule, Injector } from '@angular/core';

// @dynamic
@NgModule({
  declarations: [],
  imports: [],
  providers: [],
  exports: [],
})
export class DataInjectorModule {
  static InjectorInstance: Injector;

  constructor(injector: Injector) {
    DataInjectorModule.InjectorInstance = injector;
  }

}

In my library's main module file:

import { ModuleWithProviders } from '@angular/compiler/src/core';
import { NgModule, Injector } from '@angular/core';
import { DataInjectorModule } from './data-injector.module';
import { RemoteDataService } from './services/remote-data/remote-data.service';

// @dynamic
@NgModule({
  declarations: [],
  imports: [
    DataInjectorModule,
  ],
  providers: [],
  exports: [],
})
export class DataCoreModule {
  static InjectorInstance: Injector;

  constructor(injector: Injector) {
    DataCoreModule.InjectorInstance = injector;
  }

  public static forRoot(environment: any): ModuleWithProviders {
    return {
      ngModule: DataCoreModule,
      providers: [
        RemoteDataService,
        { provide: 'env', useValue: environment }
      ]
    };
  }
}

Finally in my application's app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { DataCoreModule } from 'data-core';

import { AppRoutingModule } from './app-routing.module';
import { environment } from 'src/environments/environment';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    DdataCoreModule.forRoot(environment),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Build goes well, but in the browser I get this error:

Error: Can't resolve all parameters for RemoteDataService: (?).
    at getUndecoratedInjectableFactory (core.js:11338)
    at injectableDefOrInjectorDefFactory (core.js:11328)
    at providerToFactory (core.js:11371)
    at providerToRecord (core.js:11358)
    at R3Injector.processProvider (core.js:11256)
    at core.js:11230
    at core.js:1146
    at Array.forEach (<anonymous>)
    at deepForEach (core.js:1146)
    at R3Injector.processInjectorType (core.js:11230)

I checked several questions about this topic in StackOverflow, like this but almost everywhere just the @Injectable() was the missing part, but in this case I use this decorator.

Any idea how can I solve this issue?

3

There are 3 answers

0
netdjw On BEST ANSWER

I found a solution (actually a workaround). I think this isn't an elegant way, but it's wokring.

I created an EnvService class what can pick up the environment parameter from the module, and doesn't have sideeffects with constructor attributes:

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

@Injectable({
  providedIn: 'root',
})
export class EnvService {
  public environment: any = {};

  constructor(@Inject('env') private env?: any) {
    this.environment = env ?? {};
  }
}

Then in my library's main module file I set up the EnvService instead of RemoteDataService:

import { ModuleWithProviders } from '@angular/compiler/src/core';
import { NgModule, Injector } from '@angular/core';
import { DataInjectorModule } from './data-injector.module';
import { EnvService } from './services/env/env.service';

// @dynamic
@NgModule({
  declarations: [],
  imports: [
    DataInjectorModule,
  ],
  providers: [],
  exports: [],
})
export class DataCoreModule {
  static InjectorInstance: Injector;

  constructor(injector: Injector) {
    DataCoreModule.InjectorInstance = injector;
  }

  public static forRoot(environment: any): ModuleWithProviders {
    return {
      ngModule: DataCoreModule,
      providers: [
        EnvService,
        { provide: 'env', useValue: environment }
      ]
    };
  }
}

Finally in my RemoteDataService changed the @Inject solution to an InjectorInstance.get(EnvService) solution:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DataInjectorModule } from '../../data-injector.module';
import { EnvService } from '../env/env.service';

// @dynamic
@Injectable()
export class RemoteDataService<T> {
  private env = DdataInjectorModule.InjectorInstance.get(EnvService);
  public type: new () => T;
  
  constructor(
    model: T,
  ) {
    this.http = DataInjectorModule.InjectorInstance.get(HttpClient);
  }

  // ...
}

So the RemoteDataService's constructor attributes are untouched, but the service can access to the environment variables over the EnvService.

2
Andrei On

Angular DI is done in constructor in most cases. As you've written constructor like this

constructor(
    model: T,
  )

Angular thinks that you are trying to inject T. you should be injecting all the things you want in the constructor

constructor( @Inject('env') private environment) {}

but make sure that env is provided correctly. and even better use InjectionTokens for that reason

0
Akash On

Error: Can't resolve all parameters for RemoteDataService: (?)

The error says it all. Angular can't resolve all the constructor parameters in RemoteDataService. When this service is instantiated, it expects the required parameters.

You can provide the required dependency via InjectionToken, please see this answer for details.

But your service uses generics and you did not mention how you're using this service in your components, so I'd suggest you declare the providers in your components (or module will work too) and use @Inject() to inject different versions of this service in your component like show below (checkout this StackBlitz and see the console for a log from service constructor)-

import { Component, Inject } from "@angular/core";
import { RemoteDataService } from "./custom/remote-data.service";

export class A {}

export class B {}

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
  providers: [
    { provide: "env", useValue: {} },
    {
      provide: "ARemoteDataService",
      useFactory: () => new RemoteDataService<A>(new A())
    },
    {
      provide: "BRemoteDataService",
      useFactory: () => new RemoteDataService<B>(new B())
    }
  ]
})
export class AppComponent {
  constructor(
    @Inject("ARemoteDataService")
    private aRemoteDataService: RemoteDataService<A>,
    @Inject("BRemoteDataService")
    private bRemoteDataService: RemoteDataService<B>
  ) {}
}

Also, not sure if you can use @Inject() outside constructor. But you can always use the injector to get your other dependencies (env in your case) -

// @Inject('env') private environment: any = {};
constructor(model: T) {
this.environment = DataInjectorModule.InjectorInstance.get("env");

}