├── .editorconfig
├── .gitignore
├── LICENCE
├── README.md
├── angular.json
├── migration-guide-to-v2.md
├── migration-guide.md
├── package-lock.json
├── package.json
├── projects
└── ngx-mfe
│ ├── LICENCE
│ ├── README.md
│ ├── karma.conf.js
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ ├── lib
│ │ ├── decorators
│ │ │ ├── index.ts
│ │ │ └── track-changes.decorator.ts
│ │ ├── directives
│ │ │ ├── index.ts
│ │ │ ├── mfe-outlet.directive.spec.ts
│ │ │ └── mfe-outlet.directive.ts
│ │ ├── helpers
│ │ │ ├── delay.ts
│ │ │ ├── index.ts
│ │ │ └── load-mfe.ts
│ │ ├── injection-tokens
│ │ │ ├── index.ts
│ │ │ └── options.token.ts
│ │ ├── interfaces
│ │ │ ├── index.ts
│ │ │ ├── mfe-config.interface.ts
│ │ │ ├── ngx-mfe-options.interface.ts
│ │ │ └── remote-component.interface.ts
│ │ ├── mfe.module.ts
│ │ ├── registry
│ │ │ ├── index.ts
│ │ │ └── mfe-registry.ts
│ │ ├── services
│ │ │ ├── dynamic-component-binding.spec.ts
│ │ │ ├── dynamic-component-binding.ts
│ │ │ ├── index.ts
│ │ │ ├── remote-component-loader.spec.ts
│ │ │ ├── remote-component-loader.ts
│ │ │ ├── remote-components-cache.spec.ts
│ │ │ └── remote-components-cache.ts
│ │ └── types
│ │ │ ├── component-with-ng-module-ref.ts
│ │ │ ├── index.ts
│ │ │ ├── mfe-outlet-inputs.ts
│ │ │ └── mfe-outlet-outputs.ts
│ ├── public-api.ts
│ └── test.ts
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ └── tsconfig.spec.json
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 |
16 | # IDEs and editors
17 | /.idea
18 | .project
19 | .classpath
20 | .c9/
21 | *.launch
22 | .settings/
23 | *.sublime-workspace
24 |
25 | # IDE - VSCode
26 | .vscode/*
27 | !.vscode/settings.json
28 | !.vscode/tasks.json
29 | !.vscode/launch.json
30 | !.vscode/extensions.json
31 | .history/*
32 |
33 | # misc
34 | /.angular/cache
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Denis Khrunov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular micro-frontend library - ngx-mfe
2 |
3 | A library for working with MFE in Angular in a plugin-based approach and with Angular routing.
4 |
5 | > If you have production build issues check this [issue](https://github.com/dkhrunov/ngx-mfe/issues/7). __This issue has been fixed in version 2.0.0.__
6 |
7 | Have problems with updates? Check out the [migration guides](../../migration-guide.md).
8 |
9 | ## Contents
10 |
11 | - [Version Compliance](#version-compliance)
12 | - [Motivation](#motivation)
13 | - [Features](#features)
14 | - [Examples](#examples)
15 | - [Conventions](#conventions)
16 | - [Configuring](#configuring)
17 | - [Display MFE in HTML template / plugin-based approach](#display-mfe-in-html-template--plugin-based-approach)
18 | - [Display Angular v14 Standalone Components](#display-angular-v14-standalone-components)
19 | - [Passing Data to the MFE Component via mfeOutlet directive](#passing-data-to-the-mfe-component-via-mfeoutlet-directive)
20 | - [Load MFE by Route](#load-mfe-by-route)
21 | - [Changelog](#changelog)
22 |
23 | ## Version Compliance
24 | ngx-mfe | v1.0.0 | v1.0.5 | v2.0.0 | v3.0.0 |
25 | --------------------------------------| ------- | ------- | ------- | ------- |
26 | Angular | v12.0.0 | v13.0.0 | v13.0.0 | v14.0.0 |
27 | @angular-architects/module-federation | v12.0.0 | v14.0.0 | v14.0.0 | v14.3.0 |
28 |
29 | **Since v15.0.0 version of ngx-mfe library is compatible with Angular version**
30 |
31 | ## Motivation
32 |
33 | When Webpack 5 came along and the Module Federation plugin, it became possible to separately compile and deploy code for front-end applications, thereby breaking up a monolithic front-end application into separate and independent **M**icro**F**ront**E**nd (MFE) applications.
34 |
35 | The **ngx-mfe** is an extension of the functionality of the [@angular-architects/module-federation](https://www.npmjs.com/package/@angular-architects/module-federation). Using @angular-architects/module-federation you could only upload one micro-frontend per page (in the Routing), this limitation was the main reason for the creation of this library.
36 |
37 | The key feature of the **ngx-mfe** library is ability to work with micro-frontends directly in the HTML template using a plugin-based approach. You can load more than one micro-frontend per page.
38 |
39 | > You can use both **ngx-mfe** and **@angular-architects/module-federation** libs together in the same project.
40 |
41 | ## Features
42 |
43 | 🔥 Load multiple micro-frontend directly from an HTML template with the ability to display a loader component during loading and a fallback component when an error occurs during loading and/or rendering of the mfe component.
44 |
45 | 🔥 Easy to use, just declare structural directive `*mfeOutlet` in your template.
46 |
47 | 🔥 Supports Angular Standalone Components.
48 |
49 | 🔥 More convenient way to load MFE via Angular Routing.
50 |
51 | 🔥 It's easy to set up different remoteEntryUrl MFEs for different builds (dev/prod/etc).
52 |
53 | ## Examples
54 |
55 | - [Example of an application using ngx-mfe v1.](https://github.com/dkhrunov/ngx-mfe-test/tree/lesson_4)
56 | - [Example of an application using ngx-mfe v2.](https://github.com/dkhrunov/ngx-mfe-test/tree/update-to-ngx-mfe-v2)
57 | - [Example of an application using ngx-mfe v3 with Angular 14 Standalone Components.](https://github.com/dkhrunov/ngx-mfe-test)
58 | - [Here you can find a series of articles about Micro-frontends/Module Federation and a step-by-step guide to building an application with Micro-frontends.](https://dekh.medium.com/angular-micro-frontend-architecture-part-1-3-the-concept-of-micro-frontend-architecture-2ff56a5ac264)
59 |
60 | ## Conventions
61 |
62 | 1. To display a standalone MFE component, you only need to __the component file itself__.
63 |
64 | > A standalone component is a component that does not have any dependencies provided or imported in the module where that component is declared.
65 | >
66 | > Since Angular v14 standalone component it is component that marked with `standalone: true` in `@Component({...})` decorator.
67 |
68 | When you display a standalone MFE component through `[mfeOutlet]` directive you must omit `[mfeOutletModule]` input.
69 |
70 | ```typescript
71 | // Standalone Component - standalone.component.ts
72 | import { Component } from '@angular/core';
73 | import { CommonModule } from '@angular/common';
74 |
75 | @Component({
76 | selector: 'app-standalone',
77 | standalone: true,
78 | imports: [CommonModule],
79 | template: `
Standalone component works!
`,
80 | styles: [],
81 | })
82 | export class StandaloneComponent {}
83 | ```
84 |
85 | ```typescript
86 | // dashboard-mfe webpack.config
87 | {
88 | new ModuleFederationPlugin({
89 | name: 'dashboard-mfe',
90 | filename: 'remoteEntry.js',
91 | exposes: {
92 | StandaloneComponent: 'apps/dashboard-mfe/src/app/standalone.component.ts',
93 | },
94 | [...]
95 | });
96 | }
97 | ```
98 |
99 | ```html
100 |
101 |
105 |
106 | ```
107 |
108 | 2. To display an MFE component with dependencies in the module where the component was declared, you must expose both __the component file and the module file__ from ModuleFederationPlugin.
109 |
110 | > This approach is widely used and recommended.
111 |
112 | When you display this type of MFE component with the `[mfeOutlet]` directive, you must declare an input `[mfeOutletModule]` with the value of the exposed module name.
113 |
114 | 3. The file key of an exposed Module or Component (declared in the ModuleFederationPlugin in the 'expose' property) must match the class name of that file.
115 |
116 | For the plugin-based approach, when loads MFE using `[mfeOutlet]` directive you must declare Component in the exposed Module and the Component name must match the file key of an exposed Component class.
117 |
118 | ```typescript
119 | // webpack.config
120 | {
121 | new ModuleFederationPlugin({
122 | name: 'dashboard-mfe',
123 | filename: 'remoteEntry.js',
124 | exposes: {
125 | // EntryModule is the key of the entry.module.ts file and corresponds to the exported EntryModule class from this file.
126 | EntryModule: 'apps/dashboard-mfe/src/app/remote-entry/entry.module.ts',
127 | // the EntryComponent is key of file entry.module.ts, and match to exported EntryComponent class from that file.
128 | EntryComponent: 'apps/dashboard-mfe/src/app/remote-entry/entry.component.ts',
129 | },
130 | [...]
131 | });
132 | }
133 | ```
134 |
135 | > If the name of Module doesn't match, you can specify a custom name for this Module in the @Input() property `mfeOutletOptions = { componentName: 'CustomName' }` of `[mfeOutlet]` directive, and pass `{ moduleName: 'CustomName' }` options to the `loadMfe()` function;
136 |
137 | > If the name of Component doesn't match, you can specify a custom name for this Component in the @Input() property `mfeOutletOptions = { componentName: 'CustomName' }` of `[mfeOutlet]` directive, and pass `{ moduleName: 'CustomName' }` options to the `loadMfe()` function;
138 |
139 | 4. You must follow the rule that only one Component must be declared for an exposed Module. This is known as SCAM (**S**ingle **C**omponent **A**ngular **M**odule) pattern.
140 |
141 | ## Configuring
142 |
143 | Add the **ngx-mfe** library to a shared property in the ModuleFederationPlugin inside webpack.config.js file for each application in your workspace.
144 |
145 | ```typescript
146 | module.exports = {
147 | [...]
148 | plugins: [
149 | [...]
150 | new ModuleFederationPlugin({
151 | remotes: {},
152 | shared: share({
153 | [...]
154 | "ngx-mfe": {
155 | singleton: true,
156 | strictVersion: true,
157 | requiredVersion: 'auto',
158 | includeSecondaries: true
159 | },
160 | ...sharedMappings.getDescriptors(),
161 | }),
162 | library: {
163 | type: 'module'
164 | },
165 | }),
166 | [...]
167 | ],
168 | [...]
169 | };
170 | ```
171 |
172 | To configure this library, you must import `MfeModule.forRoot(options: NgxMfeOptions)` into the root module of the Host app(s) and the root module of the Remote apps in order for Remote to work correctly when running as a standalone application:
173 |
174 | > For feature modules just import `MfeModule` without options, where, you may need the functionality of the library, for example, the `MfeOutlet` directive.
175 |
176 | For core / app module:
177 | ```typescript
178 | @NgModule({
179 | imports: [
180 | MfeModule.forRoot({
181 | mfeConfig: {
182 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
183 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
184 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
185 | },
186 | preload: ['loaders-mfe', 'fallbacks-mfe'],
187 | loader: {
188 | app: 'loaders',
189 | module: 'SpinnerModule',
190 | component: 'SpinnerComponent',
191 | },
192 | loaderDelay: 500,
193 | fallback: {
194 | app: 'fallbacks',
195 | module: 'MfeFallbackModule',
196 | component: 'MfeFallbackComponent',
197 | },
198 | }),
199 | ],
200 | })
201 | export class AppModule {}
202 | ```
203 |
204 | For feature module:
205 | ```typescript
206 | @NgModule({
207 | imports: [
208 | MfeModule,
209 | ],
210 | })
211 | export class Feature1Module {}
212 | ```
213 |
214 | ### List of all available options:
215 |
216 | - **mfeConfig**
217 |
218 | ----------------
219 |
220 | **Sync variant of providing mfeConfig:**
221 |
222 | ----------------
223 |
224 | object where **key** is micro-frontend app name specified in `ModuleFederationPlugin` (webpack.config.js) and **value** is remoteEntryUrl string. All data will be sets to [MfeRegistry](https://github.com/dkhrunov/ngx-mfe/blob/master/projects/ngx-mfe/src/lib/registry/mfe-registry.ts).
225 |
226 | **Key** it's the name same specified in webpack.config.js of MFE (Remote) in option name in `ModuleFederationPlugin`.
227 |
228 | **Value** set the following pattern: `{url}/{remoteEntrypointFilename}`.
229 |
230 | - `url` is the url where the remote application is hosted.
231 |
232 | - `remoteEntrypointFilename` is the filename supplied in the remote's webpack configuration.
233 |
234 | Example
235 |
236 | > (Deprecated from v15.1.0) You can get `MfeRegistry` from DI :
237 | >
238 | > ```typescript
239 | > class AppComponent {
240 | >
241 | > constructor(public mfeRegistry: MfeRegistry) {}
242 | > }
243 | > ```
244 |
245 | You can even get instace of `MfeRegistry` like this:
246 |
247 | ```typescript
248 | const mfeRegistry: MfeRegistry = MfeRegistry.instace;
249 | ```
250 |
251 | ----------------
252 |
253 | **Async variant of providing mfeConfig:**
254 |
255 | ----------------
256 |
257 | > NOTE: The application will wait for initialization and completes when the promise resolves or the observable completes.
258 | >
259 | > Because under the hood used `APP_INITIALIZER` injection token with useFactory that returns Observale or Promise. [More about `APP_INITIALIZER`](https://angular.io/api/core/APP_INITIALIZER)
260 |
261 |
262 | Also you can provide mfeConfig with loading it from external resource as `Observale` or `Promise`, for this you should provide this type of object:
263 |
264 | ```typescript
265 | type NgxMfeAsyncConfig = {
266 | /**
267 | * A function to invoke to load a `MfeConfig`. The function is invoked with
268 | * resolved values of `token`s in the `deps` field.
269 | */
270 | useLoader: (...deps: any[]) => Observable | Promise;
271 | /**
272 | * A list of `token`s to be resolved by the injector. The list of values is then
273 | * used as arguments to the `useLoader` function.
274 | */
275 | deps?: any[];
276 | };
277 | ```
278 |
279 | For example:
280 |
281 | ```typescript
282 | mfeConfig: {
283 | useLoader: (http: HttpClient): Observable =>
284 | http.get('/manifest.json'),
285 | deps: [HttpClient]
286 | },
287 | ```
288 |
289 |
290 | - **preload** (Optional) - a list of micro-frontend names, their bundles (remoteEntry.js) will be loaded and saved in the cache when the application starts.
291 |
292 | Next options are only works in plugin-based approach with `MfeOutletDirective`:
293 |
294 | - **loaderDelay** (Optional) - Specifies the minimum loader display time in ms. This is to avoid flickering when the micro-frontend loads very quickly.
295 |
296 | *By default is 0.*
297 |
298 | - **loader** (Optional) - Displayed when loading the micro-frontend. Implements the `RemoteComponent` interface.
299 |
300 | *Example:*
301 | ```typescript
302 | // Globally uses the "SpinnerComponent" loader component declared in the "SpinnerModule" of the app "loaders".
303 | loader: {
304 | app: 'loaders',
305 | module: 'SpinnerModule',
306 | component: 'SpinnerComponent',
307 | },
308 | ```
309 |
310 | > For better UX, add loader micro-frontends to the `preload`.
311 |
312 | - **fallback** (Optional) - Displayed when loading or compiling a micro-frontend with an error. Implements the `RemoteComponent` interface.
313 |
314 | *Example:*
315 | ```typescript
316 | // Globally uses the "MfeFallbackComponent" fallback component declared in the "MfeFallbackModule" of the app "fallbacks".
317 | fallback: {
318 | app: 'fallbacks',
319 | module: 'MfeFallbackModule',
320 | component: 'MfeFallbackComponent',
321 | },
322 | ```
323 |
324 | > For better UX, add fallback micro-frontends to the `preload`.
325 |
326 | You can get all configured options by injecting `NGX_MFE_OPTIONS` by DI:
327 |
328 | ```typescript
329 | class AppComponent {
330 | constructor(@Inject(NGX_MFE_OPTIONS) public options: NgxMfeOptions) {}
331 | }
332 | ```
333 |
334 | ## Display MFE in HTML template / plugin-based approach
335 |
336 | This approach allows us to load micro-frontends directly from HTML.
337 |
338 | The advantages of this approach are that we can display several MFEs at once on the same page, even display several of the same MFEs.
339 |
340 | > More about plugin-based approach [here](https://dekh.medium.com/angular-micro-frontend-architecture-part-3-3-mfe-plugin-based-approach-f36dc9849b0).
341 |
342 | > Full code of this example can be found at https://github.com/dkhrunov/ngx-mfe-test.
343 |
344 | Example app:
345 |
346 | 
347 |
348 | An example webpack.config.js that exposes the "MfeTestComponent" (brown border in the screenshot above):
349 |
350 | ```js
351 | // webpack.config.js
352 | return {
353 | [...]
354 | resolve: {
355 | alias: sharedMappings.getAliases(),
356 | },
357 | plugins: [
358 | new ModuleFederationPlugin({
359 | name: 'test',
360 | exposes: {
361 | MfeTestModule: 'apps/test/src/app/mfe-test/mfe-test.module.ts',
362 | MfeTestComponent: 'apps/test/src/app/mfe-test/mfe-test.component.ts',
363 | },
364 | filename: 'remoteEntry',
365 | shared: share({ ... }),
366 | }),
367 | sharedMappings.getPlugin(),
368 | ],
369 | };
370 | ```
371 |
372 | 1. Just display the component "MfeTestComponent" inside other MFE component "Form" from "address-form" app:
373 |
374 | One variant:
375 | ```html
376 |
381 |
382 | ```
383 |
384 | Other variant:
385 | ```html
386 |
393 |
394 | ```
395 |
396 | > These two examples are equal and display the MFE "MfeTestComponent".
397 |
398 | 2. You can pass/bind `@Input` and `@Output` props to MFE component:
399 |
400 | ```html
401 |
402 |
411 | ```
412 |
413 | ```typescript
414 | // form.component.ts file
415 | @Component({
416 | selector: 'app-form',
417 | templateUrl: './form.component.html',
418 | styleUrls: ['./form.component.scss'],
419 | changeDetection: ChangeDetectionStrategy.OnPush,
420 | })
421 | export class FormComponent {
422 | [...]
423 | // timer emits after 1 second, then every 2 seconds
424 | public readonly text$: Observable = timer(1000, 2000);
425 |
426 | // on click log to console event
427 | public onClick(event: MouseEvent): void {
428 | console.log('clicked', event);
429 | }
430 | [...]
431 | }
432 | ```
433 |
434 | > If you try to bind a @Output() property that is not in the component, then an error will fall into the console:
435 | > "Output **someOutput** is not output of **SomeComponent**."
436 | >
437 | > If you try to pass a non-function, then an error will fall into the console:
438 | > "Output **someOutput** must be a function."
439 |
440 | 3. To override the default loader delay, configured in `MfeModule.forRoot({ ... })`, provide custom number in ms to property `loaderDelay`:
441 |
442 | ```html
443 |
451 | ```
452 |
453 | 4. To override the default loader and fallback MFE components, configured in `MfeModule.forRoot({ ... })`, specify content with `TemplateRef`, pass it to the appropriate properties `loader` and `fallback`:
454 |
455 | ```html
456 |
465 |
466 |
467 | loading...
468 |
469 |
470 |
471 | Ooops! Something went wrong
472 |
473 | ```
474 |
475 | ```html
476 |
477 |
483 |
484 |
485 |
492 |
493 |
494 | ```
495 |
496 | 6. You can also provide a custom injector for a component like this:
497 |
498 | ```html
499 |
505 | ```
506 |
507 | ## Display Angular v14 Standalone components
508 |
509 | Example app:
510 |
511 | 
512 |
513 | An example webpack.config.js that exposes the "StandaloneComponent" (green border in the screenshot above):
514 |
515 | ```js
516 | // webpack.config.js
517 | return {
518 | [...]
519 | resolve: {
520 | alias: sharedMappings.getAliases(),
521 | },
522 | plugins: [
523 | new ModuleFederationPlugin({
524 | name: 'test',
525 | exposes: {
526 | [...]
527 | StandaloneComponent: 'apps/test/src/app/standalone/standalone.component.ts',
528 | },
529 | filename: 'remoteEntry',
530 | shared: share({ ... }),
531 | }),
532 | sharedMappings.getPlugin(),
533 | ],
534 | };
535 | ```
536 |
537 | ```typescript
538 | // standalone.component.ts
539 |
540 | import { Component } from '@angular/core';
541 | import { CommonModule } from '@angular/common';
542 |
543 | @Component({
544 | selector: 'app-standalone',
545 | standalone: true,
546 | imports: [CommonModule],
547 | template: ` Standalone component works!
`,
548 | styles: [],
549 | })
550 | export class StandaloneComponent {}
551 | ```
552 |
553 | ```html
554 |
555 | [...]
556 | Angular v14 Standalone component loaded as MFE:
557 |
561 | ```
562 |
563 | ## Passing Data to the MFE Component via mfeOutlet directive
564 |
565 | After using this library for some time, as the author of this library, I came to the conclusion that using @Inputs and @Outputs of an MFE component through the `[mfeOutletInputs]` `[mfeOutletOutputs]` properties is not the best practice. Try to make your MFE components as independent as possible from the external environment. But if you still have to pass some values to the component, you can do it in two ways:
566 |
567 | 1. As I wrote above through the properties `[mfeOutletInputs]` `[mfeOutletOutputs]`
568 |
569 | component.html:
570 | ```html
571 |
578 |
579 | ```
580 |
581 | component.ts
582 | ```typescript
583 | @Component({ ... })
584 | export class Component {
585 | public text$ = new BehaviorSubject('Test string');
586 |
587 | constructor() { }
588 |
589 | public onClick(bool: MouseEvent): void {
590 | console.log('login', bool);
591 | }
592 | }
593 | ```
594 |
595 | 2. The second way is to create a new injector and add the necessary data for the MFE component to it. The `[mfeOutlet]` directive has the `[mfeOutletInjector]` property through which you can pass the desired injector, when the component is created, the previously passed injector in the `[mfeOutletInjector]` property will be used instead of the current injector.
596 |
597 | component.html:
598 | ```html
599 |
605 |
606 | ```
607 |
608 | component.ts
609 | ```typescript
610 | @Component({ ... })
611 | export class Component {
612 | public readonly testComponentInjector: Injector;
613 |
614 | constructor(private readonly _injector: Injector) {
615 | this.testComponentInjector = Injector.create({
616 | parent: this._injector,
617 | providers: [
618 | {
619 | provide: TEST_DATA,
620 | useValue: data,
621 | },
622 | ],
623 | });
624 | }
625 | }
626 | ```
627 |
628 | ## Load MFE by Route
629 |
630 | To use micro-frontends in Routing, you must import and apply the helper function called `loadMfe`, like in the example below:
631 |
632 | ```typescript
633 | import { NgModule } from '@angular/core';
634 | import { RouterModule, Routes } from '@angular/router';
635 | import { loadMfe } from '@dkhrunov/ng-mfe';
636 |
637 | const routes: Routes = [
638 | {
639 | path: 'dashboard',
640 | loadChildren: () => loadMfe('dashboard-mfe', 'EntryModule'),
641 | },
642 | ];
643 |
644 | @NgModule({
645 | imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })],
646 | exports: [RouterModule],
647 | })
648 | export class AppRoutingModule {}
649 | ```
650 |
651 | ## Changelog
652 |
653 | ### Changes in __v2.1.0__
654 | Fixed:
655 | - Fix error, if the fallback is also unavailable, then simply clear the view;
656 |
657 | Refactored:
658 | - Renamed `MfeService` to `RemoteComponentLoader`;
659 | - Renamed `MfeComponentsCache` to `RemoteComponentsCache`;
660 | - Renamed `ModularRemoteComponent` type to `RemoteComponentWithModule`;
661 | - Wrapped to `ngZone.runOutside` the `loadMfe` function calls inside the `RemoteComponentLoader`;
662 | - Added new type `ComponentWithNgModuleRef`, that holds component class `Type` and `NgModuleRef`;
663 | - Changed cached value for `RemoteComponentWithModule` from `ComponentFactory` to `ComponentWithNgModuleRef`;
664 | - In `RemoteComponentLoader` (old name `MfeService`) renamed function `loadModularComponent` to `loadComponentWithModule`
665 | - Changed return type of method `loadComponentWithModule` inside class `RemoteComponentLoader` from `Promise>` to `Promise>`;
666 |
667 | ### Changes in __v2.0.0__ (_Breaking changes_)
668 |
669 | __Why has the API changed?__ - The problem is that when you use the `[mfeOutlet]` directive [issue](https://github.com/dkhrunov/ngx-mfe/issues/7), it tries to find the component inside the compiled module by name (as a string), but in runtime the class name will be optimized and replaced with a short character. For example, you have a class `TestComponent`, it can be changed to the class name `a` and this causes this error.
670 |
671 | #### General:
672 | - To properly use the plugin-based approach in a micro-frontend architecture, or simply if you are use `[mfeOutlet]` directive, you must now expose both the component file and module file in which the component is declared to the ModuleFederationPlugin.
673 |
674 | __Rarerly :__ or, if your micro-frontend component is standalone (a standalone component is a component that does not have any dependencies declared or imported in the module where that component is declared), then it is sufficient to provide just that component file to the ModuleFederationPlugin;
675 |
676 | - Now __ngx-mfe__ does not use `Micro-frontend string` (or anouther name `MFE string`) is a kebab-case style string and matches the pattern `"mfe-app-name/exposed-file-name"` (__it was used until version 2.0.0__);
677 |
678 | - `MFE string` has been replaced by a new type `RemoteComponent`;
679 |
680 | - The `validateMfe` function has been removed (__it was used until version 2.0.0__);
681 |
682 | - The `loader` and `fallback` properties in the `NgxMfeOptions` has been changed from `MFE string` to `RemoteComponent` type:
683 |
684 | Before v2.0.0:
685 | ```typescript
686 | @NgModule({
687 | declarations: [AppComponent],
688 | imports: [
689 | BrowserModule,
690 | BrowserAnimationsModule,
691 | MfeModule.forRoot({
692 | mfeConfig: {
693 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
694 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
695 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
696 | },
697 | loader: 'loaders/spinner',
698 | fallback: 'fallbacks/mfe-fallback',
699 | }),
700 | ],
701 | bootstrap: [AppComponent],
702 | })
703 | export class AppModule {}
704 | ```
705 |
706 | Since v2.0.0:
707 | ```typescript
708 | @NgModule({
709 | declarations: [AppComponent],
710 | imports: [
711 | BrowserModule,
712 | BrowserAnimationsModule,
713 | MfeModule.forRoot({
714 | mfeConfig: {
715 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
716 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
717 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
718 | },
719 | loader: {
720 | app: 'loaders',
721 | module: 'SpinnerModule',
722 | component: 'SpinnerComponent',
723 | },
724 | fallback: {
725 | app: 'fallbacks',
726 | module: 'MfeFallbackModule',
727 | component: 'MfeFallbackComponent',
728 | },
729 | }),
730 | ],
731 | bootstrap: [AppComponent],
732 | })
733 | export class AppModule {}
734 | ```
735 |
736 | - Removed `moduleName` property from `LoadMfeOptions` type;
737 | - Now, wherever you need to specify the name of the exposed file through the config in the webpack.config in the ModuleFederationPlugin, you must specify exactly the same name as in the config itself, the kebab-style name was used earlier.
738 | ```javascript
739 | // webpack.config.js
740 | exposes: {
741 | // LoginModule name of the exposed file login.module.ts
742 | LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts',
743 | },
744 | ```
745 |
746 | Before v2.0.0:
747 | ```typescript
748 | loadMfe('auth-mfe/login-module')
749 | ```
750 |
751 | Since v2.0.0:
752 | ```typescript
753 | loadMfe('auth-mfe' 'LoginModule')
754 | ```
755 |
756 | #### LoadMfe function:
757 | - Arguments changed in `LoadMfe` function:
758 |
759 | Before v2.0.0:
760 | ```typescript
761 | async function loadMfe(mfeString: string, options?: LoadMfeOptions): Promise> {}
762 | ```
763 |
764 | Since v2.0.0:
765 | ```typescript
766 | async function loadMfe(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise> {}
767 | ```
768 | - `remoteApp` - is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the __name__ property;
769 | - `exposedFile` - is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the __exposes__ property;
770 |
771 | #### MfeOutlet directive:
772 | - Since the `Mfe string` has been removed from the library, the API of `[mfeOutlet]` directive has changed:
773 | 1. `mfeOutletLoader` and `mfeOutletFallback` now accept only `TemplateRef`, more details below.
774 | 2. To load a standalone component, you must specify the following details: `mfeOutlet` with the name of the application, `mfeOutletComponent` with the name of the component's open file from the ModuleFederationPlugin in webpack.config. But to load a non-standalone component, you must additionally specify `mfeOutletModule` with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config.
775 | 3.
776 | - `@Input('mfeOutletOptions')' options` changed type from `MfeComponentFactoryResolverOptions` to `LoadMfeOptions`;
777 | - `@Input('mfeOutletLoader')' loader` and `@Input('mfeOutletFallback') fallback` now accept only `TemplateRef`, not `TemplateRef` or `Mfe string`. But you can still use micro-frontend component for `loader` and `fallback` in the `[mfeOutlet]`, like in the example below:
778 |
779 | ```html
780 |
781 |
787 |
788 |
789 |
790 |
791 |
792 |
799 |
800 |
801 |
802 |
803 |
809 |
810 |
811 |
812 |
813 | loading...
814 |
815 | ```
816 |
817 | #### MfeComponentFactoryResolver:
818 | - The `MfeComponentFactoryResolver` has been replaced with `MfeService` and the API has been changed;
819 | - The `MfeComponentFactoryResolverOptions` type has been removed;
820 |
821 | #### MfeComponentCache
822 | - Now the `MfeComponentCache` not only saves `ComponentFactory` but also `Type`;
823 | - In version 2.1.0 `ComponentFactory` was replaced to `ComponentWithNgModuleRef`;
824 |
825 | #### DynamicComponentBinding
826 | - The `bindInputs()` and `bindOutputs()` methods now require `ComponentRef` in the first argument, `MfeOutletInputs`/`MfeOutletOutputs` are method dependent in the second, and the third argument has been removed;
827 | - The `DynamicComponentInputs` and `DynamicComponentOutputs` types have been removed because these types are replaced in `bindInputs()` and `bindOutputs()` respectively by the `ComponentRef` type;
828 | - The `validateInputs()` method has been removed;
829 | - The `validateOutputs()` method is now private;
830 |
831 | ---------------
832 |
833 | ### Changes in __v1.1.0__:
834 |
835 | - Deleted the `loadMfeComponent` helper function;
836 | - Deleted the `parseMfeString` helper function;
837 | - Renamed the `loadMfeModule` helper function to `loadMfe` and added optional parameter `options: LoadMfeOptions`. `LoadMfeOptions` has property a `moduleName`, that sets a custom name for the Module class within the opened file, and has `type` that specify type of Module Federation;
838 | - Renamed the `MfeService` to `MfeComponentFactoryResolver`;
839 | - `MfeComponentFactoryResolver` has the same method as `MfeService`, but now it can accepts an optional `options: MfeComponentFactoryResolver` parameter. This parameter extends `LoadMfeOptions` type, added a `componentName` parameter, that sets a custom name for the Component class.
840 | - Added new Input prop to the `MfeOutletDirective` - `options: MfeComponentFactoryResolver`, this parameter provided to `resolveComponentFactory` method of the `MfeComponentFactoryResolver` when resolving the component factory of MFE.
841 | - Since **v1.1.0** you don't need to expose from `ModuleFederationPlugin` for plugin-based approach both Module and Component, just specify the Module file.
842 |
843 | The exposed Module key must match the name of the exposed module without the 'Module' suffix. Also, if the name doesn't match, you can specify a custom Module name in the options `{ moduleName: 'CustomName' }` in the property `mfeOutletOptions` inside `MfeOutletDirective` and in the options parameter of the `loadMfe` helper function.
844 |
845 | For the plugin-based approach, when loads MFE using `MfeOutletDirective` you must declare Component in the exposed Module and the Component name must match the exposed Module key without suffix 'Component'. Also, if the name doesn't match, you can specify a custom Component name in the Input property `mfeOutletOptions = { componentName: 'CustomName' }`;
846 |
847 | ---------------
848 |
849 | ### Changes in __v1.0.8__:
850 |
851 | - `IMfeModuleRootOptions` interface renamed to `NgxMfeOptions`;
852 | - Property `delay` in the `NgxMfeOptions` renamed to `loaderDelay`;
853 | - `OPTIONS` injection token renamed to `NGX_MFE_OPTIONS`;
854 |
855 | ---------------
856 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "ngx-mfe": {
7 | "projectType": "library",
8 | "root": "projects/ngx-mfe",
9 | "sourceRoot": "projects/ngx-mfe/src",
10 | "prefix": "lib",
11 | "architect": {
12 | "build": {
13 | "builder": "@angular-devkit/build-angular:ng-packagr",
14 | "options": {
15 | "project": "projects/ngx-mfe/ng-package.json"
16 | },
17 | "configurations": {
18 | "production": {
19 | "tsConfig": "projects/ngx-mfe/tsconfig.lib.prod.json"
20 | },
21 | "development": {
22 | "tsConfig": "projects/ngx-mfe/tsconfig.lib.json"
23 | }
24 | },
25 | "defaultConfiguration": "production"
26 | },
27 | "test": {
28 | "builder": "@angular-devkit/build-angular:karma",
29 | "options": {
30 | "main": "projects/ngx-mfe/src/test.ts",
31 | "tsConfig": "projects/ngx-mfe/tsconfig.spec.json",
32 | "karmaConfig": "projects/ngx-mfe/karma.conf.js"
33 | }
34 | }
35 | }
36 | }
37 | },
38 | "cli": {
39 | "analytics": "f4cceced-74e6-402c-ba25-05b16ec1da51"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/migration-guide-to-v2.md:
--------------------------------------------------------------------------------
1 | # Migration Guide to v2
2 |
3 | __Why has the API changed?__ - The problem is that when you use the `[mfeOutlet]` directive [issue](https://github.com/dkhrunov/ngx-mfe/issues/7), it tries to find the component inside the compiled module by name (as a string), but in runtime the class name will be optimized and replaced with a short character. For example, you have a class `TestComponent`, it can be changed to the class name `a` and this causes this error.
4 |
5 | ## General:
6 | - To properly use the plugin-based approach in a micro-frontend architecture, or simply if you are use `[mfeOutlet]` directive, you must now expose both the component file and module file in which the component is declared to the ModuleFederationPlugin.
7 |
8 | __Rarerly :__ or, if your micro-frontend component is standalone (a standalone component is a component that does not have any dependencies declared or imported in the module where that component is declared), then it is sufficient to provide just that component file to the ModuleFederationPlugin;
9 |
10 | - Now __ngx-mfe__ does not use `Micro-frontend string` (or anouther name `MFE string`) is a kebab-case style string and matches the pattern `"mfe-app-name/exposed-file-name"` (__it was used until version 2.0.0__);
11 |
12 | - `MFE string` has been replaced by a new type `RemoteComponent`;
13 |
14 | - The `validateMfe` function has been removed (__it was used until version 2.0.0__);
15 |
16 | - The `loader` and `fallback` properties in the `NgxMfeOptions` has been changed from `MFE string` to `RemoteComponent` type:
17 |
18 | Before v2.0.0:
19 | ```typescript
20 | @NgModule({
21 | declarations: [AppComponent],
22 | imports: [
23 | BrowserModule,
24 | BrowserAnimationsModule,
25 | MfeModule.forRoot({
26 | mfeConfig: {
27 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
28 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
29 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
30 | },
31 | loader: 'loaders/spinner',
32 | fallback: 'fallbacks/mfe-fallback',
33 | }),
34 | ],
35 | bootstrap: [AppComponent],
36 | })
37 | export class AppModule {}
38 | ```
39 |
40 | Since v2.0.0:
41 | ```typescript
42 | @NgModule({
43 | declarations: [AppComponent],
44 | imports: [
45 | BrowserModule,
46 | BrowserAnimationsModule,
47 | MfeModule.forRoot({
48 | mfeConfig: {
49 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
50 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
51 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
52 | },
53 | loader: {
54 | app: 'loaders',
55 | module: 'SpinnerModule',
56 | component: 'SpinnerComponent',
57 | },
58 | fallback: {
59 | app: 'fallbacks',
60 | module: 'MfeFallbackModule',
61 | component: 'MfeFallbackComponent',
62 | },
63 | }),
64 | ],
65 | bootstrap: [AppComponent],
66 | })
67 | export class AppModule {}
68 | ```
69 |
70 | - Removed `moduleName` property from `LoadMfeOptions` type;
71 | - Now, wherever you need to specify the name of the exposed file through the config in the webpack.config in the ModuleFederationPlugin, you must specify exactly the same name as in the config itself, the kebab-style name was used earlier.
72 | ```javascript
73 | // webpack.config.js
74 | exposes: {
75 | // LoginModule name of the exposed file login.module.ts
76 | LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts',
77 | },
78 | ```
79 |
80 | Before v2.0.0:
81 | ```typescript
82 | loadMfe('auth-mfe/login-module')
83 | ```
84 |
85 | Since v2.0.0:
86 | ```typescript
87 | loadMfe('auth-mfe' 'LoginModule')
88 | ```
89 |
90 | ## LoadMfe function:
91 | - Arguments changed in `LoadMfe` function:
92 |
93 | Before v2.0.0:
94 | ```typescript
95 | async function loadMfe(mfeString: string, options?: LoadMfeOptions): Promise> {}
96 | ```
97 |
98 | Since v2.0.0:
99 | ```typescript
100 | async function loadMfe(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise> {}
101 | ```
102 | - `remoteApp` - is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the __name__ property;
103 | - `exposedFile` - is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the __exposes__ property;
104 |
105 | ## MfeOutlet directive:
106 | - Since the `Mfe string` has been removed from the library, the API of `[mfeOutlet]` directive has changed:
107 | 1. `mfeOutletLoader` and `mfeOutletFallback` now accept only `TemplateRef`, more details below.
108 | 2. To load a standalone component, you must specify the following details: `mfeOutlet` with the name of the application, `mfeOutletComponent` with the name of the component's open file from the ModuleFederationPlugin in webpack.config. But to load a non-standalone component, you must additionally specify `mfeOutletModule` with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config.
109 | 3.
110 | - `@Input('mfeOutletOptions')' options` changed type from `MfeComponentFactoryResolverOptions` to `LoadMfeOptions`;
111 | - `@Input('mfeOutletLoader')' loader` and `@Input('mfeOutletFallback') fallback` now accept only `TemplateRef`, not `TemplateRef` or `Mfe string`. But you can still use micro-frontend component for `loader` and `fallback` in the `[mfeOutlet]`, like in the example below:
112 |
113 | ```html
114 |
115 |
121 |
122 |
123 |
124 |
125 |
126 |
133 |
134 |
135 |
136 |
137 |
143 |
144 |
145 |
146 |
147 | loading...
148 |
149 | ```
150 |
151 | ## MfeComponentFactoryResolver:
152 | - The `MfeComponentFactoryResolver` has been replaced with `MfeService` and the API has been changed;
153 | - The `MfeComponentFactoryResolverOptions` type has been removed;
154 |
155 | ## MfeComponentCache
156 | - Now the `MfeComponentCache` not only saves `ComponentFactory` but also `Type`;
157 | - In version 2.1.0 `ComponentFactory` was replaced to `ComponentWithNgModuleRef`;
158 |
159 | ## DynamicComponentBinding
160 | - The `bindInputs()` and `bindOutputs()` methods now require `ComponentRef` in the first argument, `MfeOutletInputs`/`MfeOutletOutputs` are method dependent in the second, and the third argument has been removed;
161 | - The `DynamicComponentInputs` and `DynamicComponentOutputs` types have been removed because these types are replaced in `bindInputs()` and `bindOutputs()` respectively by the `ComponentRef` type;
162 | - The `validateInputs()` method has been removed;
163 | - The `validateOutputs()` method is now private;
--------------------------------------------------------------------------------
/migration-guide.md:
--------------------------------------------------------------------------------
1 | # Migration Guides
2 |
3 | I'm trying to make as few breaking changes as possible. However, sometimes I need to break backwards compatibility in order to add a new feature or fix a bug.
4 |
5 | - [From version 1 and 2](./migration-guide-to-v2.md.md)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-mfe",
3 | "version": "15.1.0",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "GitHub",
7 | "url": "https://github.com/dkhrunov/ngx-mfe"
8 | },
9 | "author": {
10 | "name": "Denis Khrunov",
11 | "email": "therealpanda98@gmail.com"
12 | },
13 | "keywords": [
14 | "microfrontend",
15 | "mfe",
16 | "angular-microfrontend",
17 | "angular-mfe",
18 | "microcomponent",
19 | "angular"
20 | ],
21 | "scripts": {
22 | "ng": "ng",
23 | "start": "ng serve",
24 | "build": "ng build",
25 | "watch": "ng build --watch --configuration development",
26 | "test": "ng test"
27 | },
28 | "private": true,
29 | "dependencies": {
30 | "@angular-architects/module-federation": "^19.0.3",
31 | "@angular/animations": "^19.2.5",
32 | "@angular/common": "^19.2.5",
33 | "@angular/compiler": "^19.2.5",
34 | "@angular/core": "^19.2.5",
35 | "@angular/forms": "^19.2.5",
36 | "@angular/platform-browser": "^19.2.5",
37 | "@angular/platform-browser-dynamic": "^19.2.5",
38 | "@angular/router": "^19.2.5",
39 | "rxjs": "~7.5.5",
40 | "tslib": "^2.3.0",
41 | "zone.js": "~0.15.0"
42 | },
43 | "devDependencies": {
44 | "@angular-devkit/build-angular": "^19.2.6",
45 | "@angular/cli": "^19.2.6",
46 | "@angular/compiler-cli": "^19.2.5",
47 | "@types/jasmine": "~3.8.0",
48 | "@types/node": "^12.11.1",
49 | "jasmine-core": "~3.8.0",
50 | "karma": "~6.3.0",
51 | "karma-chrome-launcher": "~3.1.0",
52 | "karma-coverage": "~2.0.3",
53 | "karma-jasmine": "~4.0.0",
54 | "karma-jasmine-html-reporter": "~1.7.0",
55 | "ng-packagr": "^19.2.1",
56 | "typescript": "^5.4.2"
57 | }
58 | }
--------------------------------------------------------------------------------
/projects/ngx-mfe/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Denis Khrunov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/projects/ngx-mfe/README.md:
--------------------------------------------------------------------------------
1 | # Angular micro-frontend library - ngx-mfe
2 |
3 | A library for working with MFE in Angular in a plugin-based approach and with Angular routing.
4 |
5 | > If you have production build issues check this [issue](https://github.com/dkhrunov/ngx-mfe/issues/7). __This issue has been fixed in version 2.0.0.__
6 |
7 | Have problems with updates? Check out the [migration guides](../../migration-guide.md).
8 |
9 | ## Contents
10 |
11 | - [Version Compliance](#version-compliance)
12 | - [Motivation](#motivation)
13 | - [Features](#features)
14 | - [Examples](#examples)
15 | - [Conventions](#conventions)
16 | - [Configuring](#configuring)
17 | - [Display MFE in HTML template / plugin-based approach](#display-mfe-in-html-template--plugin-based-approach)
18 | - [Display Angular v14 Standalone Components](#display-angular-v14-standalone-components)
19 | - [Passing Data to the MFE Component via mfeOutlet directive](#passing-data-to-the-mfe-component-via-mfeoutlet-directive)
20 | - [Load MFE by Route](#load-mfe-by-route)
21 | - [Changelog](#changelog)
22 |
23 | ## Version Compliance
24 | ngx-mfe | v1.0.0 | v1.0.5 | v2.0.0 | v3.0.0 |
25 | --------------------------------------| ------- | ------- | ------- | ------- |
26 | Angular | v12.0.0 | v13.0.0 | v13.0.0 | v14.0.0 |
27 | @angular-architects/module-federation | v12.0.0 | v14.0.0 | v14.0.0 | v14.3.0 |
28 |
29 | **Since v15.0.0 version of ngx-mfe library is compatible with Angular version**
30 |
31 | ## Motivation
32 |
33 | When Webpack 5 came along and the Module Federation plugin, it became possible to separately compile and deploy code for front-end applications, thereby breaking up a monolithic front-end application into separate and independent **M**icro**F**ront**E**nd (MFE) applications.
34 |
35 | The **ngx-mfe** is an extension of the functionality of the [@angular-architects/module-federation](https://www.npmjs.com/package/@angular-architects/module-federation). Using @angular-architects/module-federation you could only upload one micro-frontend per page (in the Routing), this limitation was the main reason for the creation of this library.
36 |
37 | The key feature of the **ngx-mfe** library is ability to work with micro-frontends directly in the HTML template using a plugin-based approach. You can load more than one micro-frontend per page.
38 |
39 | > You can use both **ngx-mfe** and **@angular-architects/module-federation** libs together in the same project.
40 |
41 | ## Features
42 |
43 | 🔥 Load multiple micro-frontend directly from an HTML template with the ability to display a loader component during loading and a fallback component when an error occurs during loading and/or rendering of the mfe component.
44 |
45 | 🔥 Easy to use, just declare structural directive `*mfeOutlet` in your template.
46 |
47 | 🔥 Supports Angular Standalone Components.
48 |
49 | 🔥 More convenient way to load MFE via Angular Routing.
50 |
51 | 🔥 It's easy to set up different remoteEntryUrl MFEs for different builds (dev/prod/etc).
52 |
53 | ## Examples
54 |
55 | - [Example of an application using ngx-mfe v1.](https://github.com/dkhrunov/ngx-mfe-test/tree/lesson_4)
56 | - [Example of an application using ngx-mfe v2.](https://github.com/dkhrunov/ngx-mfe-test/tree/update-to-ngx-mfe-v2)
57 | - [Example of an application using ngx-mfe v3 with Angular 14 Standalone Components.](https://github.com/dkhrunov/ngx-mfe-test)
58 | - [Here you can find a series of articles about Micro-frontends/Module Federation and a step-by-step guide to building an application with Micro-frontends.](https://dekh.medium.com/angular-micro-frontend-architecture-part-1-3-the-concept-of-micro-frontend-architecture-2ff56a5ac264)
59 |
60 | ## Conventions
61 |
62 | 1. To display a standalone MFE component, you only need to __the component file itself__.
63 |
64 | > A standalone component is a component that does not have any dependencies provided or imported in the module where that component is declared.
65 | >
66 | > Since Angular v14 standalone component it is component that marked with `standalone: true` in `@Component({...})` decorator.
67 |
68 | When you display a standalone MFE component through `[mfeOutlet]` directive you must omit `[mfeOutletModule]` input.
69 |
70 | ```typescript
71 | // Standalone Component - standalone.component.ts
72 | import { Component } from '@angular/core';
73 | import { CommonModule } from '@angular/common';
74 |
75 | @Component({
76 | selector: 'app-standalone',
77 | standalone: true,
78 | imports: [CommonModule],
79 | template: ` Standalone component works!
`,
80 | styles: [],
81 | })
82 | export class StandaloneComponent {}
83 | ```
84 |
85 | ```typescript
86 | // dashboard-mfe webpack.config
87 | {
88 | new ModuleFederationPlugin({
89 | name: 'dashboard-mfe',
90 | filename: 'remoteEntry.js',
91 | exposes: {
92 | StandaloneComponent: 'apps/dashboard-mfe/src/app/standalone.component.ts',
93 | },
94 | [...]
95 | });
96 | }
97 | ```
98 |
99 | ```html
100 |
101 |
105 |
106 | ```
107 |
108 | 2. To display an MFE component with dependencies in the module where the component was declared, you must expose both __the component file and the module file__ from ModuleFederationPlugin.
109 |
110 | > This approach is widely used and recommended.
111 |
112 | When you display this type of MFE component with the `[mfeOutlet]` directive, you must declare an input `[mfeOutletModule]` with the value of the exposed module name.
113 |
114 | 3. The file key of an exposed Module or Component (declared in the ModuleFederationPlugin in the 'expose' property) must match the class name of that file.
115 |
116 | For the plugin-based approach, when loads MFE using `[mfeOutlet]` directive you must declare Component in the exposed Module and the Component name must match the file key of an exposed Component class.
117 |
118 | ```typescript
119 | // webpack.config
120 | {
121 | new ModuleFederationPlugin({
122 | name: 'dashboard-mfe',
123 | filename: 'remoteEntry.js',
124 | exposes: {
125 | // EntryModule is the key of the entry.module.ts file and corresponds to the exported EntryModule class from this file.
126 | EntryModule: 'apps/dashboard-mfe/src/app/remote-entry/entry.module.ts',
127 | // the EntryComponent is key of file entry.module.ts, and match to exported EntryComponent class from that file.
128 | EntryComponent: 'apps/dashboard-mfe/src/app/remote-entry/entry.component.ts',
129 | },
130 | [...]
131 | });
132 | }
133 | ```
134 |
135 | > If the name of Module doesn't match, you can specify a custom name for this Module in the @Input() property `mfeOutletOptions = { componentName: 'CustomName' }` of `[mfeOutlet]` directive, and pass `{ moduleName: 'CustomName' }` options to the `loadMfe()` function;
136 |
137 | > If the name of Component doesn't match, you can specify a custom name for this Component in the @Input() property `mfeOutletOptions = { componentName: 'CustomName' }` of `[mfeOutlet]` directive, and pass `{ moduleName: 'CustomName' }` options to the `loadMfe()` function;
138 |
139 | 4. You must follow the rule that only one Component must be declared for an exposed Module. This is known as SCAM (**S**ingle **C**omponent **A**ngular **M**odule) pattern.
140 |
141 | ## Configuring
142 |
143 | Add the **ngx-mfe** library to a shared property in the ModuleFederationPlugin inside webpack.config.js file for each application in your workspace.
144 |
145 | ```typescript
146 | module.exports = {
147 | [...]
148 | plugins: [
149 | [...]
150 | new ModuleFederationPlugin({
151 | remotes: {},
152 | shared: share({
153 | [...]
154 | "ngx-mfe": {
155 | singleton: true,
156 | strictVersion: true,
157 | requiredVersion: 'auto',
158 | includeSecondaries: true
159 | },
160 | ...sharedMappings.getDescriptors(),
161 | }),
162 | library: {
163 | type: 'module'
164 | },
165 | }),
166 | [...]
167 | ],
168 | [...]
169 | };
170 | ```
171 |
172 | To configure this library, you must import `MfeModule.forRoot(options: NgxMfeOptions)` into the root module of the Host app(s) and the root module of the Remote apps in order for Remote to work correctly when running as a standalone application:
173 |
174 | > For feature modules just import `MfeModule` without options, where, you may need the functionality of the library, for example, the `MfeOutlet` directive.
175 |
176 | For core / app module:
177 | ```typescript
178 | @NgModule({
179 | imports: [
180 | MfeModule.forRoot({
181 | mfeConfig: {
182 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
183 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
184 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
185 | },
186 | preload: ['loaders-mfe', 'fallbacks-mfe'],
187 | loader: {
188 | app: 'loaders',
189 | module: 'SpinnerModule',
190 | component: 'SpinnerComponent',
191 | },
192 | loaderDelay: 500,
193 | fallback: {
194 | app: 'fallbacks',
195 | module: 'MfeFallbackModule',
196 | component: 'MfeFallbackComponent',
197 | },
198 | }),
199 | ],
200 | })
201 | export class AppModule {}
202 | ```
203 |
204 | For feature module:
205 | ```typescript
206 | @NgModule({
207 | imports: [
208 | MfeModule,
209 | ],
210 | })
211 | export class Feature1Module {}
212 | ```
213 |
214 | ### List of all available options:
215 |
216 | - **mfeConfig**
217 |
218 | ----------------
219 |
220 | **Sync variant of providing mfeConfig:**
221 |
222 | ----------------
223 |
224 | object where **key** is micro-frontend app name specified in `ModuleFederationPlugin` (webpack.config.js) and **value** is remoteEntryUrl string. All data will be sets to [MfeRegistry](https://github.com/dkhrunov/ngx-mfe/blob/master/projects/ngx-mfe/src/lib/registry/mfe-registry.ts).
225 |
226 | **Key** it's the name same specified in webpack.config.js of MFE (Remote) in option name in `ModuleFederationPlugin`.
227 |
228 | **Value** set the following pattern: `{url}/{remoteEntrypointFilename}`.
229 |
230 | - `url` is the url where the remote application is hosted.
231 |
232 | - `remoteEntrypointFilename` is the filename supplied in the remote's webpack configuration.
233 |
234 | Example
235 |
236 | > (Deprecated from v15.1.0) You can get `MfeRegistry` from DI :
237 | >
238 | > ```typescript
239 | > class AppComponent {
240 | >
241 | > constructor(public mfeRegistry: MfeRegistry) {}
242 | > }
243 | > ```
244 |
245 | You can even get instace of `MfeRegistry` like this:
246 |
247 | ```typescript
248 | const mfeRegistry: MfeRegistry = MfeRegistry.instace;
249 | ```
250 |
251 | ----------------
252 |
253 | **Async variant of providing mfeConfig:**
254 |
255 | ----------------
256 |
257 | > NOTE: The application will wait for initialization and completes when the promise resolves or the observable completes.
258 | >
259 | > Because under the hood used `APP_INITIALIZER` injection token with useFactory that returns Observale or Promise. [More about `APP_INITIALIZER`](https://angular.io/api/core/APP_INITIALIZER)
260 |
261 |
262 | Also you can provide mfeConfig with loading it from external resource as `Observale` or `Promise`, for this you should provide this type of object:
263 |
264 | ```typescript
265 | type NgxMfeAsyncConfig = {
266 | /**
267 | * A function to invoke to load a `MfeConfig`. The function is invoked with
268 | * resolved values of `token`s in the `deps` field.
269 | */
270 | useLoader: (...deps: any[]) => Observable | Promise;
271 | /**
272 | * A list of `token`s to be resolved by the injector. The list of values is then
273 | * used as arguments to the `useLoader` function.
274 | */
275 | deps?: any[];
276 | };
277 | ```
278 |
279 | For example:
280 |
281 | ```typescript
282 | mfeConfig: {
283 | useLoader: (http: HttpClient): Observable =>
284 | http.get('/manifest.json'),
285 | deps: [HttpClient]
286 | },
287 | ```
288 |
289 |
290 | - **preload** (Optional) - a list of micro-frontend names, their bundles (remoteEntry.js) will be loaded and saved in the cache when the application starts.
291 |
292 | Next options are only works in plugin-based approach with `MfeOutletDirective`:
293 |
294 | - **loaderDelay** (Optional) - Specifies the minimum loader display time in ms. This is to avoid flickering when the micro-frontend loads very quickly.
295 |
296 | *By default is 0.*
297 |
298 | - **loader** (Optional) - Displayed when loading the micro-frontend. Implements the `RemoteComponent` interface.
299 |
300 | *Example:*
301 | ```typescript
302 | // Globally uses the "SpinnerComponent" loader component declared in the "SpinnerModule" of the app "loaders".
303 | loader: {
304 | app: 'loaders',
305 | module: 'SpinnerModule',
306 | component: 'SpinnerComponent',
307 | },
308 | ```
309 |
310 | > For better UX, add loader micro-frontends to the `preload`.
311 |
312 | - **fallback** (Optional) - Displayed when loading or compiling a micro-frontend with an error. Implements the `RemoteComponent` interface.
313 |
314 | *Example:*
315 | ```typescript
316 | // Globally uses the "MfeFallbackComponent" fallback component declared in the "MfeFallbackModule" of the app "fallbacks".
317 | fallback: {
318 | app: 'fallbacks',
319 | module: 'MfeFallbackModule',
320 | component: 'MfeFallbackComponent',
321 | },
322 | ```
323 |
324 | > For better UX, add fallback micro-frontends to the `preload`.
325 |
326 | You can get all configured options by injecting `NGX_MFE_OPTIONS` by DI:
327 |
328 | ```typescript
329 | class AppComponent {
330 | constructor(@Inject(NGX_MFE_OPTIONS) public options: NgxMfeOptions) {}
331 | }
332 | ```
333 |
334 | ## Display MFE in HTML template / plugin-based approach
335 |
336 | This approach allows us to load micro-frontends directly from HTML.
337 |
338 | The advantages of this approach are that we can display several MFEs at once on the same page, even display several of the same MFEs.
339 |
340 | > More about plugin-based approach [here](https://dekh.medium.com/angular-micro-frontend-architecture-part-3-3-mfe-plugin-based-approach-f36dc9849b0).
341 |
342 | > Full code of this example can be found at https://github.com/dkhrunov/ngx-mfe-test.
343 |
344 | Example app:
345 |
346 | 
347 |
348 | An example webpack.config.js that exposes the "MfeTestComponent" (brown border in the screenshot above):
349 |
350 | ```js
351 | // webpack.config.js
352 | return {
353 | [...]
354 | resolve: {
355 | alias: sharedMappings.getAliases(),
356 | },
357 | plugins: [
358 | new ModuleFederationPlugin({
359 | name: 'test',
360 | exposes: {
361 | MfeTestModule: 'apps/test/src/app/mfe-test/mfe-test.module.ts',
362 | MfeTestComponent: 'apps/test/src/app/mfe-test/mfe-test.component.ts',
363 | },
364 | filename: 'remoteEntry',
365 | shared: share({ ... }),
366 | }),
367 | sharedMappings.getPlugin(),
368 | ],
369 | };
370 | ```
371 |
372 | 1. Just display the component "MfeTestComponent" inside other MFE component "Form" from "address-form" app:
373 |
374 | One variant:
375 | ```html
376 |
381 |
382 | ```
383 |
384 | Other variant:
385 | ```html
386 |
393 |
394 | ```
395 |
396 | > These two examples are equal and display the MFE "MfeTestComponent".
397 |
398 | 2. You can pass/bind `@Input` and `@Output` props to MFE component:
399 |
400 | ```html
401 |
402 |
411 | ```
412 |
413 | ```typescript
414 | // form.component.ts file
415 | @Component({
416 | selector: 'app-form',
417 | templateUrl: './form.component.html',
418 | styleUrls: ['./form.component.scss'],
419 | changeDetection: ChangeDetectionStrategy.OnPush,
420 | })
421 | export class FormComponent {
422 | [...]
423 | // timer emits after 1 second, then every 2 seconds
424 | public readonly text$: Observable = timer(1000, 2000);
425 |
426 | // on click log to console event
427 | public onClick(event: MouseEvent): void {
428 | console.log('clicked', event);
429 | }
430 | [...]
431 | }
432 | ```
433 |
434 | > If you try to bind a @Output() property that is not in the component, then an error will fall into the console:
435 | > "Output **someOutput** is not output of **SomeComponent**."
436 | >
437 | > If you try to pass a non-function, then an error will fall into the console:
438 | > "Output **someOutput** must be a function."
439 |
440 | 3. To override the default loader delay, configured in `MfeModule.forRoot({ ... })`, provide custom number in ms to property `loaderDelay`:
441 |
442 | ```html
443 |
451 | ```
452 |
453 | 4. To override the default loader and fallback MFE components, configured in `MfeModule.forRoot({ ... })`, specify content with `TemplateRef`, pass it to the appropriate properties `loader` and `fallback`:
454 |
455 | ```html
456 |
465 |
466 |
467 | loading...
468 |
469 |
470 |
471 | Ooops! Something went wrong
472 |
473 | ```
474 |
475 | ```html
476 |
477 |
483 |
484 |
485 |
492 |
493 |
494 | ```
495 |
496 | 6. You can also provide a custom injector for a component like this:
497 |
498 | ```html
499 |
505 | ```
506 |
507 | ## Display Angular v14 Standalone components
508 |
509 | Example app:
510 |
511 | 
512 |
513 | An example webpack.config.js that exposes the "StandaloneComponent" (green border in the screenshot above):
514 |
515 | ```js
516 | // webpack.config.js
517 | return {
518 | [...]
519 | resolve: {
520 | alias: sharedMappings.getAliases(),
521 | },
522 | plugins: [
523 | new ModuleFederationPlugin({
524 | name: 'test',
525 | exposes: {
526 | [...]
527 | StandaloneComponent: 'apps/test/src/app/standalone/standalone.component.ts',
528 | },
529 | filename: 'remoteEntry',
530 | shared: share({ ... }),
531 | }),
532 | sharedMappings.getPlugin(),
533 | ],
534 | };
535 | ```
536 |
537 | ```typescript
538 | // standalone.component.ts
539 |
540 | import { Component } from '@angular/core';
541 | import { CommonModule } from '@angular/common';
542 |
543 | @Component({
544 | selector: 'app-standalone',
545 | standalone: true,
546 | imports: [CommonModule],
547 | template: ` Standalone component works!
`,
548 | styles: [],
549 | })
550 | export class StandaloneComponent {}
551 | ```
552 |
553 | ```html
554 |
555 | [...]
556 | Angular v14 Standalone component loaded as MFE:
557 |
561 | ```
562 |
563 | ## Passing Data to the MFE Component via mfeOutlet directive
564 |
565 | After using this library for some time, as the author of this library, I came to the conclusion that using @Inputs and @Outputs of an MFE component through the `[mfeOutletInputs]` `[mfeOutletOutputs]` properties is not the best practice. Try to make your MFE components as independent as possible from the external environment. But if you still have to pass some values to the component, you can do it in two ways:
566 |
567 | 1. As I wrote above through the properties `[mfeOutletInputs]` `[mfeOutletOutputs]`
568 |
569 | component.html:
570 | ```html
571 |
578 |
579 | ```
580 |
581 | component.ts
582 | ```typescript
583 | @Component({ ... })
584 | export class Component {
585 | public text$ = new BehaviorSubject('Test string');
586 |
587 | constructor() { }
588 |
589 | public onClick(bool: MouseEvent): void {
590 | console.log('login', bool);
591 | }
592 | }
593 | ```
594 |
595 | 2. The second way is to create a new injector and add the necessary data for the MFE component to it. The `[mfeOutlet]` directive has the `[mfeOutletInjector]` property through which you can pass the desired injector, when the component is created, the previously passed injector in the `[mfeOutletInjector]` property will be used instead of the current injector.
596 |
597 | component.html:
598 | ```html
599 |
605 |
606 | ```
607 |
608 | component.ts
609 | ```typescript
610 | @Component({ ... })
611 | export class Component {
612 | public readonly testComponentInjector: Injector;
613 |
614 | constructor(private readonly _injector: Injector) {
615 | this.testComponentInjector = Injector.create({
616 | parent: this._injector,
617 | providers: [
618 | {
619 | provide: TEST_DATA,
620 | useValue: data,
621 | },
622 | ],
623 | });
624 | }
625 | }
626 | ```
627 |
628 | ## Load MFE by Route
629 |
630 | To use micro-frontends in Routing, you must import and apply the helper function called `loadMfe`, like in the example below:
631 |
632 | ```typescript
633 | import { NgModule } from '@angular/core';
634 | import { RouterModule, Routes } from '@angular/router';
635 | import { loadMfe } from '@dkhrunov/ng-mfe';
636 |
637 | const routes: Routes = [
638 | {
639 | path: 'dashboard',
640 | loadChildren: () => loadMfe('dashboard-mfe', 'EntryModule'),
641 | },
642 | ];
643 |
644 | @NgModule({
645 | imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })],
646 | exports: [RouterModule],
647 | })
648 | export class AppRoutingModule {}
649 | ```
650 |
651 | ## Changelog
652 |
653 | ### Changes in __v2.1.0__
654 | Fixed:
655 | - Fix error, if the fallback is also unavailable, then simply clear the view;
656 |
657 | Refactored:
658 | - Renamed `MfeService` to `RemoteComponentLoader`;
659 | - Renamed `MfeComponentsCache` to `RemoteComponentsCache`;
660 | - Renamed `ModularRemoteComponent` type to `RemoteComponentWithModule`;
661 | - Wrapped to `ngZone.runOutside` the `loadMfe` function calls inside the `RemoteComponentLoader`;
662 | - Added new type `ComponentWithNgModuleRef`, that holds component class `Type` and `NgModuleRef`;
663 | - Changed cached value for `RemoteComponentWithModule` from `ComponentFactory` to `ComponentWithNgModuleRef`;
664 | - In `RemoteComponentLoader` (old name `MfeService`) renamed function `loadModularComponent` to `loadComponentWithModule`
665 | - Changed return type of method `loadComponentWithModule` inside class `RemoteComponentLoader` from `Promise>` to `Promise>`;
666 |
667 | ### Changes in __v2.0.0__ (_Breaking changes_)
668 |
669 | __Why has the API changed?__ - The problem is that when you use the `[mfeOutlet]` directive [issue](https://github.com/dkhrunov/ngx-mfe/issues/7), it tries to find the component inside the compiled module by name (as a string), but in runtime the class name will be optimized and replaced with a short character. For example, you have a class `TestComponent`, it can be changed to the class name `a` and this causes this error.
670 |
671 | #### General:
672 | - To properly use the plugin-based approach in a micro-frontend architecture, or simply if you are use `[mfeOutlet]` directive, you must now expose both the component file and module file in which the component is declared to the ModuleFederationPlugin.
673 |
674 | __Rarerly :__ or, if your micro-frontend component is standalone (a standalone component is a component that does not have any dependencies declared or imported in the module where that component is declared), then it is sufficient to provide just that component file to the ModuleFederationPlugin;
675 |
676 | - Now __ngx-mfe__ does not use `Micro-frontend string` (or anouther name `MFE string`) is a kebab-case style string and matches the pattern `"mfe-app-name/exposed-file-name"` (__it was used until version 2.0.0__);
677 |
678 | - `MFE string` has been replaced by a new type `RemoteComponent`;
679 |
680 | - The `validateMfe` function has been removed (__it was used until version 2.0.0__);
681 |
682 | - The `loader` and `fallback` properties in the `NgxMfeOptions` has been changed from `MFE string` to `RemoteComponent` type:
683 |
684 | Before v2.0.0:
685 | ```typescript
686 | @NgModule({
687 | declarations: [AppComponent],
688 | imports: [
689 | BrowserModule,
690 | BrowserAnimationsModule,
691 | MfeModule.forRoot({
692 | mfeConfig: {
693 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
694 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
695 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
696 | },
697 | loader: 'loaders/spinner',
698 | fallback: 'fallbacks/mfe-fallback',
699 | }),
700 | ],
701 | bootstrap: [AppComponent],
702 | })
703 | export class AppModule {}
704 | ```
705 |
706 | Since v2.0.0:
707 | ```typescript
708 | @NgModule({
709 | declarations: [AppComponent],
710 | imports: [
711 | BrowserModule,
712 | BrowserAnimationsModule,
713 | MfeModule.forRoot({
714 | mfeConfig: {
715 | "dashboard-mfe": "http://localhost:4201/remoteEntry.js",
716 | "loaders-mfe": "http://localhost:4202/remoteEntry.js",
717 | "fallbacks-mfe": "http://localhost:4203/remoteEntry.js"
718 | },
719 | loader: {
720 | app: 'loaders',
721 | module: 'SpinnerModule',
722 | component: 'SpinnerComponent',
723 | },
724 | fallback: {
725 | app: 'fallbacks',
726 | module: 'MfeFallbackModule',
727 | component: 'MfeFallbackComponent',
728 | },
729 | }),
730 | ],
731 | bootstrap: [AppComponent],
732 | })
733 | export class AppModule {}
734 | ```
735 |
736 | - Removed `moduleName` property from `LoadMfeOptions` type;
737 | - Now, wherever you need to specify the name of the exposed file through the config in the webpack.config in the ModuleFederationPlugin, you must specify exactly the same name as in the config itself, the kebab-style name was used earlier.
738 | ```javascript
739 | // webpack.config.js
740 | exposes: {
741 | // LoginModule name of the exposed file login.module.ts
742 | LoginModule: 'apps/auth-mfe/src/app/login/login.module.ts',
743 | },
744 | ```
745 |
746 | Before v2.0.0:
747 | ```typescript
748 | loadMfe('auth-mfe/login-module')
749 | ```
750 |
751 | Since v2.0.0:
752 | ```typescript
753 | loadMfe('auth-mfe' 'LoginModule')
754 | ```
755 |
756 | #### LoadMfe function:
757 | - Arguments changed in `LoadMfe` function:
758 |
759 | Before v2.0.0:
760 | ```typescript
761 | async function loadMfe(mfeString: string, options?: LoadMfeOptions): Promise> {}
762 | ```
763 |
764 | Since v2.0.0:
765 | ```typescript
766 | async function loadMfe(remoteApp: string, exposedFile: string, options?: LoadMfeOptions): Promise> {}
767 | ```
768 | - `remoteApp` - is the name of the remote app as specified in the webpack.config.js file in the ModuleFederationPlugin in the __name__ property;
769 | - `exposedFile` - is the key (or name) of the exposed file specified in the webpack.config.js file in the ModuleFederationPlugin in the __exposes__ property;
770 |
771 | #### MfeOutlet directive:
772 | - Since the `Mfe string` has been removed from the library, the API of `[mfeOutlet]` directive has changed:
773 | 1. `mfeOutletLoader` and `mfeOutletFallback` now accept only `TemplateRef`, more details below.
774 | 2. To load a standalone component, you must specify the following details: `mfeOutlet` with the name of the application, `mfeOutletComponent` with the name of the component's open file from the ModuleFederationPlugin in webpack.config. But to load a non-standalone component, you must additionally specify `mfeOutletModule` with the name of the open module file in which the component is declared for the ModuleFederationPlugin in webpack.config.
775 | 3.
776 | - `@Input('mfeOutletOptions')' options` changed type from `MfeComponentFactoryResolverOptions` to `LoadMfeOptions`;
777 | - `@Input('mfeOutletLoader')' loader` and `@Input('mfeOutletFallback') fallback` now accept only `TemplateRef`, not `TemplateRef` or `Mfe string`. But you can still use micro-frontend component for `loader` and `fallback` in the `[mfeOutlet]`, like in the example below:
778 |
779 | ```html
780 |
781 |
787 |
788 |
789 |
790 |
791 |
792 |
799 |
800 |
801 |
802 |
803 |
809 |
810 |
811 |
812 |
813 | loading...
814 |
815 | ```
816 |
817 | #### MfeComponentFactoryResolver:
818 | - The `MfeComponentFactoryResolver` has been replaced with `MfeService` and the API has been changed;
819 | - The `MfeComponentFactoryResolverOptions` type has been removed;
820 |
821 | #### MfeComponentCache
822 | - Now the `MfeComponentCache` not only saves `ComponentFactory` but also `Type`;
823 | - In version 2.1.0 `ComponentFactory` was replaced to `ComponentWithNgModuleRef`;
824 |
825 | #### DynamicComponentBinding
826 | - The `bindInputs()` and `bindOutputs()` methods now require `ComponentRef` in the first argument, `MfeOutletInputs`/`MfeOutletOutputs` are method dependent in the second, and the third argument has been removed;
827 | - The `DynamicComponentInputs` and `DynamicComponentOutputs` types have been removed because these types are replaced in `bindInputs()` and `bindOutputs()` respectively by the `ComponentRef` type;
828 | - The `validateInputs()` method has been removed;
829 | - The `validateOutputs()` method is now private;
830 |
831 | ---------------
832 |
833 | ### Changes in __v1.1.0__:
834 |
835 | - Deleted the `loadMfeComponent` helper function;
836 | - Deleted the `parseMfeString` helper function;
837 | - Renamed the `loadMfeModule` helper function to `loadMfe` and added optional parameter `options: LoadMfeOptions`. `LoadMfeOptions` has property a `moduleName`, that sets a custom name for the Module class within the opened file, and has `type` that specify type of Module Federation;
838 | - Renamed the `MfeService` to `MfeComponentFactoryResolver`;
839 | - `MfeComponentFactoryResolver` has the same method as `MfeService`, but now it can accepts an optional `options: MfeComponentFactoryResolver` parameter. This parameter extends `LoadMfeOptions` type, added a `componentName` parameter, that sets a custom name for the Component class.
840 | - Added new Input prop to the `MfeOutletDirective` - `options: MfeComponentFactoryResolver`, this parameter provided to `resolveComponentFactory` method of the `MfeComponentFactoryResolver` when resolving the component factory of MFE.
841 | - Since **v1.1.0** you don't need to expose from `ModuleFederationPlugin` for plugin-based approach both Module and Component, just specify the Module file.
842 |
843 | The exposed Module key must match the name of the exposed module without the 'Module' suffix. Also, if the name doesn't match, you can specify a custom Module name in the options `{ moduleName: 'CustomName' }` in the property `mfeOutletOptions` inside `MfeOutletDirective` and in the options parameter of the `loadMfe` helper function.
844 |
845 | For the plugin-based approach, when loads MFE using `MfeOutletDirective` you must declare Component in the exposed Module and the Component name must match the exposed Module key without suffix 'Component'. Also, if the name doesn't match, you can specify a custom Component name in the Input property `mfeOutletOptions = { componentName: 'CustomName' }`;
846 |
847 | ---------------
848 |
849 | ### Changes in __v1.0.8__:
850 |
851 | - `IMfeModuleRootOptions` interface renamed to `NgxMfeOptions`;
852 | - Property `delay` in the `NgxMfeOptions` renamed to `loaderDelay`;
853 | - `OPTIONS` injection token renamed to `NGX_MFE_OPTIONS`;
854 |
855 | ---------------
856 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | jasmine: {
17 | // you can add configuration options for Jasmine here
18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
19 | // for example, you can disable the random execution with `random: false`
20 | // or set a specific seed with `seed: 4321`
21 | },
22 | clearContext: false // leave Jasmine Spec Runner output visible in browser
23 | },
24 | jasmineHtmlReporter: {
25 | suppressAll: true // removes the duplicated traces
26 | },
27 | coverageReporter: {
28 | dir: require('path').join(__dirname, '../../coverage/ngx-mfe'),
29 | subdir: '.',
30 | reporters: [
31 | { type: 'html' },
32 | { type: 'text-summary' }
33 | ]
34 | },
35 | reporters: ['progress', 'kjhtml'],
36 | port: 9876,
37 | colors: true,
38 | logLevel: config.LOG_INFO,
39 | autoWatch: true,
40 | browsers: ['Chrome'],
41 | singleRun: false,
42 | restartOnFileChange: true
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/ngx-mfe",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | }
7 | }
--------------------------------------------------------------------------------
/projects/ngx-mfe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-mfe",
3 | "version": "19.0.0",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "GitHub",
7 | "url": "https://github.com/dkhrunov/ngx-mfe"
8 | },
9 | "keywords": [
10 | "microfrontend",
11 | "mfe",
12 | "angular-microfrontend",
13 | "angular-mfe",
14 | "microcomponent",
15 | "angular"
16 | ],
17 | "author": {
18 | "name": "Denis Khrunov",
19 | "email": "therealpanda98@gmail.com"
20 | },
21 | "peerDependencies": {
22 | "@angular/common": "^19.0.0",
23 | "@angular/core": "^19.0.0",
24 | "@angular-architects/module-federation": "^19.0.0",
25 | "rxjs": "^7.0.0"
26 | },
27 | "dependencies": {
28 | "tslib": "^2.3.0"
29 | }
30 | }
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/decorators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './track-changes.decorator';
2 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/decorators/track-changes.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SimpleChanges } from '@angular/core';
2 |
3 | /**
4 | * Strategy for changing the component's @Input() variable.
5 | */
6 | export enum EChangesStrategy {
7 | /**
8 | * Called on every change.
9 | */
10 | Each,
11 | /**
12 | * Called only on the first change.
13 | */
14 | First,
15 | /**
16 | * Called on every change except the first.
17 | */
18 | NonFirst,
19 | }
20 |
21 | export interface TrackChangesOptions {
22 | /**
23 | * Change strategy.
24 | * @default EChangesStrategy.Each
25 | */
26 | strategy?: EChangesStrategy;
27 | /**
28 | * Compare the previous value with the current one
29 | * and execute the method only if the values differ.
30 | *
31 | * Values must be immutable, as values are compared by reference.
32 | * @default false
33 | */
34 | compare?: boolean;
35 | }
36 |
37 | const defaultOptions: TrackChangesOptions = {
38 | strategy: EChangesStrategy.Each,
39 | compare: false,
40 | };
41 |
42 | /**
43 | * Decorator of lifecycle hook ngOnChanges, that call specified method when changes prop (@Input) value.
44 | * -------
45 | *
46 | * Method decorator.
47 | *
48 | * @param prop Variable name of Input, that will be call method when changes.
49 | * @param methodName The name of the method that will be called when the variable changes.
50 | * @param options Options.
51 | */
52 | export function TrackChanges(
53 | prop: string,
54 | methodName: string,
55 | options?: TrackChangesOptions
56 | ): MethodDecorator {
57 | return function (
58 | target: any,
59 | _propertyKey: string | symbol,
60 | descriptor: PropertyDescriptor
61 | ): TypedPropertyDescriptor {
62 | const _options = { ...defaultOptions, ...options };
63 | const originalMethod = descriptor.value as (changes: SimpleChanges) => void;
64 |
65 | descriptor.value = function (changes: SimpleChanges): void {
66 | if (changes && changes[prop] && changes[prop].currentValue !== undefined) {
67 | const isFirstChange = changes[prop].firstChange;
68 | const shouldCompareValues = _options.compare;
69 | const isValuesDifference = changes[prop].previousValue !== changes[prop].currentValue;
70 |
71 | if (
72 | _options.strategy === EChangesStrategy.Each ||
73 | (_options.strategy === EChangesStrategy.First && isFirstChange) ||
74 | (_options.strategy === EChangesStrategy.NonFirst && !isFirstChange)
75 | ) {
76 | if (!shouldCompareValues) {
77 | target[methodName].call(this, changes[prop].currentValue as T);
78 | } else if (isValuesDifference) {
79 | target[methodName].call(this, changes[prop].currentValue as T);
80 | }
81 | }
82 | }
83 |
84 | originalMethod.call(this, changes);
85 | };
86 |
87 | return descriptor;
88 | };
89 | }
90 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/directives/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mfe-outlet.directive';
2 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/directives/mfe-outlet.directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { MfeOutletDirective } from '.';
2 |
3 | describe('MfeOutletDirective', () => {
4 | it('should create an instance', () => {
5 | const directive = new MfeOutletDirective();
6 | expect(directive).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/directives/mfe-outlet.directive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AfterViewInit,
3 | ChangeDetectorRef,
4 | ComponentRef,
5 | Directive,
6 | EmbeddedViewRef,
7 | Inject,
8 | Injector,
9 | Input,
10 | OnChanges,
11 | OnDestroy,
12 | TemplateRef,
13 | ViewContainerRef,
14 | } from '@angular/core';
15 |
16 | import { EChangesStrategy, TrackChanges } from '../decorators';
17 | import { delay, LoadMfeOptions } from '../helpers';
18 | import { NGX_MFE_OPTIONS } from '../injection-tokens';
19 | import {
20 | isRemoteComponentWithModule,
21 | isStandaloneRemoteComponent,
22 | NgxMfeOptions,
23 | RemoteComponent,
24 | RemoteComponentWithModule,
25 | StandaloneRemoteComponent,
26 | } from '../interfaces';
27 | import {
28 | DynamicComponentBinding,
29 | RemoteComponentLoader,
30 | RemoteComponentsCache,
31 | } from '../services';
32 | import { MfeOutletInputs, MfeOutletOutputs } from '../types';
33 |
34 | /**
35 | * Micro-frontend directive for plugin-based approach.
36 | * -------------
37 | *
38 | * This directive give you to load micro-frontend inside in HTML template.
39 | *
40 | * @example Loads remote component and show as embed view or as a plugin.
41 | * ```html
42 | *
43 | *
48 | *
49 | *
50 | *
51 | *
56 | *
57 | * ```
58 | *
59 | * @example Loads standalone remote component. Standalone component - it is a component that does not depend on anything and does not need dependencies from other modules.
60 | * ```html
61 | *
62 | *
66 | *
67 | * ```
68 | *
69 | * @example You can sets Inputs and sets handlers for Output events of the Remote component.
70 | * ```html
71 | *
78 | *
79 | *```
80 | *
81 | * @example Loads remote component and sets custom loader, same approach for fallback view.
82 | * ```html
83 | *
90 | *
91 | *
92 | *
93 | *
94 | *
95 | *
102 | *
103 | *
104 | *
105 | *
106 | *
107 | * loading...
108 | *
109 | * ```
110 | */
111 | @Directive({
112 | // eslint-disable-next-line @angular-eslint/directive-selector
113 | selector: '[mfeOutlet]',
114 | exportAs: 'mfeOutlet',
115 | providers: [DynamicComponentBinding],
116 | standalone: false,
117 | })
118 | export class MfeOutletDirective implements OnChanges, AfterViewInit, OnDestroy {
119 | /**
120 | * Sets the Remote app name.
121 | */
122 | @Input('mfeOutlet')
123 | public mfeApp?: string;
124 |
125 | /**
126 | * Sets the Remote compoennt.
127 | */
128 | // eslint-disable-next-line @angular-eslint/no-input-rename
129 | @Input('mfeOutletComponent')
130 | public mfeComponent?: string;
131 |
132 | /**
133 | * Sets the Remote module where declared Remote component (```mfeOutletComponent```)
134 | */
135 | // eslint-disable-next-line @angular-eslint/no-input-rename
136 | @Input('mfeOutletModule')
137 | public mfeModule?: string;
138 |
139 | /**
140 | * A map of Inputs for a micro-frontend component.
141 | */
142 | @Input('mfeOutletInputs')
143 | public inputs?: MfeOutletInputs;
144 |
145 | /**
146 | * A map of Outputs for a micro-frontend component.
147 | */
148 | @Input('mfeOutletOutputs')
149 | public outputs?: MfeOutletOutputs;
150 |
151 | /**
152 | * Custom injector for micro-frontend component.
153 | * @default current injector
154 | */
155 | @Input('mfeOutletInjector')
156 | public injector?: Injector = this._injector;
157 |
158 | /**
159 | * MFE RemoteComponent or TemplateRef.
160 | * Displayed when loading the micro-frontend.
161 | *
162 | * **Overrides the loader specified in the global library settings.**
163 | * @default options.loader
164 | */
165 | @Input('mfeOutletLoader')
166 | public set loader(value: TemplateRef | undefined) {
167 | this._loader = value;
168 | }
169 |
170 | /**
171 | * The delay between displaying the contents of the bootloader and the micro-frontend .
172 | *
173 | * This is to avoid flickering when the micro-frontend loads very quickly.
174 | *
175 | * @default options.delay, if not set, then 0
176 | */
177 | @Input('mfeOutletLoaderDelay')
178 | public loaderDelay = this._options.loaderDelay ?? 0;
179 |
180 | /**
181 | * MFE RemoteComponent or TemplateRef.
182 | * Displayed when loaded or compiled a micro-frontend with an error.
183 | *
184 | * **Overrides fallback the specified in the global library settings.**
185 | * @default options.fallback
186 | */
187 | @Input('mfeOutletFallback')
188 | public set fallback(value: TemplateRef | undefined) {
189 | this._fallback = value;
190 | }
191 |
192 | /**
193 | * Custom options for loading Mfe.
194 | */
195 | @Input('mfeOutletOptions')
196 | public options?: LoadMfeOptions;
197 |
198 | private _loader?: RemoteComponent | TemplateRef =
199 | this._options.loader;
200 | private _fallback?: RemoteComponent | TemplateRef =
201 | this._options.fallback;
202 |
203 | private _mfeComponentRef?: ComponentRef;
204 | private _loaderComponentRef?:
205 | | ComponentRef
206 | | EmbeddedViewRef;
207 | private _fallbackComponentRef?:
208 | | ComponentRef
209 | | EmbeddedViewRef;
210 |
211 | /**
212 | * Remote component object.
213 | */
214 | private get _remoteComponent(): RemoteComponent {
215 | if (this.mfeModule) {
216 | return {
217 | app: this.mfeApp,
218 | component: this.mfeComponent,
219 | module: this.mfeModule,
220 | } as RemoteComponentWithModule;
221 | }
222 |
223 | return {
224 | app: this.mfeApp,
225 | component: this.mfeComponent,
226 | } as StandaloneRemoteComponent;
227 | }
228 |
229 | constructor(
230 | private readonly _vcr: ViewContainerRef,
231 | // INSTEAD OF USE THIS REF TO INJECTOR USE `this.injector`
232 | private readonly _injector: Injector,
233 | private readonly _remoteComponentLoader: RemoteComponentLoader,
234 | private readonly _remoteComponentCache: RemoteComponentsCache,
235 | private readonly _dynamicBinding: DynamicComponentBinding,
236 | @Inject(NGX_MFE_OPTIONS) private readonly _options: NgxMfeOptions
237 | ) {}
238 |
239 | @TrackChanges('mfeRemote', 'renderMfe', {
240 | compare: true,
241 | strategy: EChangesStrategy.NonFirst,
242 | })
243 | @TrackChanges('mfeComponent', 'renderMfe', {
244 | compare: true,
245 | strategy: EChangesStrategy.NonFirst,
246 | })
247 | @TrackChanges('mfeModule', 'renderMfe', {
248 | compare: true,
249 | strategy: EChangesStrategy.NonFirst,
250 | })
251 | @TrackChanges('inputs', 'transferInputs', {
252 | strategy: EChangesStrategy.NonFirst,
253 | compare: true,
254 | })
255 | public ngOnChanges(): void {
256 | return;
257 | }
258 |
259 | public ngAfterViewInit(): void {
260 | this.renderMfe();
261 | }
262 |
263 | public ngOnDestroy(): void {
264 | this._clearView();
265 | }
266 |
267 | /**
268 | * Transfer MfeOutletInputs to micro-frontend component.
269 | *
270 | * Used when changing input "inputs" of this directive.
271 | * @internal
272 | */
273 | protected transferInputs(): void {
274 | if (!this._mfeComponentRef) return;
275 |
276 | this._dynamicBinding.bindInputs(this._mfeComponentRef, this.inputs ?? {});
277 |
278 | // Workaround for bug related to Angular and dynamic components.
279 | // Link - https://github.com/angular/angular/issues/36667#issuecomment-926526405
280 | this._mfeComponentRef?.injector.get(ChangeDetectorRef).detectChanges();
281 | }
282 |
283 | /**
284 | * Render micro-frontend component.
285 | *
286 | * While loading bundle of micro-frontend showing loader.
287 | * If error occur then showing fallback.
288 | *
289 | * Used when changing input "mfe" of this directive.
290 | * @internal
291 | */
292 | protected async renderMfe(): Promise {
293 | try {
294 | // If some component already rendered then need to unbind outputs
295 | if (this._mfeComponentRef) this._dynamicBinding.unbindOutputs();
296 |
297 | if (this._remoteComponentCache.isRegistered(this._remoteComponent)) {
298 | this._showMfe();
299 | } else {
300 | await this._showLoader();
301 | await delay(this.loaderDelay);
302 | this._showMfe();
303 | }
304 | } catch (error) {
305 | console.error(error);
306 | this._showFallback();
307 | }
308 | }
309 |
310 | /**
311 | * Shows micro-frontend component.
312 | * @internal
313 | */
314 | private async _showMfe(): Promise {
315 | try {
316 | if (this.mfeApp) {
317 | this._mfeComponentRef = await this._createView(
318 | this._remoteComponent,
319 | this.options
320 | );
321 | this._bindMfeData();
322 | }
323 | } catch (error) {
324 | console.group(`Error in Microfronted "${this._remoteComponent.app}"`);
325 | if (isRemoteComponentWithModule(this._remoteComponent)) {
326 | console.log('module :>> ', this._remoteComponent.module);
327 | }
328 | console.log('component :>> ', this._remoteComponent.component);
329 | console.log(
330 | 'is standalone :>> ',
331 | isStandaloneRemoteComponent(this._remoteComponent)
332 | );
333 | console.error(error);
334 | console.groupEnd();
335 | this._showFallback();
336 | }
337 | }
338 |
339 | /**
340 | * Shows loader content.
341 | * @internal
342 | */
343 | private async _showLoader(): Promise {
344 | try {
345 | if (this._loader) {
346 | this._loaderComponentRef = await this._createView(this._loader);
347 | }
348 | } catch (error) {
349 | console.error(error);
350 | this._showFallback();
351 | }
352 | }
353 |
354 | /**
355 | * Shows fallback content.
356 | * @internal
357 | */
358 | private async _showFallback(): Promise {
359 | if (this._fallback) {
360 | try {
361 | this._fallbackComponentRef = await this._createView(this._fallback);
362 | } catch (error) {
363 | console.error(error);
364 | this._clearView();
365 | }
366 | } else {
367 | this._clearView();
368 | }
369 | }
370 |
371 | /**
372 | * Shows MFE Component or TemlateRef.
373 | * @param content MFE (Remote component) or TemlateRef.
374 | * @param options Custom options for MfeComponentFactoryResolver.
375 | * @internal
376 | */
377 | private async _createView(
378 | templateRef: TemplateRef
379 | ): Promise>;
380 |
381 | private async _createView(
382 | remoteComponent: RemoteComponent,
383 | options?: LoadMfeOptions
384 | ): Promise>;
385 |
386 | private async _createView(
387 | remoteComponentOrTemplateRef: RemoteComponent | TemplateRef,
388 | options?: LoadMfeOptions
389 | ): Promise | ComponentRef>;
390 |
391 | private async _createView(
392 | content: RemoteComponent | TemplateRef,
393 | options?: LoadMfeOptions
394 | ): Promise | ComponentRef> {
395 | // TemplateRef
396 | if (content instanceof TemplateRef) {
397 | this._clearView();
398 | return this._vcr.createEmbeddedView(content);
399 | }
400 | // MFE (Remote Component)
401 | else {
402 | const componentRef: ComponentRef = isRemoteComponentWithModule(content)
403 | ? // for modular Angular (any version) components
404 | await this._createRemoteComponent(content, options)
405 | : // for standalone Angular v13+ components
406 | await this._createStandaloneRemoteComponent(content, options);
407 |
408 | componentRef.changeDetectorRef.detectChanges();
409 | return componentRef;
410 | }
411 | }
412 |
413 | // TODO pattern strategy 1
414 | /**
415 | * Create view for modular remote component.
416 | * @param remoteComponent MFE remote component
417 | * @param options (Optional) object of options.
418 | */
419 | private async _createRemoteComponent(
420 | remoteComponent: RemoteComponentWithModule,
421 | options?: LoadMfeOptions
422 | ): Promise> {
423 | const { component, ngModuleRef } =
424 | await this._remoteComponentLoader.loadComponentWithModule<
425 | TComponent,
426 | unknown
427 | >(remoteComponent, this.injector, options);
428 |
429 | this._clearView();
430 |
431 | const componentRef = this._vcr.createComponent(component, {
432 | ngModuleRef,
433 | injector: this.injector,
434 | });
435 |
436 | return componentRef;
437 | }
438 |
439 | // TODO pattern strategy 2
440 | /**
441 | * Create view for standalone remote component.
442 | * @param remoteComponent MFE remote component
443 | * @param options (Optional) object of options.
444 | */
445 | private async _createStandaloneRemoteComponent(
446 | remoteComponent: StandaloneRemoteComponent,
447 | options?: LoadMfeOptions
448 | ): Promise> {
449 | const component =
450 | await this._remoteComponentLoader.loadStandaloneComponent(
451 | remoteComponent,
452 | options
453 | );
454 |
455 | this._clearView();
456 |
457 | const componentRef = this._vcr.createComponent(component, {
458 | injector: this.injector,
459 | });
460 |
461 | return componentRef;
462 | }
463 |
464 | // TODO работает и без этого метода, но не работает output
465 | /**
466 | * Binding the initial data of the micro-frontend.
467 | * @internal
468 | */
469 | private _bindMfeData(): void {
470 | if (!this._mfeComponentRef) {
471 | throw new Error(
472 | `_bindMfeData method must be called after micro-frontend component "${this.mfeApp}" has been initialized.`
473 | );
474 | }
475 |
476 | this._dynamicBinding.bindInputs(this._mfeComponentRef, this.inputs ?? {});
477 | this._dynamicBinding.bindOutputs(this._mfeComponentRef, this.outputs ?? {});
478 |
479 | // TODO похоже что не актуально больше работает все и без этой штуки все
480 |
481 | // Workaround for bug related to Angular and dynamic components.
482 | // Link - https://github.com/angular/angular/issues/36667#issuecomment-926526405
483 | this._mfeComponentRef?.injector.get(ChangeDetectorRef).detectChanges();
484 | }
485 |
486 | /**
487 | * Destroy all displayed components and clear view container ref.
488 | * @internal
489 | */
490 | private _clearView() {
491 | this._loaderComponentRef?.destroy();
492 | this._fallbackComponentRef?.destroy();
493 | this._mfeComponentRef?.destroy();
494 | this._vcr.clear();
495 | }
496 | }
497 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/helpers/delay.ts:
--------------------------------------------------------------------------------
1 | export const delay = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './delay';
2 | export * from './load-mfe';
3 |
4 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/helpers/load-mfe.ts:
--------------------------------------------------------------------------------
1 | import { loadRemoteModule, LoadRemoteModuleOptions } from '@angular-architects/module-federation';
2 | import { Type } from '@angular/core';
3 |
4 | import { firstValueFrom } from 'rxjs';
5 | import { MfeRegistry } from '../registry';
6 |
7 | /**
8 | * Options for ```loadMfe``` function.
9 | */
10 | export type LoadMfeOptions = {
11 | /**
12 | * Set custom exposed module name, by default module name = exposedItem + 'Module'.
13 | */
14 | moduleName?: string,
15 | /**
16 | * Type of loaded module as a ```script``` or as a ```module```.
17 | */
18 | type?: LoadRemoteModuleOptions['type'];
19 | };
20 |
21 | const loadMfeDefaultOptions: LoadMfeOptions = { type: 'module' };
22 |
23 | /**
24 | * Loads remote bundle.
25 | *
26 | * @param remoteApp The name of the micro-frontend app decalred in ModuleFederationPlugin.
27 | * @param exposedModule The key of the exposed module decalred in ModuleFederationPlugin.
28 | * @param options (Optional) object of options.
29 | */
30 | export async function loadMfe(
31 | remoteApp: string,
32 | exposedModule: string,
33 | options: LoadMfeOptions = loadMfeDefaultOptions
34 | ): Promise> {
35 | const _options: LoadMfeOptions = { ...loadMfeDefaultOptions, ...options };
36 | const remoteEntry = await firstValueFrom(MfeRegistry.instance.getMfeRemoteEntry(remoteApp));
37 | const loadRemoteModuleOptions: LoadRemoteModuleOptions =
38 | _options.type === 'module'
39 | ? { type: _options.type, remoteEntry, exposedModule }
40 | : { type: _options.type, remoteEntry, exposedModule, remoteName: remoteApp };
41 | const bundle = await loadRemoteModule(loadRemoteModuleOptions);
42 | const moduleName = _options.moduleName ?? exposedModule;
43 | const module = bundle[moduleName]
44 |
45 | if (!module) {
46 | throw new Error(`Module with name "${moduleName}" does not exist in the exposed file. Key of exposed file must match with class name in this file (Key of exposed file it is key of 'exposes' object in webpack config inside ModuleFederationPlugin).`);
47 | }
48 |
49 | return module;
50 | }
51 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/injection-tokens/index.ts:
--------------------------------------------------------------------------------
1 | export * from './options.token';
2 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/injection-tokens/options.token.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 | import { NgxMfeOptions } from '../interfaces';
3 |
4 | /**
5 | * InjectionToken of options.
6 | */
7 | export const NGX_MFE_OPTIONS = new InjectionToken('ngx-mfe/options');
8 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mfe-config.interface';
2 | export * from './ngx-mfe-options.interface';
3 | export * from './remote-component.interface';
4 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/interfaces/mfe-config.interface.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * MFE app options.
3 | */
4 | export interface MfeConfig {
5 | [mfeName: string]: string;
6 | }
7 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/interfaces/ngx-mfe-options.interface.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs';
2 | import { MfeConfig } from './mfe-config.interface';
3 | import { RemoteComponent } from './remote-component.interface';
4 |
5 | /**
6 | * Sync list of available micro-frontends.
7 | */
8 | export type NgxMfeSyncConfig = MfeConfig;
9 |
10 | /**
11 | * Async list of available micro-frontends.
12 | */
13 | export type NgxMfeAsyncConfig = {
14 | /**
15 | * A function to invoke to load a `MfeConfig`. The function is invoked with
16 | * resolved values of `token`s in the `deps` field.
17 | */
18 | useLoader: (...deps: any[]) => Observable | Promise;
19 | /**
20 | * A list of `token`s to be resolved by the injector. The list of values is then
21 | * used as arguments to the `useLoader` function.
22 | */
23 | deps?: any[];
24 | };
25 |
26 | /**
27 | * Type of sync / async list of available micro-frontends.
28 | */
29 | export type NgxMfeConfigOption = NgxMfeSyncConfig | NgxMfeAsyncConfig;
30 |
31 | /**
32 | * Type guard check that NgxMfeConfig is async list of available micro-frontends.
33 | */
34 | export const isNgxMfeConfigAsync = (config: NgxMfeConfigOption): config is NgxMfeAsyncConfig => {
35 | return Object.prototype.hasOwnProperty.call(config, 'useLoader');
36 | }
37 |
38 | /**
39 | * Global options.
40 | */
41 | export interface NgxMfeOptions {
42 | /**
43 | * List of names of remote appls, declared apps will be downloaded immediately and stored in the cache.
44 | */
45 | preload?: string[];
46 | /**
47 | * Loader remote component.
48 | *
49 | * Shows when load bundle of the micro-frontend.
50 | *
51 | * For better UX, add this micro-frontend to {@link preload} array.
52 | */
53 | loader?: RemoteComponent;
54 | /**
55 | * The delay between displaying the contents of the bootloader and the micro-frontend.
56 | *
57 | * This is to avoid flickering when the micro-frontend loads very quickly.
58 | */
59 | loaderDelay?: number;
60 | /**
61 | * Fallback remote component.
62 | *
63 | * Showing when an error occurs while loading bundle
64 | * or when trying to display the contents of the micro-frontend.
65 | *
66 | * For better UX, add this micro-frontend to {@link preload} array.
67 | */
68 | fallback?: RemoteComponent;
69 | }
70 |
71 | /**
72 | * Options forRoot configuration of `NgxMfeModule`
73 | */
74 | export type NgxMfeForRootOptions = NgxMfeOptions & {
75 | /**
76 | * List of available micro-frontends.
77 | */
78 | mfeConfig: NgxMfeConfigOption;
79 | }
80 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/interfaces/remote-component.interface.ts:
--------------------------------------------------------------------------------
1 | export interface StandaloneRemoteComponent {
2 | /**
3 | * Remote app name, specified in ModuleFederationPlugin in name config.
4 | */
5 | app: string;
6 | /**
7 | * The key of the exposed component.
8 | */
9 | component: string;
10 | }
11 |
12 | export interface RemoteComponentWithModule {
13 | /**
14 | * Remote app name, specified in ModuleFederationPlugin in name config.
15 | */
16 | app: string;
17 | /**
18 | * The key of the exposed component.
19 | */
20 | component: string;
21 | /**
22 | * The key of the exposed module in which the component is declared.
23 | */
24 | module: string;
25 | }
26 |
27 | export type RemoteComponent = StandaloneRemoteComponent | RemoteComponentWithModule;
28 |
29 | /**
30 | * Type Guard for RemoteComponent, checks if RemoteComponent is Standalone
31 | * @param remoteComponent Mfe Remote Component
32 | * @returns
33 | */
34 | export function isStandaloneRemoteComponent(
35 | remoteComponent: RemoteComponent
36 | ): remoteComponent is StandaloneRemoteComponent {
37 | return !Object.prototype.hasOwnProperty.call(remoteComponent, 'module');
38 | }
39 |
40 | /**
41 | * Type Guard for RemoteComponent, checks if RemoteComponent is Modular
42 | * @param remoteComponent Mfe Remote Component
43 | * @returns
44 | */
45 | export function isRemoteComponentWithModule(
46 | remoteComponent: RemoteComponent
47 | ): remoteComponent is RemoteComponentWithModule {
48 | return Object.prototype.hasOwnProperty.call(remoteComponent, 'module');
49 | }
50 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/mfe.module.ts:
--------------------------------------------------------------------------------
1 | import { loadRemoteEntry } from '@angular-architects/module-federation';
2 | import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angular/core';
3 | import { firstValueFrom, from, Observable, tap } from 'rxjs';
4 | import { MfeOutletDirective } from './directives';
5 | import { NGX_MFE_OPTIONS } from './injection-tokens';
6 | import { isNgxMfeConfigAsync, MfeConfig, NgxMfeForRootOptions } from './interfaces';
7 | import { MfeRegistry } from './registry';
8 |
9 | /**
10 | * Core lib of micro-frontend architecture.
11 | * ---------------
12 | *
13 | * For core module provide MfeModule.forRoot(options).
14 | *
15 | * For feature modules provide MfeModule.
16 | */
17 | @NgModule({
18 | declarations: [MfeOutletDirective],
19 | exports: [MfeOutletDirective],
20 | })
21 | export class MfeModule {
22 | /**
23 | * Sets global configuration of Mfe lib.
24 | * @param options Object of options.
25 | */
26 | public static forRoot(options: NgxMfeForRootOptions): ModuleWithProviders {
27 | const { preload, mfeConfig } = options;
28 | const providers: Provider[] = [
29 | {
30 | provide: NGX_MFE_OPTIONS,
31 | useValue: options,
32 | },
33 | ];
34 |
35 | if (isNgxMfeConfigAsync(mfeConfig)) {
36 | providers.push({
37 | provide: APP_INITIALIZER,
38 | useFactory: (): (() => Observable) => {
39 | return () => {
40 | return from(mfeConfig.useLoader(...(mfeConfig.deps ?? []))).pipe(
41 | tap((config) => initializeMfeRegistry(config, preload))
42 | );
43 | };
44 | },
45 | multi: true,
46 | });
47 | } else {
48 | initializeMfeRegistry(mfeConfig, preload);
49 | }
50 |
51 | return { ngModule: MfeModule, providers };
52 | }
53 | }
54 |
55 | function initializeMfeRegistry(config: MfeConfig, preload?: string[]): MfeRegistry {
56 | const mfeRegistry = MfeRegistry.instance;
57 | mfeRegistry.setMfeConfig(config);
58 | const loadMfeBundle = loadMfeBundleWithMfeRegistry(mfeRegistry);
59 |
60 | if (preload) {
61 | preload.map((mfe) => loadMfeBundle(mfe));
62 | }
63 |
64 | return mfeRegistry;
65 | }
66 |
67 | /**
68 | * Loads micro-frontend app bundle (HOF - High Order Function).
69 | * ------
70 | *
71 | * Returns function that can load micro-frontend app by provided name.
72 | * @param mfeRegistry Registry of micro-frontends apps.
73 | */
74 | function loadMfeBundleWithMfeRegistry(mfeRegistry: MfeRegistry): (mfe: string) => Promise {
75 | return async (mfeString: string): Promise => {
76 | const remoteEntry = await firstValueFrom(mfeRegistry.getMfeRemoteEntry(mfeString));
77 |
78 | return loadRemoteEntry({ type: 'module', remoteEntry });
79 | };
80 | }
81 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/registry/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mfe-registry';
2 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/registry/mfe-registry.ts:
--------------------------------------------------------------------------------
1 | import { map, Observable, ReplaySubject, take } from 'rxjs';
2 | import { MfeConfig } from '../interfaces';
3 |
4 | /**
5 | * Registry of micro-frontends apps.
6 | */
7 | export class MfeRegistry {
8 | private static _instance: MfeRegistry;
9 |
10 | private readonly _mfeConfig$ = new ReplaySubject(1);
11 |
12 | /**
13 | * Get instance of the MfeRegistry
14 | */
15 | public static get instance(): MfeRegistry {
16 | if (!MfeRegistry._instance) {
17 | MfeRegistry._instance = new MfeRegistry();
18 | }
19 |
20 | return MfeRegistry._instance;
21 | }
22 |
23 | private constructor() {}
24 |
25 | /**
26 | * Set config.
27 | * @param config Micro-frontends config
28 | */
29 | public setMfeConfig(config: MfeConfig): void {
30 | this._mfeConfig$.next(config);
31 | }
32 |
33 | /**
34 | * Get the remote entry URL the micro-frontend app
35 | * @param mfeApp Micro-frontend app name
36 | */
37 | public getMfeRemoteEntry(mfeApp: string): Observable {
38 | return this._mfeConfig$.pipe(
39 | take(1),
40 | map((config) => {
41 | const remoteEntry = config[mfeApp];
42 |
43 | if (!remoteEntry) {
44 | throw new Error(
45 | `'${mfeApp}' micro-frontend is not registered in the MfeRegistery using MfeModule.forRoot({ mfeConfig })`
46 | );
47 | }
48 |
49 | return remoteEntry;
50 | })
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/services/dynamic-component-binding.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { DynamicComponentBinding } from './dynamic-component-binding';
4 |
5 | describe(DynamicComponentBinding, () => {
6 | let service: DynamicComponentBinding;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(DynamicComponentBinding);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/services/dynamic-component-binding.ts:
--------------------------------------------------------------------------------
1 | import { ComponentRef, EventEmitter, Injectable, OnDestroy } from '@angular/core';
2 | import { Subject, takeUntil } from 'rxjs';
3 |
4 | import { MfeOutletInputs, MfeOutletOutputs } from '../types';
5 |
6 | /**
7 | * The service that binds the dynamic component.
8 | */
9 | @Injectable()
10 | export class DynamicComponentBinding implements OnDestroy {
11 | private readonly _destroy$ = new Subject();
12 |
13 | public ngOnDestroy(): void {
14 | this._destroy$.next();
15 | this._destroy$.complete();
16 | }
17 |
18 | /**
19 | * Bind provided MfeOutletInputs to dynamic component.
20 | * @param componentRef Reference of component.
21 | * @param inputs Provided MfeOutletInputs.
22 | */
23 | public bindInputs(componentRef: ComponentRef, inputs: MfeOutletInputs): void {
24 | for (const key in inputs) {
25 | if (Object.prototype.hasOwnProperty.call(inputs, key)) {
26 | (componentRef.instance as any)[key] = inputs[key];
27 | }
28 | }
29 | }
30 |
31 | /**
32 | * Bind provided MfeOutletOutputs to dynamic component.
33 | * @param componentRef Reference of component.
34 | * @param outputs Provided MfeOutletOutputs.
35 | */
36 | public bindOutputs(
37 | componentRef: ComponentRef,
38 | outputs: MfeOutletOutputs
39 | ): void {
40 | this._validateOutputs(componentRef, outputs);
41 |
42 | for (const key in outputs) {
43 | if (Object.prototype.hasOwnProperty.call(outputs, key)) {
44 | ((componentRef.instance as any)[key] as EventEmitter)
45 | .pipe(takeUntil(this._destroy$))
46 | .subscribe((event) => {
47 | const handler = outputs[key];
48 | if (handler) {
49 | // in case the output has not been provided at all
50 | handler(event);
51 | }
52 | });
53 | }
54 | }
55 | }
56 |
57 | /**
58 | * Unbind all outputs.
59 | */
60 | public unbindOutputs(): void {
61 | this._destroy$.next();
62 | }
63 |
64 | /**
65 | * Validate MfeOutletOutputs of dynamic component.
66 | * @param componentRef Reference of component.
67 | * @param outputs Provided MfeOutletOutputs.
68 | */
69 | private _validateOutputs(
70 | componentRef: ComponentRef,
71 | outputs: MfeOutletOutputs
72 | ): void {
73 | Object.keys(outputs).forEach((key) => {
74 | const isComponentHaveOutput = Object.prototype.hasOwnProperty.call(
75 | componentRef.instance,
76 | key
77 | );
78 |
79 | if (!isComponentHaveOutput) {
80 | throw new Error(
81 | `Dynamically bound Output "${key}" is not declared in target component ${componentRef.componentType.constructor.name}.`
82 | );
83 | }
84 |
85 | if (!((componentRef.instance as any)[key] instanceof EventEmitter)) {
86 | throw new Error(
87 | `Dynamically bound Output "${key}" must be an instance of EventEmitter.`
88 | );
89 | }
90 |
91 | if (!(outputs[key] instanceof Function)) {
92 | throw new Error(`Dynamically bound Output "${key}" must be a function.`);
93 | }
94 | });
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dynamic-component-binding';
2 | export * from './remote-component-loader';
3 | export * from './remote-components-cache';
4 |
5 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/services/remote-component-loader.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { RemoteComponentLoader } from './remote-component-loader';
3 |
4 | describe('RemoteComponentLoader', () => {
5 | let service: RemoteComponentLoader;
6 |
7 | beforeEach(() => {
8 | TestBed.configureTestingModule({});
9 | service = TestBed.inject(RemoteComponentLoader);
10 | });
11 |
12 | it('should be created', () => {
13 | expect(service).toBeTruthy();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/services/remote-component-loader.ts:
--------------------------------------------------------------------------------
1 | import { createNgModuleRef, Injectable, Injector, NgZone, Type } from '@angular/core';
2 |
3 | import { loadMfe, LoadMfeOptions } from '../helpers';
4 | import { RemoteComponentWithModule, StandaloneRemoteComponent } from '../interfaces';
5 | import { ComponentWithNgModuleRef } from '../types';
6 | import { RemoteComponentsCache } from './remote-components-cache';
7 |
8 | /**
9 | * A low-level service for loading a remote micro-frontend component.
10 | */
11 | @Injectable({
12 | providedIn: 'root',
13 | })
14 | export class RemoteComponentLoader {
15 | constructor(
16 | private readonly _ngZone: NgZone,
17 | private readonly _injector: Injector,
18 | private readonly _cache: RemoteComponentsCache
19 | ) {}
20 |
21 | /**
22 | * Loads a remote component with module where was declared this component.
23 | * @param remoteComponent Remote component.
24 | * @param injector (Optional) Injector, use root injector by default.
25 | * @param options (Optional) object of options.
26 | */
27 | public async loadComponentWithModule(
28 | remoteComponent: RemoteComponentWithModule,
29 | injector: Injector = this._injector,
30 | options?: LoadMfeOptions
31 | ): Promise> {
32 | try {
33 | if (this._cache.isRegistered(remoteComponent)) {
34 | return this._cache.getValue(remoteComponent);
35 | }
36 |
37 | this._cache.register(remoteComponent);
38 |
39 | const { component, module } = await this._ngZone.runOutsideAngular(async () => {
40 | const component = await loadMfe(
41 | remoteComponent.app,
42 | remoteComponent.component,
43 | options
44 | );
45 | const module = await loadMfe(
46 | remoteComponent.app,
47 | remoteComponent.module,
48 | options
49 | );
50 |
51 | return { component, module };
52 | });
53 |
54 | const ngModuleRef = createNgModuleRef(module, injector);
55 |
56 | const componentWithNgModuleRef: ComponentWithNgModuleRef = { component, ngModuleRef };
57 | this._cache.setValue(remoteComponent, componentWithNgModuleRef);
58 | return componentWithNgModuleRef;
59 | } catch (error: unknown) {
60 | this._cache.setError(remoteComponent, error);
61 | throw error;
62 | }
63 | }
64 |
65 | /**
66 | * Loads a standalone remote component.
67 | * @param remoteComponent Remote component
68 | * @param options (Optional) object of options.
69 | */
70 | public async loadStandaloneComponent(
71 | remoteComponent: StandaloneRemoteComponent,
72 | options?: LoadMfeOptions
73 | ): Promise> {
74 | try {
75 | if (this._cache.isRegistered(remoteComponent)) {
76 | return this._cache.getValue(remoteComponent);
77 | }
78 |
79 | this._cache.register(remoteComponent);
80 |
81 |
82 | const componentType = await this._ngZone.runOutsideAngular(() =>
83 | loadMfe(remoteComponent.app, remoteComponent.component, options)
84 | );
85 |
86 | this._cache.setValue(remoteComponent, componentType);
87 |
88 | return componentType;
89 | } catch (error: unknown) {
90 | this._cache.setError(remoteComponent, error);
91 | throw error;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/services/remote-components-cache.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { RemoteComponentsCache } from '.';
4 |
5 | describe('RemoteComponentsCache', () => {
6 | let service: RemoteComponentsCache;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(RemoteComponentsCache);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/services/remote-components-cache.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Type } from '@angular/core';
2 | import { AsyncSubject, lastValueFrom } from 'rxjs';
3 |
4 | import {
5 | isRemoteComponentWithModule,
6 | RemoteComponentWithModule,
7 | RemoteComponent,
8 | StandaloneRemoteComponent,
9 | isStandaloneRemoteComponent,
10 | } from '../interfaces';
11 | import { ComponentWithNgModuleRef } from '../types';
12 |
13 | /**
14 | * Cache of the loaded micro-frontend apps.
15 | *
16 | * Main reasons to create cache:
17 | * 1) Avoid race condition, when same micro-frontend are requested twice or more times at the same time.
18 | * 2) Cache already loaded MFE component and dont make same request twice.
19 | */
20 | @Injectable({
21 | providedIn: 'root',
22 | })
23 | export class RemoteComponentsCache {
24 | private readonly _map = new Map | Type>>();
25 |
26 | /**
27 | * Register a new micro-frontend cache.
28 | * @param remoteComponent Mfe Remote Component
29 | */
30 | public register(remoteComponent: RemoteComponent): void {
31 | if (this.isRegistered(remoteComponent)) return;
32 |
33 | const key = this.generateKey(remoteComponent);
34 | this._map.set(key, new AsyncSubject | Type>());
35 | }
36 |
37 | /**
38 | * Unregister a micro-frontend cache.
39 | * @param remoteComponent Mfe Remote Component
40 | */
41 | public unregister(remoteComponent: RemoteComponent): void {
42 | if (!this.isRegistered(remoteComponent)) return;
43 |
44 | const key = this.generateKey(remoteComponent);
45 | this._map.delete(key);
46 | }
47 |
48 | /**
49 | * Checks that specified micro-frontend app already registered.
50 | * @param remoteComponent Mfe Remote Component
51 | */
52 | public isRegistered(remoteComponent: RemoteComponent): boolean {
53 | const key = this.generateKey(remoteComponent);
54 | return this._map.has(key);
55 | }
56 |
57 | /**
58 | * Set to cache ComponentWithNgModuleRef of micro-frontend.
59 | * @param remoteComponent Mfe Remote Component
60 | * @param value ComponentWithNgModuleRef for that micro-frontend
61 | */
62 | public setValue(
63 | remoteComponent: RemoteComponentWithModule,
64 | value: ComponentWithNgModuleRef
65 | ): void;
66 |
67 | public setValue(
68 | remoteComponent: StandaloneRemoteComponent,
69 | value: Type
70 | ): void;
71 |
72 | public setValue(
73 | remoteComponent: RemoteComponent,
74 | value: ComponentWithNgModuleRef | Type
75 | ): void {
76 | if (!this.isRegistered(remoteComponent)) {
77 | throw new Error(
78 | `Error while trying to set value into MFE cache, this key - "${JSON.stringify(
79 | remoteComponent
80 | )}" does not exist in cache`
81 | );
82 | }
83 |
84 | if (isStandaloneRemoteComponent(remoteComponent)) {
85 | const cache = this.getCache(remoteComponent);
86 | cache.next(value as Type);
87 | cache.complete();
88 | return;
89 | }
90 |
91 | const cache = this.getCache(remoteComponent);
92 | cache.next(value as ComponentWithNgModuleRef);
93 | cache.complete();
94 | }
95 |
96 | /**
97 | * Sets the error that occurs in the loading and compiling micro-frontend.
98 | * @param remoteComponent Mfe Remote Component
99 | * @param error Error
100 | */
101 | public setError(remoteComponent: RemoteComponent, error: any): void {
102 | if (!this.isRegistered(remoteComponent)) {
103 | throw new Error(
104 | `Error while trying to set error into MFE cache, this key - "${JSON.stringify(
105 | remoteComponent
106 | )}" does not exist in cache`
107 | );
108 | }
109 |
110 | const cache = this.getCache(remoteComponent);
111 | cache.error(error);
112 | cache.complete();
113 | }
114 |
115 | /**
116 | * Gets ComponentWithNgModuleRef or Component Class of the micro-frontend.
117 | *
118 | * ---------------------
119 | * Returns ComponentWithNgModuleRef of MFE for component with module,
120 | * or returns Component Class of MFE for standalone component.
121 | * @param remoteComponent Mfe Remote Component
122 | */
123 | public getValue(
124 | remoteComponent: RemoteComponentWithModule
125 | ): Promise>;
126 |
127 | public getValue(
128 | remoteComponent: StandaloneRemoteComponent
129 | ): Promise>;
130 |
131 | public getValue(
132 | remoteComponent: RemoteComponent
133 | ): Promise> | Promise> {
134 | if (isStandaloneRemoteComponent(remoteComponent)) {
135 | const cache = this.getCache(remoteComponent);
136 | return lastValueFrom(cache);
137 | }
138 |
139 | const cache = this.getCache(remoteComponent);
140 | return lastValueFrom(cache);
141 | }
142 |
143 | /**
144 | * Gets the AsyncSubject cache value from Map
145 | * @param remoteComponent Mfe Remote Component
146 | */
147 | protected getCache(
148 | remoteComponent: RemoteComponentWithModule
149 | ): AsyncSubject>;
150 |
151 | protected getCache(
152 | remoteComponent: StandaloneRemoteComponent
153 | ): AsyncSubject>;
154 |
155 | protected getCache(
156 | remoteComponent: RemoteComponent
157 | ): AsyncSubject> | AsyncSubject> {
158 | const key = this.generateKey(remoteComponent);
159 | const value = this._map.get(key);
160 |
161 | if (!value)
162 | throw new Error(
163 | `Error MFE "${JSON.stringify(remoteComponent)}" does not exist in cache`
164 | );
165 |
166 | if (isStandaloneRemoteComponent(remoteComponent)) {
167 | return value as AsyncSubject>;
168 | }
169 |
170 | return value as AsyncSubject>;
171 | }
172 |
173 | /**
174 | * Generates a cache key based on RemoteComponent
175 | * @param remoteComponent Mfe Remote Component
176 | */
177 | protected generateKey(remoteComponent: RemoteComponent): string {
178 | if (isRemoteComponentWithModule(remoteComponent)) {
179 | return `${remoteComponent.app}/${remoteComponent.component}/${remoteComponent.module}`;
180 | }
181 |
182 | return `${remoteComponent.app}/${remoteComponent.component}`;
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/types/component-with-ng-module-ref.ts:
--------------------------------------------------------------------------------
1 | import { Type, NgModuleRef } from "@angular/core";
2 |
3 | export type ComponentWithNgModuleRef = {
4 | component: Type;
5 | ngModuleRef: NgModuleRef;
6 | }
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './component-with-ng-module-ref'
2 | export * from './mfe-outlet-inputs';
3 | export * from './mfe-outlet-outputs';
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/types/mfe-outlet-inputs.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Inputs that projects to micro-frontend component.
3 | */
4 | export type MfeOutletInputs = Record;
5 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/lib/types/mfe-outlet-outputs.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Outputs that projects to micro-frontend component.
3 | */
4 | export type MfeOutletOutputs = Record void>;
5 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/public-api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of ngx-mfe
3 | */
4 |
5 | export * from './lib/directives';
6 | export * from './lib/helpers';
7 | export * from './lib/injection-tokens';
8 | export * from './lib/interfaces';
9 | export * from './lib/mfe.module';
10 | export * from './lib/registry';
11 | export * from './lib/services';
12 |
13 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js';
4 | import 'zone.js/testing';
5 | import { getTestBed } from '@angular/core/testing';
6 | import {
7 | BrowserDynamicTestingModule,
8 | platformBrowserDynamicTesting
9 | } from '@angular/platform-browser-dynamic/testing';
10 |
11 | // First, initialize the Angular testing environment.
12 | getTestBed().initTestEnvironment(
13 | BrowserDynamicTestingModule,
14 | platformBrowserDynamicTesting(),
15 | { teardown: { destroyAfterEach: true }},
16 | );
17 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/lib",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "inlineSources": true,
9 | "types": [],
10 | "lib": [
11 | "dom",
12 | "es2018"
13 | ]
14 | },
15 | "exclude": [
16 | "src/test.ts",
17 | "**/*.spec.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.lib.json",
4 | "compilerOptions": {
5 | "declarationMap": false,
6 | "removeComments": true,
7 | "sourceMap": true,
8 | },
9 | "angularCompilerOptions": {
10 | "compilationMode": "partial"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/projects/ngx-mfe/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts"
12 | ],
13 | "include": [
14 | "**/*.spec.ts",
15 | "**/*.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "esModuleInterop": true,
9 | "strict": true,
10 | "noImplicitReturns": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "sourceMap": true,
13 | "declaration": false,
14 | "paths": {
15 | "ngx-mfe": [
16 | "dist/ngx-mfe/ngx-mfe",
17 | "dist/ngx-mfe"
18 | ]
19 | },
20 | "experimentalDecorators": true,
21 | "moduleResolution": "node",
22 | "importHelpers": true,
23 | "target": "ES2022",
24 | "module": "es2020",
25 | "lib": [
26 | "es2018",
27 | "dom"
28 | ],
29 | "useDefineForClassFields": false
30 | },
31 | "angularCompilerOptions": {
32 | "enableI18nLegacyMessageIdFormat": false,
33 | "strictInjectionParameters": true,
34 | "strictInputAccessModifiers": true,
35 | "strictTemplates": true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------