5

I have a dashboard app where I was lazy loading widgets (not tied to a route).

I was doing this by defining an object {name: string, loadChildren: string}. Then in my app.module I would do provideRoutes(...).

This would cause the cli to create a chunk for each widget module.

Then at runtime I would use the SystemJsModuleLoader to load that string and get an NgModuleRef.

Using that I could create the component from the module and call createComponent on the ViewContainerRef.

Here is that function:

 loadWidget(
    name: string,
    container: ViewContainerRef,
    widget: Widget
  ): Promise<{ instance: WidgetComponent; personlize?: { comp: any; factory: ComponentFactoryResolver } }> {
    if (this.lazyWidgets.hasOwnProperty(name)) {
      return this.loader.load(this.lazyWidgets[name]).then((moduleFactory: NgModuleFactory<any>) => {
        const entryComponent = (<any>moduleFactory.moduleType).entry;
        const moduleRef = moduleFactory.create(this.injector);
        const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);

        const comp = container.createComponent(compFactory);
        (<WidgetComponent>comp.instance).widget = widget;
        const personalize = (<any>moduleFactory.moduleType).personalize;
        if (personalize) {
          return {
            instance: <WidgetComponent>comp.instance,
            personlize: {
              comp: personalize,
              factory: moduleRef.componentFactoryResolver,
              injector: moduleRef.injector
            }
          };
        } else {
          return {
            instance: <WidgetComponent>comp.instance
          };
        }
      });
    } else {
      return new Promise(resolve => {
        resolve();
      });
    }

In angular 8 the loadChildren changes to the import function.

Instead of an NgModuleRef you get the actual module instance.

I thought I could fix my code by taking that module, compiling it to get the NgModuleRef then keeping the rest of the code the same.

It seems that in AOT mode though the compiler does not get bundled.

So I am basically stuck now with an instance of the component I need but no way to add it to the View container.

It requires a component factory resolver which I can't get.

I guess my question is how to take an instance of a component and add it to view container in angular 8. For now I have reverted to using the string version of loadChildren but that will only work until version 9 comes out.

Here is the version with the compiler that does not work in AOT

 if (this.lazyWidgets.hasOwnProperty(name)) {
      return this.lazyWidgets[name]().then((mod: any) => {
        const moduleFactory = this.compiler.compileModuleSync(mod);
        const entryComponent = (<any>moduleFactory.moduleType).entry;
        const moduleRef = moduleFactory.create(this.injector);
        const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);

        const comp = container.createComponent(compFactory);
        (<WidgetComponent>comp.instance).widget = widget;
        const personalize = (<any>moduleFactory.moduleType).personalize;
        if (personalize) {
          return {
            instance: <WidgetComponent>comp.instance,
            personlize: {
              comp: personalize,
              factory: moduleRef.componentFactoryResolver,
              injector: moduleRef.injector
            }
          };
        }

And here is an example of how I was thinking to do it but then having no way to add it to the ViewContainerRef.

The module instance implements an interface that requires an 'entry' property.

This defines the actual component to load:

  if (this.lazyWidgets.hasOwnProperty(name)) {
      return this.lazyWidgets[name]().then((mod: any) => {

        const comp = mod.entry;
        (<WidgetComponent>comp.instance).widget = widget;
        const personalize = mod.personalize;
        if (personalize) {
          return {
            instance: <WidgetComponent>comp.instance,
            personlize: {
              comp: personalize,
               factory: null //this no longer works:  moduleRef.componentFactoryResolver,
              // injector: moduleRef.injector
            }
          };
        } else {
          return {
            instance: <WidgetComponent>comp.instance
          };
        }
      });
    } 

EDIT:

I tried to add an example in stackblitz but the compiler is turning my string to functions. At least the code is more readable. this is what I was doing in angular 8. I basically need a way to do this with import() instead of magic string.

https://stackblitz.com/edit/angular-9yaj4l

Azer
  • 1,200
  • 13
  • 23
Jason
  • 1,316
  • 13
  • 25
  • Do you get a runtime error or compile error when building/running with `--aot`? – John Jun 20 '19 at 12:22
  • When I use the Compiler it is a run time error. no build time errors. – Jason Jun 20 '19 at 12:48
  • Does this answer your question? [How to manually lazy load a module?](https://stackoverflow.com/questions/40293240/how-to-manually-lazy-load-a-module) – TylerH Mar 23 '23 at 13:41

2 Answers2

17

In Angular 8 the result of loadChildren function is either Promise of NgModule type in JIT mode or Promise of NgModuleFactory in AOT mode.

With this in mind you can rewrite your service as follows:

import { 
    Injectable, Compiler, Injector, Type, 
    ViewContainerRef, ComponentFactoryResolver,
    NgModuleFactory, Inject 
} from '@angular/core';

@Injectable()
export class LazyLoaderService {

  constructor(private injector: Injector,
    private compiler: Compiler,
    @Inject(LAZY_WIDGETS) private lazyWidgets: 
       { [key: string]: () => Promise<NgModuleFactory<any> | Type<any>> }) { }


  async load(name: string, container: ViewContainerRef) {
    const ngModuleOrNgModuleFactory = await this.lazyWidgets[name]();
 
    let moduleFactory;

    if (ngModuleOrNgModuleFactory instanceof NgModuleFactory) {
      // aot mode
      moduleFactory = ngModuleOrNgModuleFactory;
    } else {
      // jit mode
      moduleFactory = await this.compiler.compileModuleAsync(ngModuleOrNgModuleFactory);
    }

    const entryComponent = (<any>moduleFactory.moduleType).entry;
    const moduleRef = moduleFactory.create(this.injector);

    const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);

    const comp = container.createComponent(compFactory);
  }   
}

Stackblitz Example

Tip: Always look at the source code when you're in doubt

Community
  • 1
  • 1
yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Even in AOT mode this does not come back as a NgModuleFactory. so jit or aot executes the else statement and you get the error about no compiler – Jason Jun 20 '19 at 15:13
  • I use this approach in my project and it works well. Can you please provide a minimal reproduction on github? – yurzui Jun 20 '19 at 16:14
  • well, I created a repo but of course it is coming back as an ngModuleFactory. so now I will begin the process of comparing build configs – Jason Jun 20 '19 at 17:05
  • I found the difference! In my app I was using a tick mark instead of single quote. for some reason that makes a difference. if you use a tick mark to define the path to your module then the cli will not build an ng factory. this is why I was getting an instance even in AOT. – Jason Jun 20 '19 at 21:59
  • I your stackblitz example, what does the provideRoutes in the providers:[] do? We are not really creating routes right? (I'm new to Angular and am trying to understand DI) – milesmeow Jan 31 '20 at 18:41
4

It seems that when using Router, lazyWidgets const should have not name but path property:

export const lazyWidgets: { path: string, loadChildren: () => .....

Otherwise you'll get error:

Invalid configuration of route '': routes must have either a path or a matcher specified

ysf
  • 4,634
  • 3
  • 27
  • 29