Skip to content

Commit 26ed84e

Browse files
committed
Expose bookings app as web-component
1 parent b81172c commit 26ed84e

File tree

10 files changed

+154
-7
lines changed

10 files changed

+154
-7
lines changed

angular.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@
137137
"prefix": "mf",
138138
"architect": {
139139
"build": {
140-
"builder": "@angular-devkit/build-angular:browser",
140+
"builder": "ngx-build-plus:build",
141141
"options": {
142+
"singleBundle": true,
143+
"outputHashing": "none",
142144
"outputPath": "projects/bookings/dist",
143145
"index": "projects/bookings/src/index.html",
144146
"main": "projects/bookings/src/main.ts",
@@ -187,8 +189,7 @@
187189
"replace": "projects/bookings/src/environments/environment.ts",
188190
"with": "projects/bookings/src/environments/environment.prod.ts"
189191
}
190-
],
191-
"outputHashing": "all"
192+
]
192193
}
193194
}
194195
},

projects/bookings/src/app/booking.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ import { BookingComponent } from './booking.component';
2424
HotToastModule.forRoot(),
2525
TranslateModule.forRoot(),
2626
],
27-
bootstrap: [BookingComponent],
27+
exports: [BookingComponent],
2828
})
2929
export class BookingModule {}

projects/bookings/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { enableProdMode } from '@angular/core';
22
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3-
import { BookingModule } from './app/booking.module';
43
import { environment } from './environments/environment';
4+
import { MicroFrontendModule } from './micro-frontend/micro-frontend.module';
55

66
if (environment.production) {
77
enableProdMode();
88
}
99

1010
platformBrowserDynamic()
11-
.bootstrapModule(BookingModule)
11+
.bootstrapModule(MicroFrontendModule)
1212
.catch((err) => console.error(err));
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
template: '<mf-booking></mf-booking>',
5+
})
6+
export class EntryComponent {}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { DoBootstrap, Injector, NgModule } from '@angular/core';
2+
import { createCustomElement } from '@angular/elements';
3+
import { BookingModule } from '../app/booking.module';
4+
import { EntryComponent } from './entry.component';
5+
@NgModule({
6+
declarations: [EntryComponent],
7+
imports: [BookingModule],
8+
})
9+
export class MicroFrontendModule implements DoBootstrap {
10+
constructor(private injector: Injector) {}
11+
12+
ngDoBootstrap(): void {
13+
const customElement = createCustomElement(EntryComponent, {
14+
injector: this.injector,
15+
});
16+
window.customElements.define('mf-bookings-entry', customElement);
17+
console.log('Registered custom element mf-bookings-entry');
18+
}
19+
}

projects/train-platform/src/app/app-routing.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ const routes: Routes = [
1313
{
1414
path: 'bookings',
1515
loadChildren: () =>
16-
import('./../booking/booking.module').then((m) => m.BookingModule),
16+
import('./../micro-frontends/bookings-host.module').then(
17+
(m) => m.BookingsHostModule
18+
),
1719
},
1820
];
1921

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-bookings-host',
5+
template: ` <mf-bookings-entry></mf-bookings-entry> `,
6+
})
7+
export class BookingsHostComponent {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
2+
import { RouterModule } from '@angular/router';
3+
import { BookingsHostComponent } from './bookings-host.component';
4+
import { LoadMicroFrontendGuard } from './load-micro-frontend.guard';
5+
6+
@NgModule({
7+
declarations: [BookingsHostComponent],
8+
imports: [
9+
RouterModule.forChild([
10+
{
11+
path: '**',
12+
canActivate: [LoadMicroFrontendGuard],
13+
component: BookingsHostComponent,
14+
data: {
15+
bundleUrl: 'http://localhost:4201/main.js',
16+
},
17+
},
18+
]),
19+
],
20+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
21+
})
22+
export class BookingsHostModule {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Injectable } from '@angular/core';
2+
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
3+
import { MicroFrontendRegistryService } from './micro-frontend-registry.service';
4+
5+
@Injectable({ providedIn: 'root' })
6+
export class LoadMicroFrontendGuard implements CanActivate {
7+
constructor(
8+
private microFrontendRegistryService: MicroFrontendRegistryService
9+
) {}
10+
11+
canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
12+
const bundleUrl = route.data.bundleUrl as unknown;
13+
if (!(typeof bundleUrl === 'string')) {
14+
console.error(`
15+
The LoadMicroFrontendGuard is missing information on which bundle to load.
16+
Did you forget to provide a bundleUrl: string as data to the route?
17+
`);
18+
return Promise.resolve(false);
19+
}
20+
return this.microFrontendRegistryService.loadBundle(bundleUrl);
21+
}
22+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Injectable } from '@angular/core';
2+
3+
function load(url: string): Promise<void> {
4+
return new Promise((resolve, reject) => {
5+
const script = document.createElement('script');
6+
script.src = url;
7+
script.onload = () => resolve();
8+
script.onerror = () =>
9+
reject({
10+
error: `Bundle ${url} could not be loaded`,
11+
});
12+
document.body.appendChild(script);
13+
});
14+
}
15+
16+
/**
17+
* The loading state of a bundle.
18+
*
19+
* UNKNOWN -> It has not been tried to load this bundle.
20+
* LOADING -> The loading of this bundle is currently happening.
21+
* LOADED -> The bundle has been successfully loaded.
22+
* FAILED -> The loading of the bundle failed.
23+
*/
24+
type LoadingState = 'UNKNOWN' | 'LOADING' | 'LOADED' | 'FAILED';
25+
26+
/**
27+
* This service loads bundles and keeps track of which bundles have been already loaded.
28+
* This way, it prevents errors that would occur if a bundle is loaded a second time.
29+
*/
30+
@Injectable({ providedIn: 'root' })
31+
export class MicroFrontendRegistryService {
32+
private loadingStates: Record<string, LoadingState> = {};
33+
34+
/**
35+
* Loads the given bundle if not already loaded, registering its custom elements in the browser.
36+
*
37+
* @param bundleUrl The url of the bundle, can be absolute or relative to the domain + base href.
38+
*/
39+
async loadBundle(bundleUrl: string): Promise<boolean> {
40+
if (['LOADING', 'LOADED'].includes(this.getLoadingState(bundleUrl))) {
41+
return true;
42+
}
43+
this.loadingStates[bundleUrl] = 'LOADING';
44+
const isSuccess = await load(bundleUrl)
45+
.then(() => true)
46+
.catch(() => false);
47+
this.loadingStates[bundleUrl] = isSuccess ? 'LOADED' : 'FAILED';
48+
return isSuccess;
49+
}
50+
51+
/**
52+
* Returns the loading state of the bundle.
53+
*
54+
* @param bundleUrl The url of the bundle.
55+
*/
56+
getLoadingState(bundleUrl: string): LoadingState {
57+
return this.loadingStates[bundleUrl] || 'UNKNOWN';
58+
}
59+
60+
/**
61+
* Returns if the bundle has already been loaded successfully.
62+
*
63+
* @param bundleUrl The url of the bundle.
64+
*/
65+
isBundleLoaded(bundleUrl: string): boolean {
66+
return this.getLoadingState(bundleUrl) === 'LOADED';
67+
}
68+
}

0 commit comments

Comments
 (0)