Create Framework Agnostic Web Components in Angular
After the introduction to angular version 6, it became very easy to reuse your angular components outside your angular app. There is a nice article to know more about web components and shadow DOM.
Let ‘s see how we can create a framework agnostic web components.
Install Angular CLI and initialize angular
npm i -g @angular/cli
ng new elements-demo — prefix custom
Add elements
ng add @angular/elements
The @angular/elements
package exports a createCustomElement()
API that provides a bridge from Angular's component interface and change detection functionality to the built-in DOM API.
Create a component
ng g component button — inline-style — inline-template
import {Input,Component,ViewEncapsulation,EventEmitter,Output} from '@angular/core';@Component({selector: 'custom-button',template: `<button (click)="handleClick()">{{label}}</button>`,styles: [`button {border: solid 3px;padding: 8px 10px;background: #FFD700;font-size: 20px;}`],encapsulation: ViewEncapsulation.ShadowDom})export class ButtonComponent {@Input() label = 'Click me';@Output() action = new EventEmitter<number>();private clicksCt = 0;handleClick() {this.clicksCt++;this.action.emit(this.clicksCt);}}
We use ViewEncapsulation.ShadowDom
so that the styles are bundled with the template and the component’s class into one file.
We are creating a button component which we will register as a custom element.Here we have specified some styles for our button. We have an @Input and @Output properties to allow to share data between components.
Registering component in NgModule
import { Injector, NgModule } from '@angular/core';import { createCustomElement } from '@angular/elements';import { BrowserModule } from '@angular/platform-browser';import { ButtonComponent } from './button/button.component';@NgModule({declarations: [ButtonComponent],imports: [BrowserModule,],})export class AppModule {constructor(private injector: Injector){}ngDoBootstrap(){const customButton = createCustomElement(ButtonComponent, {injector: this.injector});customElements.define('custom-element', customButton);}}
If you are using angular version less than 9, you need to specify
entryComponents: [ButtonComponent]
As with Ivy, we do not need to specify entry components.Here we have used we use the Angular’s createCustomElement
function to create a class that can be used with browsers’ native customElements.define
functionality.
Since our ButtonComponent
is not a part of any other component, and is also not a root of an Angular application,we need to tell angular to use this module for bootstrapping, hence the ngDoBootstrap
method.
Build, optimize and run the code
To try our component out we will serve a simple html with http-server
, so let’s add it:
npm i -D http-server
In order to build we will use a standard ng build
command, but since it outputs 4 files (runtime.js
, scripts.js
, polyfills.js
and main.js
) and we’d like to distribute our component as a single js file, we need to turn hashing file names off to know what are the names of files to manually concatenate in a moment. Let’s modify the “build” script inpackage.json
and add “package” and “serve” entries:
"build": "ng build --prod --output-hashing=none",
"package":
"cat dist/elements-demo/{runtime,polyfills,scripts,main}.js
| gzip > elements.js.gz",
"serve": "http-server --gzip"
Now the sample [projectFolder]/index.html
:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Custom Button Test Page</title><script src="elements.js"></script></head><body><custom-button label="First Value"></custom-button><script>const button = document.querySelector('custom-button');button.addEventListener('action', (event) => {console.log(`"action" emitted: ${event.detail}`);})setTimeout(() => button.label = 'Second Value', 3000);</script></body></html>
And let’s see this in action!