share-loader icon indicating copy to clipboard operation
share-loader copied to clipboard

How to load external module/micro app not via route

Open ofirbenezra opened this issue 6 years ago • 13 comments

Hi MrFrankel, I'm trying to load an external module not via route (in the loadChildren) and call some function on. I've tried using the loadScript function and the .js is indeed loaded but it is not loaded in the same way done by the router (i.e the dependencies are missing). I've tried to also do it with import() but i could get it to working with module which isn't feature module. i.e. the module is not on the same project. What am I missing here? Any help would be greatly appreciated. Thanks

ofirbenezra avatar May 28 '19 07:05 ofirbenezra

hey, can you share a repo or code sample?

MrFrankel avatar May 28 '19 08:05 MrFrankel

Hey. Thank you for the quick reply. I can prepare a repo or some code sample but what I want to achieve is dynamic (UMD) module loading of the external module (ext-app1 in your sample). Will I need to compile the module (compileModuleAndAllComponentsAsync) in order for this to work? I have some code I want to call from the shell app which is found only on the external app (example: I have a component in the external app which I want to show when clicking on some button in the shell app). If you need some more info please let me know.

ofirbenezra avatar May 28 '19 11:05 ofirbenezra

yes, you will need to pass any module loaded at runtime through Angular compiler. even AOT module need's to pass through Angular NgModuleFactoryLoader

MrFrankel avatar May 28 '19 12:05 MrFrankel

Thank you. I'll try that but that seems a bit cumbersome in order to do "cross app/module communication". My initial thought was about having some message/event bus that the shell application would emit event and the external app would listen to it and then render some internal component. but still I need to load the external app before emitting the event. Do you have any other suggestion on how to approach that?

ofirbenezra avatar May 28 '19 13:05 ofirbenezra

but the external module is an Angular module right? it has to go through the Angular module loading process in order to be available to Angular.

Generally speaking, the bundle that you are loading externally is just JS, meaning you can have some code that that adds an eventlistener on the body for instance, once the bundle is loaded into the document you can fire say event on the DOM by the shell app and the loaded bundle listener will be triggered. theortically if you pass in that event the main injector the external module can load itself into Angular.

But no components from the external bundle can be avaliable for use without passing the module through Angular modue loader.

Angular team have been working on something that can allow that, Angular Elements, if you just want to load components you can look into that.

MrFrankel avatar May 28 '19 13:05 MrFrankel

theortically if you pass in that event the main injector the external module can load itself into Angular. Can you show some code (even pseudo) that demonstrates that? I'll need to render components from the external app. Would that work with the above or like you said the only way would be to go through the Angular module loader? Thanks a lot for all the help.

ofirbenezra avatar May 28 '19 13:05 ofirbenezra

i'm not sure i'm following you all the way, you want to use the components in the external bundle without compiling and loading the module?

MrFrankel avatar May 28 '19 13:05 MrFrankel

Let me explain - what I was asking if the method you've suggested ("... theortically if you pass in that event the main injector the external module can load itself into Angular") will enable me to show/use components from the external module (which is something I need) or the only way to do that would be by going through the Angular module loader?

ofirbenezra avatar May 28 '19 14:05 ofirbenezra

@ofirbenezra Hi, it's a well addressed problem in Angular world, e.g.: https://netbasal.com/the-need-for-speed-lazy-load-non-routable-modules-in-angular-30c8f1c33093 https://www.youtube.com/watch?v=pERhnBBae2k It's not related to share-loader solution :slightly_smiling_face:

haskelcurry avatar Jun 21 '19 10:06 haskelcurry

@ofirbenezra : if you need multiple micro frontends on 1 single page you can add auxiliary routes (named router-outlets). In that way one will be in your main router-outlet and the other in the auxiliary one. And as @mtuzinskiy pointed out: it is not related to share-loader solution

alexej-strelzow avatar Jun 21 '19 15:06 alexej-strelzow

@ofirbenezra If you want to load a component from a child app into the shell without routing you can do something like this where you load the module using loadScript but then create the component manually. This is essentially exactly what the route is doing under the covers.

In the parent app:

export class AppComponent {
 // Since you aren't using routing, you need a way to indicate where the component will be rendered
 // which can be done by adding <template #outlet></template> into your component's template.
 @ViewChild('outlet', { read: ViewContainerRef }) outlet: ViewContainerRef;

  constructor(private injector: Injector) {
    this.load();
  }

  load() {
    const url = 'http://localhost:4300/main.js';
    const namespace = 'extapp';
    const moduleName = 'AppModule';

    loadScript(url, namespace, moduleName).then((moduleFactory: NgModuleFactory<any>) => {
      // NOTE: In the module in the child app, you need a way to expose which component
      // can be loaded dynamically since you aren't using route configurations. See below
      const entryComponent = (<any>moduleFactory.moduleType).component;
      if (!entryComponent) {
        return Promise.reject(`[Angular] No component found in module ${url}`);
      }
      // use the module factory loaded from the child to create the module
      const moduleRef = moduleFactory.create(this.injector);
      // Once you have the module and the component to renderer, you can create a component
      // factory that you will use to create your component
      const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);
      // remove anything that was already created in your outlet
      this.outlet.clear();
      // create your component
      this.outlet.createComponent(compFactory);
      // NOTE: createComponent will return a ComponentRef from which you can get its 'instance' 
      // property which will refer to the component you just created. You can then set its properties 
      // and call it's lifecycle methods: ngInit, ngOnChanges, etc. since they won't get call automatically

    });
  }
}

For completeness, here is where you would add a static component property to the child module:

In child's app.external.module:

@NgModule({
    declarations: [MyComponent],
    exports: [MyComponent],
    entryComponents: [MyComponent]
})
export class AppModule {
  static component: Type<MyComponent> = MyComponent;
}

RobM-ADP avatar Sep 11 '19 20:09 RobM-ADP

@RobM-ADP : thank you so much for this solution (reminds me of Wes Grimes' one - see link at the bottom of this comment). I already gave it a try and it works like a charm.

However, I discovered a limitation: the micro frontend cannot have its own independent routing when you use this solution. So it's perfect for widgets (e.g. complex lists with search functionality). If you need routing then you must use auxiliary routes in you shell app (named router outlet) and load the Micro Frontend (via routing) into that outlet.

Some literature: https://wesleygrimes.com/angular/2019/02/05/building-an-aot-friendly-dynamic-content-outlet.html

alexej-strelzow avatar Sep 12 '19 12:09 alexej-strelzow

@alexej-strelzow I would say that if you are using routing in the child app, you probably would be loading the child app with routing in most cases. But it would probably be good for some of these limitations to be documented. @MrFrankel - Would you accept PRs with some of these gotchas and examples in a README?

RobM-ADP avatar Sep 12 '19 13:09 RobM-ADP