Load new modules dynamically in run-time with Angular CLI & Angular 5

57,749

Solution 1

I was facing the same problem. As far as I understand it until now:

Webpack puts all resources in a bundle and replaces all System.import with __webpack_require__. Therefore, if you want to load a module dynamically at runtime by using SystemJsNgModuleLoader, the loader will search for the module in the bundle. If the module does not exist in the bundle, you will get an error. Webpack is not going to ask the server for that module. This is a problem for us, since we want to load a module that we do not know at build/compile time. What we need is loader that will load a module for us at runtime (lazy and dynamic). In my example, I am using SystemJS and Angular 6 / CLI.

  1. Install SystemJS: npm install systemjs –save
  2. Add it to angular.json: "scripts": [ "node_modules/systemjs/dist/system.src.js"]

app.component.ts

import { Compiler, Component, Injector, ViewChild, ViewContainerRef } from '@angular/core';

import * as AngularCommon from '@angular/common';
import * as AngularCore from '@angular/core';

declare var SystemJS;

@Component({
  selector: 'app-root',
  template: '<button (click)="load()">Load</button><ng-container #vc></ng-container>'
})
export class AppComponent {
  @ViewChild('vc', {read: ViewContainerRef}) vc;

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

  load() {
    // register the modules that we already loaded so that no HTTP request is made
    // in my case, the modules are already available in my bundle (bundled by webpack)
    SystemJS.set('@angular/core', SystemJS.newModule(AngularCore));
    SystemJS.set('@angular/common', SystemJS.newModule(AngularCommon));

    // now, import the new module
    SystemJS.import('my-dynamic.component.js').then((module) => {
      this.compiler.compileModuleAndAllComponentsAsync(module.default)
            .then((compiled) => {
                let moduleRef = compiled.ngModuleFactory.create(this.injector);
                let factory = compiled.componentFactories[0];
                if (factory) {
                    let component = this.vc.createComponent(factory);
                    let instance = component.instance;
                }
            });
    });
  }
}

my-dynamic.component.ts

import { NgModule, Component } from '@angular/core';
import { CommonModule } from '@angular/common';

import { Other } from './other';

@Component({
    selector: 'my-dynamic-component',
    template: '<h1>Dynamic component</h1><button (click)="LoadMore()">LoadMore</button>'
})    
export class MyDynamicComponent {
    LoadMore() {
        let other = new Other();
        other.hello();
    }
}
@NgModule({
    declarations: [MyDynamicComponent],
    imports: [CommonModule],
})
export default class MyDynamicModule {}

other.component.ts

export class Other {
    hello() {
        console.log("hello");
    }
}

As you can see, we can tell SystemJS what modules already exist in our bundle. So we do not need to load them again (SystemJS.set). All other modules that we import in our my-dynamic-component (in this example other) will be requested from the server at runtime.

Solution 2

I've used the https://github.com/kirjs/angular-dynamic-module-loading solution with Angular 6's library support to create an application I shared on Github. Due to company policy it needed to be taken offline. As soon as discussions are over regarding the example project source I will share it on Github!

UPDATE: repo can be found ; https://github.com/lmeijdam/angular-umd-dynamic-example

Solution 3

Do it with angular 6 library and rollup do the trick. I've just experiment with it and i can share standalone angular AOT module with the main app without rebuild last.

  1. In angular library set angularCompilerOptions.skipTemplateCodegen to false and after build library you will get module factory.
  2. After that build an umd module with rollup like this: rollup dist/plugin/esm2015/lib/plugin.module.ngfactory.js --file src/assets/plugin.module.umd.js --format umd --name plugin
  3. Load text source umd bundle in main app and eval it with module context
  4. Now you can access to ModuleFactory from exports object

Here https://github.com/iwnow/angular-plugin-example you can find how develop plugin with standalone building and AOT

Solution 4

I have tested in Angular 6, below solution works for dynamically loading a module from an external package or an internal module.

1. If you want to dynamically load a module from a library project or a package:

I have a library project "admin" (or you can use a package) and an application project "app". In my "admin" library project, I have AdminModule and AdminRoutingModule. In my "app" project:

a. Make change in tsconfig.app.json:

  "compilerOptions": {
    "module": "esNext",
  },

b. In app-routing.module.ts:

const routes: Routes = [
    {
        path: 'admin',
        loadChildren: async () => {
            const a = await import('admin')
            return a['AdminModule'];
        }
    },
    {
        path: '',
        redirectTo: '',
        pathMatch: 'full'
    }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule {
}

2. if you want to load a module from the same project.

There are 4 different options:

a. In app-routing.module.ts:

const routes: Routes = [
    {
        path: 'example',
        /* Options 1: Use component */
        // component: ExampleComponent,  // Load router from component
        /* Options 2: Use Angular default lazy load syntax */
        loadChildren: './example/example.module#ExampleModule',  // lazy load router from module
        /* Options 3: Use Module */
        // loadChildren: () => ExampleModule, // load router from module
        /* Options 4: Use esNext, you need to change tsconfig.app.json */
        /*
        loadChildren: async () => {
            const a = await import('./example/example.module')
            return a['ExampleModule'];
        }
        */
    },
    {
        path: '',
        redirectTo: '',
        pathMatch: 'full'
    }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule {
}
``

Solution 5

I believe this is possible using SystemJS to load a UMD bundle if you build and run your main application using webpack. I used a solution that uses ng-packagr to build a UMD bundle of the dynamic plugin/addon module. This github demonstrates the procedure described: https://github.com/nmarra/dynamic-module-loading

Share:
57,749
Lars Meijdam
Author by

Lars Meijdam

Angular Enthusiast, Web and Mobile lover.

Updated on July 08, 2022

Comments

  • Lars Meijdam
    Lars Meijdam almost 2 years

    Currently I'm working on a project which is being hosted on a clients server. For new 'modules' there is no intention to recompile the entire application. That said, the client wants to update the router/lazy loaded modules in runtime. I've tried several things out but I can't get it to work. I was wondering if any of you knows what I could still try or what I missed.

    One thing I noticed, most of the resources I tried, using angular cli, are being bundled into seperate chunks by webpack by default when building the application. Which seems logical as it makes use of the webpack code splitting. but what if the module is not known yet at compile time (but a compiled module is stored somewhere on a server)? The bundling does not work because it can't find the module to import. And Using SystemJS will load up UMD modules whenever found on the system, but are also bundled in a seperate chunk by webpack.

    Some resources I already tried;

    Some code I already tried and implement, but not working at this time;

    Extending router with normal module.ts file

      this.router.config.push({
        path: "external",
        loadChildren: () =>
          System.import("./module/external.module").then(
            module => module["ExternalModule"],
            () => {
              throw { loadChunkError: true };
            }
          )
      });
    

    Normal SystemJS Import of UMD bundle

    System.import("./external/bundles/external.umd.js").then(modules => {
      console.log(modules);
      this.compiler.compileModuleAndAllComponentsAsync(modules['External'])
        .then(compiled => {
          const m = compiled.ngModuleFactory.create(this.injector);
          const factory = compiled.componentFactories[0];
          const cmp = factory.create(this.injector, [], null, m);
        });
    });
    

    Import external module, not working with webpack (afaik)

    const url = 'https://gist.githubusercontent.com/dianadujing/a7bbbf191349182e1d459286dba0282f/raw/c23281f8c5fabb10ab9d144489316919e4233d11/app.module.ts';
    const importer = (url:any) => Observable.fromPromise(System.import(url));
    console.log('importer:', importer);
    importer(url)
      .subscribe((modules) => {
        console.log('modules:', modules, modules['AppModule']);
        this.cfr = this.compiler
          .compileModuleAndAllComponentsSync(modules['AppModule']);
        console.log(this.cfr,',', this.cfr.componentFactories[0]);
        this.external.createComponent(this.cfr.componentFactories[0], 0);
    });
    

    Use SystemJsNgModuleLoader

    this.loader.load('app/lazy/lazy.module#LazyModule')
      .then((moduleFactory: NgModuleFactory<any>) => {
        console.log(moduleFactory);
        const entryComponent = (<any>moduleFactory.moduleType).entry;
        const moduleRef = moduleFactory.create(this.injector);
    
        const compFactory = moduleRef.componentFactoryResolver
          .resolveComponentFactory(entryComponent);
      });
    

    Tried loading a module made with rollup

    this.http.get(`./myplugin/${metadataFileName}`)
      .map(res => res.json())
      .map((metadata: PluginMetadata) => {
    
        // create the element to load in the module and factories
        const script = document.createElement('script');
        script.src = `./myplugin/${factoryFileName}`;
    
        script.onload = () => {
          //rollup builds the bundle so it's attached to the window 
          //object when loaded in
          const moduleFactory: NgModuleFactory<any> = 
            window[metadata.name][metadata.moduleName + factorySuffix];
          const moduleRef = moduleFactory.create(this.injector);
    
          //use the entry point token to grab the component type that 
          //we should be rendering
          const compType = moduleRef.injector.get(pluginEntryPointToken);
          const compFactory = moduleRef.componentFactoryResolver
            .resolveComponentFactory(compType); 
    // Works perfectly in debug, but when building for production it
    // returns an error 'cannot find name Component of undefined' 
    // Not getting it to work with the router module.
        }
    
        document.head.appendChild(script);
    
      }).subscribe();
    

    Example with SystemJsNgModuleLoader only works when the Module is already provided as 'lazy' route in the RouterModule of the app (which turns it into a chunk when built with webpack)

    I found a lot of discussion about this topic on StackOverflow here and there and provided solutions seem really good of loading modules/components dynamically if known up front. but none is fitting for our use case of the project. Please let me know what I can still try or dive into.

    Thanks!

    EDIT: I've found; https://github.com/kirjs/angular-dynamic-module-loading and will give this a try.

    UPDATE: I've created a repository with an example of loading modules dynamically using SystemJS (and using Angular 6); https://github.com/lmeijdam/angular-umd-dynamic-example