Indigosoft / ngxd

✨🦊 NgComponentOutlet + Data-Binding + Full Lifecycle = NgxComponentOutlet for Angular 7, 8, 9, 10, 11, 12, 13, 14, 15, 16+
MIT License
321 stars 29 forks source link

Add Dynamic Lazy Load Module #19

Open tiberiuzuld opened 5 years ago

tiberiuzuld commented 5 years ago

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)
    }
    ]
  2. 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 }

3. Here I created a wrapper component around the this library to load the lazy module.
```ts
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 commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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!

tiberiuzuld commented 3 years ago

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 commented 2 years ago

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 commented 1 year ago

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.