Create Framework Agnostic Web Components in Angular

Deeksha Bilochi
3 min readFeb 1, 2021

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.definefunctionality.

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!

--

--