Skip to content

Instantly share code, notes, and snippets.

@edujtm
Created May 25, 2023 19:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save edujtm/c3520e13a2530eee987e259854ea8e00 to your computer and use it in GitHub Desktop.
Save edujtm/c3520e13a2530eee987e259854ea8e00 to your computer and use it in GitHub Desktop.
mfe-loader
import { InjectionToken } from "@angular/core";
export const BUNDLE_URLS = new InjectionToken<string[]>('BUNDLE_URLS');
export interface BundleConfig {
bundleName: string;
bundleUrl: string;
loadOnStartup?: boolean;
}
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from "@angular/core";
import { MicroFrontendLoader } from "./microfrontend-loader.service";
import { FromExternalBundleDirective } from './external-component.directive';
import { BUNDLE_URLS, BundleConfig } from "./external-component.tokens";
@NgModule({
declarations: [
FromExternalBundleDirective,
],
providers: [
MicroFrontendLoader,
{
provide: APP_INITIALIZER,
useFactory: (mfloader: MicroFrontendLoader) => () => mfloader.loadBundles(),
deps: [MicroFrontendLoader],
multi: true,
}
],
exports: [
FromExternalBundleDirective,
],
})
export class ExternalLoaderModule {
static forRoot(bundleUrls: BundleConfig[]): ModuleWithProviders<ExternalLoaderModule> {
return {
ngModule: ExternalLoaderModule,
providers: [
{ provide: BUNDLE_URLS, useValue: bundleUrls },
],
}
}
}
import { ChangeDetectorRef, Directive, Input, OnChanges, OnInit, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core';
import { MicroFrontendLoader } from './microfrontend-loader.service';
import { ExternalComponentLoadingComponent } from './external-component-loading.component';
@Directive({
selector: '[fromExternalBundle]'
})
export class FromExternalBundleDirective implements OnInit, OnChanges {
@Input('fromExternalBundle') bundleName?: string;
constructor(
private template: TemplateRef<any>,
private containerRef: ViewContainerRef,
private externalBundleLoader: MicroFrontendLoader,
private changeDetector: ChangeDetectorRef
) { }
ngOnInit(): void {
//this.loadBundle();
console.log('bundle name', this.bundleName)
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['bundleName'] && changes['bundleName'].firstChange) {
this.loadBundle();
}
}
private async loadBundle() {
const elementName = this.elementName();
console.log(`Loading bundle for ${elementName}...`);
this.containerRef.createComponent(ExternalComponentLoadingComponent);
if (this.bundleName === undefined) {
console.error(`Bundle name is not defined for external web component with tag ${elementName}.`);
return;
}
const loadSuccessful = await this.externalBundleLoader.loadBundleByName(this.bundleName);
if (!loadSuccessful) {
console.error(`Could not load bundle for external web component with tag ${elementName}.`);
return;
}
console.log('element', elementName);
await customElements.whenDefined(elementName);
this.containerRef.clear();
this.containerRef.createEmbeddedView(this.template);
this.changeDetector.markForCheck();
}
private elementName(): string {
const tpl = this.template as any
const elementTag = tpl._declarationTContainer
? tpl._declarationTContainer.tagName || tpl._declarationTContainer.value
: tpl._def.element.template.nodes[0].element.name;
return elementTag
}
}
import { Inject, Injectable } from "@angular/core";
import { BUNDLE_URLS, BundleConfig } from "./external-component.tokens";
function load(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => resolve();
script.onerror = () => reject({
error: `Bundle ${url} could not be loaded`,
});
document.body.appendChild(script);
});
}
enum BundleState {
UNKNOWN, LOADING, LOADED, FAILED
}
@Injectable()
export class MicroFrontendLoader {
private loadingStates: Record<string, BundleState> = {};
constructor(@Inject(BUNDLE_URLS) private bundles: BundleConfig[]) {}
async loadBundles(): Promise<boolean> {
const bundleLoaders = this.bundles.map(bundle => this.loadBundle(bundle.bundleUrl));
const result = await Promise.all(bundleLoaders);
return result.some(isSuccess => isSuccess === false);
}
async loadBundleByName(bundleName: string): Promise<boolean> {
const url = this.urlFromBundleName(bundleName);
return await this.loadBundle(url);
}
async loadBundle(url: string): Promise<boolean> {
if ([BundleState.LOADED, BundleState.LOADING].includes(this.loadingStateFor(url))) {
return true;
}
this.loadingStates[url] = BundleState.LOADING;
const loadSuccessful = await load(url)
.then(() => true)
.catch(() => false)
this.loadingStates[url] = loadSuccessful ? BundleState.LOADED : BundleState.FAILED;
return loadSuccessful
}
public loadingStateFor(url: string) {
return this.loadingStates[url] || BundleState.UNKNOWN;
}
public isLoaded(bundleName: string): boolean {
const url = this.urlFromBundleName(bundleName);
return this.loadingStateFor(url) === BundleState.LOADED;
}
private urlFromBundleName(bundleName: string): string {
const bundle = this.bundles.find(bundle => bundle.bundleName === bundleName);
if (bundle === undefined) {
throw new Error(
`Bundle with name ${bundleName} is not registered. Configure it on the ExternalLoaderModule.forRoot({ ...config })`
);
}
return bundle.bundleUrl
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment