63

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

ruffin
  • 16,507
  • 9
  • 88
  • 138
Lars Meijdam
  • 1,177
  • 1
  • 10
  • 18
  • I'm getting an error as ERROR Error: Cannot find module 'https://gist.githubusercontent.com/dianadujing/a7bbbf191349182e1d459286dba0282f/raw/c23281f8c5fabb10ab9d144489316919e4233d11/app.module.ts'. Can anyone help me to resolve ? – Hulk1991 Sep 15 '20 at 10:27

6 Answers6

18

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.

Michael
  • 408
  • 3
  • 13
  • The dynamic component in your example, where is this located? Did you create a library (ng g lib ) first and use the UMD module received from building that library ? Or do you transpile it yourself before serving/building the entire application? Otherwise you have a plunkr or? – Lars Meijdam May 18 '18 at 06:22
  • My setup is a little more complex. Therefore I am not sure. As soon as you execute `SystemJS.import('my-dynamic.component.js').then()` you will see where your browser is looking for it. It should be in the root folder of your app or next to `AppComponent`. I am using Visual Studio an Visual Studio is transpiling my `my-dynamic.component.ts`. You can use tsc my-dynamic.component.ts If I find some time I will create a plunkr. But until then I will update my answer here. – Michael May 18 '18 at 06:59
  • Ah yeah, I now did it by the library support and copy the umd module to the application and that was working too! Strange thing is that in the other implementations above (question) I noticed webpack was also creating chunks (by default) for stuff imported using SystemJS. but in your example it didn't! So that's a good sign!! – Lars Meijdam May 18 '18 at 08:27
  • I managed to solve this issue using the approach above, but my webpack and tsconfig are using the AMD module type. – BrendanBenting Jun 05 '18 at 06:21
  • 1
    I am having same issue, still not able to fix it. I have created an library using angular 6 CLI and now need to load it dyanamically. I have coped the library to the dist folder of the main angular application. But SystemJS.import('/sf-ws1/bundles/sf-ws1.umd.js') is not working. Getting error : Error: No NgModule metadata found for 'undefined'. Any adivice on how to achieve this and if any one clould create a working git repo, would be very helpful. – Kamal Kr Jun 05 '18 at 10:58
  • @LarsMeijdam I also created library using ng g lib and build this library. Need help in loading this dynamically. Could you please create a plunker ? – Kamal Kr Jun 05 '18 at 11:02
  • 1
    I just created a github: https://github.com/mrmscmike/ngx-dynamic-module-loader It's just a draft and we sure need to improve it. Maybe we can collect our experiences. Any help is welcome. – Michael Jun 05 '18 at 13:07
  • @Michael thanks your sample is working. Let me try it with angular library – Kamal Kr Jun 05 '18 at 17:47
  • 1
    @kamalnayan I think i know what the problem is with "No NgModule metadata found for 'undefined'". If you look at your `*.umd.js` and compare it to the example `my.module.js`, you can see that my.module does the following `exports["default"] = MyModule;`. The Angular 6 lib does `exports.MyModule = MyModule;`. If you `compileModuleAndAllComponentsAsync(module['PluginModule'])` it works. No idea how to get `.default` working – Thomas Schneiter Jun 05 '18 at 20:13
  • @ThomasSchneiter thanks, your are right default does not work and module['PluginModule'] is working. – Kamal Kr Jun 06 '18 at 04:18
  • @kamalnayan i assume that this here is where `default` comes from `export default class MyModule { }`, but if i add `default` to the library module it is not exported at all. – Thomas Schneiter Jun 06 '18 at 06:42
  • @ThomasSchneiter, Loading on button click is fine and it covers my one use case. But I am having issue to make it working with angular routing. If a user navigates to a route this dynamically loaded component should be displayed. – Kamal Kr Jun 06 '18 at 06:49
  • @ThomasSchneiter I can do it by calling load method from the constructor of a component and depending on the route paramter it can load different module. – Kamal Kr Jun 06 '18 at 06:59
  • @kamalnayan Just load it in ngOnInit() that should be the correct place to do this. – Thomas Schneiter Jun 06 '18 at 11:18
  • @LarsMeijdam how are you packaging dependencies of created angular library. My need to make this library work without peerDependencies. So this library should be self-sufficient. – Kamal Kr Jun 18 '18 at 09:51
  • @kamalnayan, I havent actually planned on releasing a library as current project does not require packaging, more dynamic loading of Angular libraries created with the Angular 6 APF. – Lars Meijdam Jun 20 '18 at 09:11
  • I tried your approach working fine, but in my real scenario a third party will upload the module to a folder and main application will automatically load the module, we are keeping the module name and other info in json, but when others created a application using 'ng new' and ng serve and they will upload whatever there in dist folder to the main application assets folder. at that time this application not working, the project i got from git is here https://github.com/ajeeshc/angular-dynamic-module-loading – Aji Jul 30 '18 at 18:12
  • the SystemJS library seems to have changed a lot, the current solution no longer works in latest Angular – Guru Kara Jul 18 '19 at 08:51
6

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

Lars Meijdam
  • 1,177
  • 1
  • 10
  • 18
  • This solution puts the script in `assets`. How about linking to a typescript module? must be first converted to js manually copied. – FindOutIslamNow Mar 06 '19 at 06:11
  • the solution we at our project searched for was more based on compiled modules (JS), load them dynamically from a server (which is still debatable). so it would only load 'finished' modules which were placed in a specific folder, not typescript though – Lars Meijdam Mar 07 '19 at 09:29
4

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

splash
  • 13,037
  • 1
  • 44
  • 67
  • I like your solution, one thing i've actually prevent in my version is the use of 'eval' which I see you're still using. One of the solutions I've used to get rid of that is the use of SystemJS. – Lars Meijdam Oct 15 '18 at 08:45
  • One issue with this setup is that, when the plugin project imports an Angular Module which has entry components listed, generated plugin UMD will end up with factories of all the components listed in entry components. – iamrakesh Dec 28 '18 at 08:53
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 {
}
``

Robin Ding
  • 741
  • 6
  • 9
  • Nice answer! to be honest I've seen several using the lazy loading mechanism, one thing I actually tried to achieve was to load them in run time, without rebuilding the entire app (so only a browser refresh would suffice). I know that this is not possible with the loadChildren as you need to know all routes up front for this to work. – Lars Meijdam Feb 26 '19 at 13:28
  • @LarsMeijdam, That's great. I have been looking for a solution similar with your case. But the difference is that, I would like to have a configuration file to list all modules, then load then into the application dynamically. Unfortunately, my approach (case 1) above still has some issues. i.e. In order to use ```await import(...)```, I can't use an variable, which means I still can't achieve the goal to dynamically load any packages. But case 2 works fine. I am still doing research with webpack to see if there is any other clue. – Robin Ding Feb 26 '19 at 18:51
  • Did you check my github project which is noted at the end of the question ?:) This might be of a help!;) https://github.com/lmeijdam/angular-umd-dynamic-example – Lars Meijdam Feb 27 '19 at 07:11
  • Thank you very much. It works well. Loading from an URL is a really good solution. :) – Robin Ding Feb 27 '19 at 16:41
2

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

N.M.
  • 21
  • 5
  • This is a pretty cool solution. Although I've tried to inject a service provided by the main-app into the addon via a shared lib defining the InjectionToken. No luck so far. Any ideas on this? – Gob Jul 22 '18 at 11:29
0

Yes, you can lazy load modules using by referring them as modules in the router. Here is an example https://github.com/start-angular/SB-Admin-BS4-Angular-6

  1. First couple all the components that you are using into a single module
  2. Now refer that module in the router and angular will lazy load your module into the view.
Jaya Krishna
  • 313
  • 4
  • 14
  • 4
    Thanks for your comment,I was already aware of lazy loading by using the router. Only thing that happens is that Angular's build process is using Webpack to create chunk files from the lazy loaded modules. And this last part is something I dont want to let happen. As I'm actually looking for a solution to load modules in runtime WITHOUT KNOWING the modules up front. Instead I would retrieve a configuration from a Server which should make Angular aware of which module / plugin to load – Lars Meijdam May 17 '18 at 08:34
  • @LarsMeijdam so did you fid a solution? I am creating html/ts angular forms dinamically and wanted to import them into a running app. I also would like this stuff to not be precompiled, just the plain ts+html. Any info? – user5328504 May 10 '19 at 13:28