4

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?

netdjw
  • 5,419
  • 21
  • 88
  • 162
  • This looks overly complicated, I believe you can simply use the `environment ` variable imported without having to inject it. Its is also easier to add the `providedIn` property to the `@Injectable` decorator rather than adding the providers array. If the environment variable is from a remote source then simply saving it as an observable should be sufficient – Owen Kelvin Oct 16 '20 at 16:25
  • The "use the environment variable imported without having to inject it" part I think it's not working in a library. Or it is? The lib is installable from npm. – netdjw Oct 16 '20 at 18:27
  • What does `model: T` constructor argument in your library `RemoteDataService` do? – Akash Oct 20 '20 at 17:30
  • @Akash in this example it's do nothing, good point. But still I think it's an important detail because some suggested solutions said fetch environment values as a constructor argument, and I don't want to do that. – netdjw Oct 20 '20 at 19:50
  • I guess the problem is with your constructor argument. Angular DI needs to know the instances you want to inject in your service's constructor. – Akash Oct 21 '20 at 02:18

3 Answers3

5

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");

}

Akash
  • 4,412
  • 4
  • 30
  • 48
2

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.

netdjw
  • 5,419
  • 21
  • 88
  • 162
1

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

Andrei
  • 10,117
  • 13
  • 21
  • The `model` variable is used in the `RemoteDataSerivce`'s functions. It can not be deleted from constructor attributes. And in the other hand the RemoteDataService is used in other services, what used in other services and so on... so I don't want to add another constructor attribute, because it involves the upgrade of the entire applications what uses this library. – netdjw Oct 21 '20 at 09:02
  • the service instance should be created by angular dependency injection, not by the custom code – Andrei Oct 21 '20 at 16:16