Micro frontend with Angular elements
This is the how-to tutorial to generate custom element with Angular elements and inject into the legacy AngularJS project as well as pure HTML and vanilla Javascript.
Disclaimer: This article is adapted from “[Tutorial] How to create Custom Angular Elements?” by Alain Chautard. The additional steps are added for the build-time integration with vanilla Javascript and legacy AngularJS application
Prerequisites: I assume you have Node, Npm, and Angular CLI installed. If not, please follow the Angular document.
What are Angular elements?
Please see the description from Angular.io
Angular elements are Angular components packaged as custom elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way.
Custom elements are a Web Platform feature currently supported by Chrome, Edge (Chromium-based), Firefox, Opera, and Safari, and available in other browsers through polyfills (see Browser Support). A custom element extends HTML by allowing you to define a tag whose content is created and controlled by JavaScript code. The browser maintains a
CustomElementRegistry
of defined custom elements, which maps an instantiable JavaScript class to an HTML tag.The
@angular/elements
package exports acreateCustomElement()
API that provides a bridge from Angular's component interface and change detection functionality to the built-in DOM API.Transforming a component to a custom element makes all of the required Angular infrastructure available to the browser. Creating a custom element is simple and straightforward, and automatically connects your component-defined view with change detection and data binding, mapping Angular functionality to the corresponding native HTML equivalents.
Simply said that Angular elements are to create the custom HTML tag (like <image> or <video> tag) where it can encapsulate its feature and functionality inside this tag. The consumer, like plain HTML file and vanilla Javascript, can consume this new tag directly by importing and placing the tag in the file.
Generate Custom elements with Angular elements
- Create new Angular application.
ng new order
2. Add the @angular/elements package to your project. This will automatically install @angular/elements package.
ng add @angular/elements
3. Create new Angular component called buyback-order-history
ng generate component buyback-order-history
4. Create new Angular service called mock-api and generate the files inside the services folder under buyback-order-history folder.
ng g s buyback-order-history/services/mockApi
5. Back to the component, buyback-order-history, it will receive the input from the parent, emit the event to the parent, and call the service to retrieve the data. This component behaves as followings:
- receive the input through companyName attribute, which will be displayed in the template
- call the mock-api service with companyName to get the list of order numbers, which will be displayed in the template
- click at each order number, the selectOrder event will emit the order number to outer component
File: buyback-order-history.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';import { MockApiService } from './services/mock-api.service';@Component({
selector: 'app-buyback-order-history',
templateUrl: './buyback-order-history.component.html',
styleUrls: ['./buyback-order-history.component.scss']
})export class BuybackOrderHistoryComponent implements OnInit { @Input() companyName = '';
@Output() selectOrder: EventEmitter<string> = new EventEmitter<string>();
orders: string[] = []; constructor(private mockApiService: MockApiService) { } ngOnInit(): void {
this.getOrders(this.companyName);
} getOrders(companyName: string): void {
this.mockApiService.getOrders(companyName).subscribe(orders => this.orders = orders);
} onSelect(orderNumber: string): void {
this.selectOrder.emit(orderNumber);
}}
File: buyback-order-history.component.html
<h2>Orders from {{ companyName }}</h2>
<div>Please select your order:</div><ul class="orders">
<li *ngFor="let order of orders; index as i" (click)="onSelect(order)">
<span class="badge">{{ i + 1 }}</span> {{order}}
</li>
</ul>
File: buyback-order-history.component.scss
.orders {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}.orders li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}.orders li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}.orders .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #405061;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
6. The mock-api service simply exposes one method to provide the result as the observable of array of mock order numbers.
File: mock-api.service.ts
import { Injectable } from '@angular/core';
import { of } from 'rxjs/internal/observable/of';
import { Observable } from 'rxjs/internal/Observable';@Injectable({
providedIn: 'root'
})export class MockApiService { constructor() { } getOrders(companyName: string): Observable<string[]> {
return of(ORDERS);
}}const ORDERS: string[] = ['12345', '23456', '34567', '45678', '56789'];
7. In the previous version of Angular, we need to put the buyback-order-history into entryComponents as to tell Angular that these components are bootstrapped components. After Angular 9 and Ivy, entryComponents is deprecated.
Then we will create and define the custom elements from the Angular component in the ngDoBootstrap method of the AppModule.
File: app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, DoBootstrap, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';import { BuybackOrderHistoryComponent } from './buyback-order-history/buyback-order-history.component';@NgModule({
declarations: [BuybackOrderHistoryComponent],
imports: [BrowserModule],
providers: [],
bootstrap: []
})export class AppModule implements DoBootstrap { constructor(private injector: Injector) { } ngDoBootstrap(): void {
const buybackOrderHistoryComponent = createCustomElement(BuybackOrderHistoryComponent, { injector: this.injector });
customElements.define('ace-order-buyback-order-history', buybackOrderHistoryComponent);
}}
Please see the below explanation from Angular.io
Angular provides the
createCustomElement()
function for converting an Angular component, together with its dependencies, to a custom element. The function collects the component's observable properties, along with the Angular functionality the browser needs to create and destroy instances, and to detect and respond to changes.The conversion process implements the
NgElementConstructor
interface, and creates a constructor class that is configured to produce a self-bootstrapping instance of your component.
const buybackOrderHistoryComponent = createCustomElement(BuybackOrderHistoryComponent, { injector: this.injector });
Use a JavaScript function,
customElements.define()
, to register the configured constructor and its associated custom-element tag with the browser'sCustomElementRegistry
. When the browser encounters the tag for the registered element, it uses the constructor to create a custom-element instance.
customElements.define('ace-order-buyback-order-history', buybackOrderHistoryComponent);
8. This is done and ready to generate the artifact to be consumed by other applications. If you run ng build — prod, you will get quite a number of files. Still, this is not convenient for the consumers to consume. Next step, we will look at how to concatenate into one file. This way is derived from the following article by Alain Chautard.
9. Create a file, called build-elements.js, and put the following codes to concatenate the following files and put it in the predefined folder.
File: build-elements.js
const fs = require('fs-extra');
const concat = require('concat');(async function build() {
const files = [
'./dist/order/runtime.js',
'./dist/order/polyfills.js',
'./dist/order/main.js'
]; await fs.ensureDir('web-components'); await concat(files, 'web-components/buyback-order-history.js');})();
Next, update the command scripts in package.json to concatenate file after build
“build:package”: “ng build — prod — output-hashing=none && node build-elements.js”,
Now, try running the following command:
npm run build:package
The file will be concat and put in the defined folder, web-components. At this stage, you can ignore index.html file. In the next chapter, I will create this html file to consume this custom element.
Use Custom elements in pure HTML and vanilla Javascript
- Import the custom element (generated Javascript file)
<script src="buyback-order-history.js" type="module"></script>
2. Place the tag inside the <body> tag and pass the data as the input. Please see that the attribute name is in the dash separated format e.g. company-name and, in this case, I pass the hardcoded text to this custom input attribute
<ace-order-buyback-order-history id="buyback-order-history" company-name="Roxxon"></ace-order-buyback-order-history>
3. As the custom element emits the event, I need to add event listener to for that custom emitted event, selectOrder, as well as the callback.
<script type="text/javascript">
const el = document.getElementById("buyback-order-history");
el.addEventListener("selectOrder", (event) => { console.log(event.detail); });
</script>
File: index.html
<!doctype html><html lang="en"><head>
<meta charset="utf-8">
<title>Angular Web Component</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head><body>
<ace-order-buyback-order-history id="buyback-order-history" company-name="Roxxon"></ace-order-buyback-order-history>
<script src="buyback-order-history.js" type="module"></script>
<script type="text/javascript">
const el = document.getElementById("buyback-order-history");
el.addEventListener("selectOrder", (event) => { console.log(event.detail); });
</script>
</body></html>
Use Custom elements in legacy AngularJS application
Disclaimer: This below code example comes from my legacy AngularJS application. The tech stack is AngularJS on SAP Hybris. The implementation details might be little different but the concept should be the same.
- In my application, I need to inject the generated Javascript file (custom element) inside .tag file but, in your case, you can simply put in the html file.
<script type="text/javascript" src="${jsResourcePath}/web-components/buyback-order-history.js" type="module"></script>
2. To consume the custom element, just simply put it in the template file (html/jsp) and bind them with the appropriate variables (in case of input) or functions (in case of output).
<ace-order-buyback-order-history
company-name="{{companyName}}"
ng-on-select_order="printSelectedOrder($event)"
> </ace-order-buyback-order-history>
3. Inside the controller, I assign the variable (that I bind with the custom element input) with the hardcoded string and create the function to bind with the custom element output. It is interesting to see the name of the output which comes in the format of ng-on-<emitted event with ‘_’ separated value>
$scope.companyName = "Roxxon";
...
$scope.printSelectedOrder = function(event) {
console.log(event.detail)
}