Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lazy load module in angular 8

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

like image 511
Jason Avatar asked Jun 20 '19 12:06

Jason


People also ask

What is lazy loaded module in Angular?

Lazy loading is an approach to limit the modules that are loaded to the ones that the user currently needs. This can improve your application's performance and reduce the initial bundle size. By default, Angular uses eager loading to load modules.

How do you use lazy loading in Angular 8?

To lazy load Angular modules, use loadChildren (instead of component ) in your AppRoutingModule routes configuration as follows. content_copy const routes: Routes = [ { path: 'items', loadChildren: () => import('./items/items. module'). then(m => m.

What is lazy loading modules?

Lazy loading is the process of loading components, modules, or other assets of a website as they're required. Since Angular creates a SPA (Single Page Application), all of its components are loaded at once. This means that a lot of unnecessary libraries or modules might be loaded as well.

How is lazy loading implement in Angular?

Lazy loading is a technique in Angular that allows you to load JavaScript components asynchronously when a specific route is activated. It improves the speed of the application load time by splitting the application into several bundles. When the user navigates through the app, the bundles are loaded as required.

How to lazy load a route in Angular 8?

As of Angular 8, you can generate a feature module with routing enabled, a default component and add as a lazy loaded route to the router. The above command must have the following flags: --module flag ( Required) – The module where to register the route for the app.

How to use lazy loading in ngmodules?

To use lazy loading, first, you need to create some feature modules. Feature modules are NgModules created for the purpose of code organization. Feature modules allow you to separate code for a feature/functionality from the rest of your app. Components, Pipes, Directives, etc.

How to use string-based lazy loading for loadchildren in angular?

In Angular version 8, the string syntax for the loadChildren route specification was deprecated in favor of the import () syntax. However, you can opt into using string-based lazy loading (loadChildren: './path/to/module#Module') by including the lazy-loaded routes in your tsconfig file, which includes the lazy-loaded files in the compilation.

How do I set up a lazy-loaded feature module?

In the lazy-loaded module's routing module, add a route for the component. Also be sure to remove the ItemsModule from the AppModule . For step-by-step instructions on lazy loading modules, continue with the following sections of this page. There are two main steps to setting up a lazy-loaded feature module:


2 Answers

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

  • https://github.com/angular/angular/blob/72ecc453639eae017f75653c9004adc406ed2ee6/packages/router/src/router_config_loader.ts#L46-L59

  • https://github.com/angular/angular/blob/32886cf9ace539e14e2b387cd8afb10715c8d3de/aio/src/app/custom-elements/elements-loader.ts#L56-L68

like image 68
yurzui Avatar answered Sep 22 '22 13:09

yurzui


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

like image 21
Dymon Khlopov Avatar answered Sep 25 '22 13:09

Dymon Khlopov