dependency-injectionlazy-loadingangular12

Angular 12. Inject service via forRoot into an external library, loaded from a module which has been lazy loaded by Compiler


I've created a library with a directive that injects a service. This library is loaded with a forRoot method in each lazy loaded component where is going to be used.

*** library.module ***

export const SERVICE_INYECTION_TOKEN: InjectionToken<any> = new InjectionToken('service')

export interface IDirectiveModuleConfig {
  serviceAdapterConfiguration?: {provider: Provider, moduleName: string};
}

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [DirectiveDirective],
  exports: [DirectiveDirective]

})
export class LibraryModule { 

  public static forRoot(config: IDirectiveModuleConfig = {}): ModuleWithProviders<LibraryModule> {
    console.log("Library loaded in module " + config.serviceAdapterConfiguration.moduleName)
    return {
        ngModule: LibraryModule,
        providers: [
            config.serviceAdapterConfiguration.provider
        ]
    };
}
}

*** directive.directive ***

@Directive({
  selector: '[directive]',
})
export class DirectiveDirective implements AfterViewInit {
  @Input() methodName: string;

  constructor(
    private element: ElementRef,
    private renderer: Renderer2,
    @Inject(SERVICE_INYECTION_TOKEN) private service: any
  ) {}
    
    ngAfterViewInit(): void {
    this.element.nativeElement.innerText += this.service[this.methodName]()

    this.renderer.setValue(this.element.nativeElement, this.service[this.methodName]())
  }
}

In my main project, I have two lazy-loadeds modules, and each one have a component. One of this modules and its component are lazylodaded by the RouterModules. It works OK

*** app-routing.module ***

const routes: Routes = [

  {
    path: 'a',
    loadChildren: () =>
      import('./modules/module-a/module-a.module').then((m) => m.ModuleAModule),

  },
  {
    path: 'b',
    loadChildren: () =>
      import('./modules/module-b/module-b.module').then((m) => m.ModuleBModule),
  },
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})

export class AppRoutingModule {}

The other one is created by compileModuleAndAllComponentsAsync() and viewContainerRef.createComponent() in the parent component. It works ok without the service inection, but when I inject the service I get a NullInjectorError.

*** app.component ***

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  @ViewChild("viewContainerRef", { read: ViewContainerRef }) viewContainerRef: ViewContainerRef

  component = null;
  title = 'component-overview';

  constructor(private compiler: Compiler, private injector: Injector) {}

 

  async createModuleAndComponetC() {

    const componentInjector: Injector = Injector.create({providers:[{provide:'service', useExisting: ServiceCService}]})

    this.viewContainerRef.clear()
    const module = (await import('./modules/module-c/module-c.module'))
      .ModuleCModule;

    this.compiler.compileModuleAndAllComponentsAsync(module).then((factory) => {
      factory.ngModuleFactory.create(this.injector);
      const componentFactory = factory.componentFactories[0]
      const component: ComponentRef<any> = this.viewContainerRef.createComponent(componentFactory);

    });

  }
}

MODULE A (lazy loaded by routerModule working OK) with its component and service

const serviceConfig: IDirectiveModuleConfig = {
  serviceAdapterConfiguration: {
    provider: { provide: SERVICE_INYECTION_TOKEN, useClass: ServiceAService },
    moduleName: 'A',
  }
};

@NgModule({
  imports: [
    LibraryModule.forRoot(serviceConfig),
    CommonModule,
    ModuleARoutingModuleModule,
  ],
  declarations: [ComponentAComponent],
  exports: [ComponentAComponent],
})
export class ModuleAModule {
  constructor(){
    console.log("moduleA loaded")
  }

}

@Component({
  selector: 'app-component-a',
  templateUrl: './component-a.component.html',
  styleUrls: ['./component-a.component.css'],
})
export class ComponentAComponent implements OnInit {
  constructor() {}

  ngOnInit() {}
}

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

  constructor() { }

  serviceA(){
    return(" service A!")
  }

}

MODULE C (loaded manually with compileModuleAndAllComponentsAsync() and viewContainerRef.createComponent()

export const serviceConfig: IDirectiveModuleConfig = {
  serviceAdapterConfiguration: {
    provider: { provide: SERVICE_INYECTION_TOKEN, useClass: ServiceCService },
    moduleName: 'C',
  },
};

@NgModule({
  imports: [CommonModule, LibraryModule.forRoot(serviceConfig)],
  declarations: [ComponentCComponent],
})
export class ModuleCModule {
  constructor() {
    console.log('moduleC loaded');
  }

  static 
}

@Component({
  selector: 'app-component-c',
  templateUrl: './component-c.component.html',
  styleUrls: ['./component-c.component.css'],
  providers: [ServiceCService],
})
export class ComponentCComponent implements OnInit {
  constructor() {
    console.log('component C constructor');
  }

  ngOnInit() {
    console.log('component C OnInit');
  }
}

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

  serviceC() {
    return ' service C!';
  }
}

In this example Modules A and B are used with router outlet, and module C is loaded with Compiler and the component is used in a *ngCompilerOutlet

I think that the problem is in the way I load my ComponentC... but I'm a little bit lost...

In adition... i've founded that the module C create a new instance each time I load this, and is not working like singleton...

stackblitz with the test project


Solution

  • Finally, I got success!

    I saw that I could pass an injector to the viewContainerRef. CreateComponent () method. I tried with the same injector I had used to create the module in the noModuleFactory. Create () method, but it was still wrong.

    Finally y realized that NgModule class exports an injector, I suposed this injector provide al the providers in this module and it works ok!!

    Now my createModuleAndComponetC() is:

      async createModuleAndComponetC() {
    
    
        this.viewContainerRef.clear();
        const module = (await import('./modules/module-c/module-c.module'))
          .ModuleCModule;
    
        this.compiler.compileModuleAndAllComponentsAsync(module).then((factory) => {
          const module = factory.ngModuleFactory.create(this.injector);
    
          const componentFactory = factory.componentFactories[0];
          const component: ComponentRef<any> =
            this.viewContainerRef.createComponent(
              componentFactory,
              0,
              module.injector
            );
        });
      }
    

    here is the corrected stackbliz