@ama-mfe/ng-utils
is an Angular library designed to streamline communication within a micro-frontend architecture that
uses iframes.
This package is built on the Amadeus Toolkit for Micro Frontends framework
and offers a suite of tools - including helpers, wrappers, and services - to facilitate seamless integration and
interaction between host and embedded applications.
Key features include:
- Connect: Connect to the communication protocol and send messages to registered applications.
- Navigation: Ensures the host application can update its navigation to reflect the embedded application's URL, maintaining consistency even after a page refresh.
- Theme: Allows the application of unified CSS variables and styles across embedded modules, ensuring a cohesive look and feel.
- Resize: Dynamically adjusts the iframe dimensions to fit the content of the embedded application, enhancing the user experience.
To install the package, run:
npm exec ng add @ama-mfe/ng-utils
In the communication protocol, a message is created by a Producer
and sent to a Consumer
which will read the message
and react according to its content.
The @ama-mfe/ng-utils
package exposes a set of features based on messages exchanged between a host application and its
embedded module. For each feature, the package provides a Producer
/Consumer
pair.
The former service is in charge of delivering the messages based on triggers (resize event, call to a public function
etc.) while the latter implements the logic behind the feature (resizing of the iframe, application of a theme, etc.).
Applications first need to provide and configure the ConnectionService
to use the communication protocol.
The provideConnection
method allows an application to register with a unique ID that others will use to
connect and target the application.
import {provideConnection} from '@ama-mfe/ng-utils';
export const appConfig: ApplicationConfig = {
providers: [
provideConnection({
id: 'applicationUniqueID'
})
]
};
An application embedded into another one can connect to its host using the [ConnectionService]. To establish the connection, the embedded application requires the host application id (set via the connection service's provider).
// main.ts
import {inject, runInInjectionContext} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {ConnectionService, NavigationConsumerService} from '@ama-mfe/ng-utils';
bootstrapApplication(AppComponent, appConfig)
.then((m) => {
runInInjectionContext(m.injector, () => {
if (window.top !== window.self) {
// If embedded in an iframe, connect to the host
inject(ConnectionService).connect('hostUniqueID');
}
// Other injections
})
});
Use the connect
directive to initiate the communication between your application and the module in the iframe.
The communication pipe will be closed once the iframe is destroyed.
<iframe [src]='myModuleUrl'
[connect]='myModuleUniqueID'>
</iframe>
In this example, myModuleUniqueID
refers to the id provided in the provideConnection
method.
To use a feature based on the message communication protocol, you need first to identify if your application will be a
user of the message (Consumer
) or the one sending the message (Producer
).
This may depend on the context and the type of message. For instance, an application can be the consumer of navigation
messages but the producer of theme messages.
If you are a consumer of the message, call the start
and stop
methods to respectively enable and disable the feature.
import {Component, inject} from '@angular/core';
import {NavigationConsumerService} from '@ama-mfe/ng-utils';
import {ThemeConsumerService} from "./theme.consumer.service";
@Component({
selector: 'app-example-module',
template: './example-module.template.html',
styleUrl: './example-module.style.scss',
})
export class ExampleModuleComponent {
private readonly navigationConsumerService = inject(NavigationConsumerService);
constructor() {
this.navigationConsumerService.start();
}
ngOnDestroy() {
this.navigationConsumerService.stop()
}
}
Depending on your use case, you might need to start the service as soon as your application start running.
In this case, you may inject it in the main.ts
:
// main.ts
import {inject, runInInjectionContext} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {ConnectionService, ThemeConsumerService} from '@ama-mfe/ng-utils';
bootstrapApplication(AppComponent, appConfig)
.then((m) => {
runInInjectionContext(m.injector, () => {
if (window.top !== window.self) {
// If embedded in an iframe, connect to the host
inject(ConnectionService).connect('hostUniqueID');
// Start the service to consume messages
inject(ThemeConsumerService).start();
}
// Other injections
})
});
If your application is a producer, just inject the message producer service and call the trigger when needed. There is no standardization on the name of the methods used to trigger a message. It will be different for each service.
You will find more information for each service in their respective README.md
:
Use the ProducerManagerService
and the ConsumerManagerService
to support your own custom messages.
A message should be identified by its type and a version to allow different message versions between the host and the embedded applications (and avoid migration issues).
import type {Message} from '@amadeus-it-group/microfrontends';
export interface CustomMessageV1_0 extends Message {
type: 'custom',
version: '1.0',
// Custom properties
customPayload: any
}
// Use union type here to add all the future version
// For example CustomMessage = CustomMessageV1_0 | CustomMessagev2_0
export type CustomMessageVersions = CustomMessageV1_0;
A consumer should implement the MessageConsumer
interface and inject the ConsumeManagerService
which handles the
registration to the communication protocol.
It should list the supported versions and map its callback function in a supportedVersions
public object.
import type {CustomMessageV1_0, CustomMessageVersions} from '@ama-mfe/messages';
import type {RoutedMessage} from '@amadeus-it-group/microfrontends';
import {DestroyRef, inject, Injectable} from '@angular/core';
import {ConsumerManagerService, type MessageConsumer} from '@ama-mfe/ng-utils';
@Injectable({
providedIn: 'root'
})
export class CustomConsumerService implements MessageConsumer<CustomMessageVersions> {
/**
* The type of messages this service handles ('custom').
*/
public readonly type = 'custom';
/**
* The supported versions of theme messages and their handlers.
*/
public readonly supportedVersions = {
'1.0': (message: RoutedMessage<CustomMessageV1_0>) => console.log('Do some stuff with this message version', message)
};
private readonly consumerManagerService = inject(ConsumerManagerService);
constructor() {
this.start();
inject(DestroyRef).onDestroy(() => this.stop());
}
/**
* Starts the theme handler service by registering it into the consumer manager service.
*/
public start() {
this.consumerManagerService.register(this);
}
/**
* Stops the theme handler service by unregistering it from the consumer manager service.
*/
public stop() {
this.consumerManagerService.unregister(this);
}
}
A producer should implement the MessageProducer
interface and inject the ProducerManagerService
which handles the
registration to the communication protocol.
Once connected, it is able to send messages via the MessagePeerService
.
import type {CustomMessageV1_0, CustomMessageVersions} from '../messages';
import {MessagePeerService} from '@amadeus-it-group/microfrontends-angular';
import {inject, Injectable} from '@angular/core';
import {type MessageProducer, type ErrorContent, registerProducer} from '@ama-mfe/ng-utils';
@Injectable({
providedIn: 'root'
})
export class CustomService implements MessageProducer<CustomMessageVersions> {
private readonly messageService = inject(MessagePeerService<CustomMessageVersions>);
constructor() {
registerProducer(this);
}
public handleError(message: ErrorContent<CustomMessage_V1_0>): void {
// If available, use your own logger
console.error('Error in custom service message', message);
}
public postMessageAction(payload: any): void {
const messageV10 = {
type: 'custom',
version: '1.0',
customPayload: 'test'
} satisfies CustomMessageV1_0;
this.messageService.send(messageV10);
}
}
A host application can send information to the embedded applications using parameters in the URL.
<iframe [src]="'myModuleUrl' | hostInfo: {hostId: 'host-app-id', moduleId: 'my-module-to-embed'}"></iframe>
This will add the location.origin
and the application id of the host to the URL of the embedded application.
The embedded application can access the data sent in the previous section using an injection token:
import {inject} from '@angular/core';
import {getHostInfo, isEmbedded} from '@ama-mfe/ng-utils';
export class SomeClass {
private readonly hostInfo = getHostInfo();
doSomething() {
if (this.hostInfo.applicationId === 'app1') {
// Do something when embedded in app1
} else if (isEmbedded()) {
// Do something when embedded elsewhere
} else {
// Do something when standalone
}
}
}
The host information is stored in session storage so it won't be lost when navigating inside the iframe.