Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dynamic Lazy Load Module #19

Open
tiberiuzuld opened this issue Jul 22, 2019 · 6 comments
Open

Add Dynamic Lazy Load Module #19

tiberiuzuld opened this issue Jul 22, 2019 · 6 comments
Labels
need doc That issue should be shown in documentation resolved That issue was resolved

Comments

@tiberiuzuld
Copy link

Hello,
I managed to implement lazy load a feature module dynamically and load the the component in the DOM using this library for dynamic rendering of the component.
Works in both JIT and AOT runtime.
I think this feature will be great addition to the list of features this library supports.
Steps:

  1. Add a lazy route which will never be accessed like after the path to not found route in the app routing. The lazy loaded module route config should be defined in the root route config.
const appRoutes: Routes = [
  ...
  {
    path: '**',
    component: NotFoundComponent,
    // redirectTo: 'login'
  },
  {
    path: 'lazyModule',
    loadChildren: () => import('./lazyModule/lazyModule.module').then(m => m.LazyModule)
  }
]
  1. Define the lazy loaded module and set the entry component as static property on the module class.
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {LazyFeatureComponent} from './lazy-feature.component';

@NgModule({
  declarations: [LazyFeatureComponent],
  exports: [LazyFeatureComponent],
  entryComponents: [LazyFeatureComponent],
  imports: [ CommonModule]
})
export class LazyModule {
  static entry = LazyFeatureComponent; // This is needed to determine the component we want to render
}
  1. Here I created a wrapper component around the this library to load the lazy module.
import {Compiler, Component, Input, NgModuleFactory, OnInit} from '@angular/core';
import {Router} from '@angular/router';

@Component({
  selector: 'app-lazy-load',
  templateUrl: './lazy-load.component.html',
  styleUrls: ['./lazy-load.component.scss']
})
export class LazyLoadComponent implements OnInit {
  @Input() module: string;
  @Input() context: object;

  component: any;
  moduleFactory: NgModuleFactory<any>;

  constructor(private compiler: Compiler, private router: Router) {
  }

  ngOnInit() {
    const routeConfigs = this.router.config.filter(routeConfig => routeConfig.path === this.module);
    if (routeConfigs[0] && routeConfigs[0].loadChildren && routeConfigs[0].loadChildren instanceof Function) {
      (routeConfigs[0].loadChildren() as any)
        .then(module => {
          if (module instanceof NgModuleFactory) { // for AOT runtime
            return module;
          } else { // for JIT runtime
            return this.compiler.compileModuleAsync(module);
          }
        })
        .then(moduleFactory => {
          this.component = (moduleFactory.moduleType as any).entry; // our entry component to our feature
          this.moduleFactory = moduleFactory;
        });
    } else {
      console.error('Lazy module not found.');
    }
  }
}
<ng-container *ngIf="moduleFactory">
  <ng-container
    *ngxComponentOutlet="component; context: context; ngModuleFactory: moduleFactory;">
  </ng-container>
</ng-container>
 <app-lazy-load [context]="{...}"
                 [module]="'lazyModule'"></app-lazy-load>

That is all the setup needed to make a feature module lazy load at runtime when needed depending on data you have.
The loading of the module is the same thing as what angular does on lazy routes here but the code is private and not accessible from Router.
If the angular team makes the code public API in the future we can reuse they're loadModuleFactory method.

Let me know if you have any further questions or need any help integrating this part in the library.

Thanks

@thekiba
Copy link
Contributor

thekiba commented Sep 4, 2019

Hello, thanks for interesting suggestion!

Did you tried using Lazy Modules with Ivy Renderer API before? I think it can be more declaratively instead of using old View Engine.

Do you want to discuss that feature and implement that via PR?

@tiberiuzuld
Copy link
Author

Hello,
Tried using the my implementation of Lazy Modules and they work with Ivy.
Didn't try using the Ivy Renderer API, didn't had the time to look into it.
We can discuss what ideas you have for improvement.

@thekiba thekiba added enhancement New feature or request help wanted Extra attention is needed labels Sep 27, 2019
@thekiba
Copy link
Contributor

thekiba commented Feb 19, 2020

I'll close that issue because with Angular Ivy we can load components lazily. You can find out an example in readme or check out that tweet.

Thanks!

@thekiba thekiba closed this as completed Feb 19, 2020
@thekiba thekiba added need doc That issue should be shown in documentation resolved That issue was resolved and removed enhancement New feature or request help wanted Extra attention is needed labels Jul 17, 2020
@thekiba thekiba reopened this Jul 17, 2020
@tiberiuzuld
Copy link
Author

Hello @thekiba ,
With the release of Ivy I think that in the ngxd needs some changes regarding ngModuleFactory.

// https://github.com/IndigoSoft/ngxd/blob/master/projects/core/src/lib/directive/component.outlet.ts
// from
@Input() ngxComponentOutletNgModuleFactory: NgModuleFactory<any> | null;
// to
@Input() ngxComponentOutletNgModule: Type<any> | null;
...
// https://angular.io/api/core/createNgModuleRef
  private createNgModuleRef() {
    if (this.ngxComponentOutletNgModule) {
      this._ngModuleRef = createNgModuleRef(this.ngxComponentOutletNgModule, this.injector);
    }
  }

So my code from original post will be a bit simpler:

      (routeConfigs[0].loadChildren() as any).then(module => {
        this.component = module.entry;
        this.module = module;
      });

I also tested your suggestion with component | async and to have it load the component directly, but I have an issue if I have some services provided on the module and the entire module is defined as a lazy load route, in this case I will get an a NullInjectorError: No provider for ... error. Yeah probably I should move them from module to providedin root.

Interestingly in angular they still use compiler and NgModuleFactory, even tho they are deprecated. https://github.com/angular/angular/blob/master/packages/router/src/router_config_loader.ts#L71

If you think this changes would be useful I can work on a PR to make the changes.

@tiberiuzuld
Copy link
Author

tiberiuzuld commented Jun 24, 2022

Hello,
Updated code for lazy load of standalone components in Angular v14:

// define your route on the root routes
// this is needed for angular to build your component in the final package, they detect dynamic imports only defined in routes.
const appRoutes: Routes = [
  ...
  {
    path: '**',
    component: NotFoundComponent,
    // redirectTo: 'login'
  }, // lazy standalone components routes below, so user never reaches them
  {
    path: LazyComponents.MY_LAZY_COMPONENT,
    loadComponent: () => import('./lazyComponent/lazy.component').then(c=> c.LazyComponent)
  }
];
// enum to make things easy
export enum LazyComponents {
  MY_LAZY_COMPONENT = 'my-random-path'
}

// app-lazy-load component to fetch the component
import {CommonModule} from '@angular/common';
import {Component, Input, OnInit, Type} from '@angular/core';
import {Router} from '@angular/router';
import {NgxdModule} from '@ngxd/core';
import {from, Observable, of} from 'rxjs';
import {LazyComponents} from '....';

const wrapInObservable = <T>(value: T | Observable<T> | Promise<T>): Observable<T> => {
  if (value instanceof Observable) {
    return value;
  }
  if (value instanceof Promise) {
    return from(value);
  }

  return of(value);
};

@Component({
  selector: 'app-lazy-load',
  templateUrl: './lazy-load.component.html',
  standalone: true,
  imports: [CommonModule, NgxdModule]
})
export class LazyLoadComponent implements OnInit {
  @Input() component: LazyComponents;
  @Input() context: object;

  component: Type<unknown>;

  constructor(private router: Router) {}

  ngOnInit() {
    const routeConfig = this.router.config.find(config => config.path === this.module);
    if (routeConfig && routeConfig.loadComponent && routeConfig.loadComponent instanceof Function) {
      wrapInObservable(routeConfig.loadComponent()).subscribe(component => (this.component = component));
    } else {
      console.error('Lazy component not found.');
    }
  }
}
<ng-container *ngIf="component">
  <ng-container *ngxComponentOutlet="component; context: context;"> </ng-container>
</ng-container>
// app component in which to load my lazy standalone component 
import {Component} from '@angular/core';
import {LazyComponents} from '....';
@Component({
  selector: 'app-component',
  template: './app-component.component.html',
})
export class AppComponent {
 LazyComponents = LazyComponents;
}
  <app-lazy-load [context]="mycontext" [component]="LazyComponents.MY_LAZY_COMPONENT"></app-lazy-load>

@alinmateut
Copy link

Hey, we can now lazy load standalone components with ngxd. If the modules are converted to standalone components, there's no need for this workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
need doc That issue should be shown in documentation resolved That issue was resolved
Projects
None yet
Development

No branches or pull requests

3 participants