I first introduced myself to web components
when I was working on a Polymer
project. You can read about polymer and how to integrate it with ASP.NET Core here in my three-part blog series.
However, this post is very straightforward. I'm just going to create a reusable Angular component and use it in a different eco-system; say for example in a normal static HTML web app. To do that we must register the component as a custom element.
Walk with me
Setting up an Angular project
Install the very latest version of Angular CLI with the following command,
npm install -g @angular/cli@latest
Once installed; do an ng -version
on the terminal to see if the CLI version is 6 or above.
Create a new Angular app with the following command,
ng new NgElement
The following command installs all the packages required to create custom elements in Angular,
ng add @angular/elements
The command also configures polyfills required to render the element in all major browsers.
Creating an Angular component
Following is the code snippet for a stand-alone subscriber
/newsletter
component made with Angular,
import {
Input,
Component,
ViewEncapsulation,
EventEmitter,
Output,
OnInit
} from '@angular/core';
import {
FormArray,
FormBuilder,
FormGroup,
FormControl,
Validators,
AbstractControl
} from '@angular/forms';
@Component({
selector: 'app-subscriber',
template: `
<form [formGroup]="newsletterForm" (submit)="onSubmit()">
<div class="container">
<label>{{title}}</label>
<input class="email" formControlName="email" id="email" type="email" placeholder="Your email"/>
<div *ngIf="(email.dirty || email.touched) && email.invalid">
<span *ngIf="email.errors.required">
<i class="error">email address is required.</i>
</span>
<span *ngIf="email.errors.email">
<i class="error">not a valid email addreess.</i>
</span>
</div>
<button class="button" type="submit">Subscribe!</button>
</div>
</form>`,
styles: [
`
.container{
background:#9c88ff;
width:400px;
height:200px;
display:flex;
flex-direction: column;
justify-content:center;
align-items:center;
border-radius:10px;
box-shadow: 0 5px 5px 0px #8c7ae6;
}
label{
margin: 10px;
font-family: 'Satisfy', cursive;
color: #fff;
font-size: 28px;
}
.email{
display: block;
width: 292px;
height: 30px;
font-size: 18px;
border-radius: 5px;
border: 0px;
padding: 4px;
text-align: center;
margin:5px;
font-family: 'Satisfy', cursive
}
.button{
width: 300px;
height: 38px;
background: #4cd137;
font-size: 18px;
border: 0px;
border-radius: 5px;
color: #fff;
margin:5px;
font-family: 'Satisfy', cursive;
}
.error{
color: #fff;
}
`
],
encapsulation: ViewEncapsulation.Native
})
export class SubscriberComponent implements OnInit {
@Input() title = 'default label';
@Output() subscribe = new EventEmitter<string>();
newsletterForm: FormGroup;
get email() {
return this.newsletterForm.get('email');
}
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.newsletterForm = new FormGroup(
{
email: new FormControl('', {
validators: [Validators.required, Validators.email]
})
},
{ updateOn: 'submit' }
);
}
onSubmit() {
if (this.newsletterForm.valid) {
this.subscribe.emit(this.email.value);
}
}
}
Things to notice,
To ship this as an Angular element, the view encapsulation of the component should be set to native.
encapsulation: ViewEncapsulation.Native
The component takes an
@Input
i.e.title
. The title of the component can be passed from the parent component/dom within an attribute.@Output subscribe
emits the enteredemail
address to its parent component/dom when the newsletter form is submitted.
Registering as a custom element
The following snippet is from the app.module.ts
file,
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { AppComponent } from './app.component';
import { SubscriberComponent } from './subscriber/subscriber.component';
import { createCustomElement } from '@angular/elements';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [AppComponent, SubscriberComponent],
imports: [BrowserModule, ReactiveFormsModule],
providers: [],
entryComponents: [SubscriberComponent]
})
export class AppModule {
constructor(private injector: Injector) {
const subscriber = createCustomElement(SubscriberComponent, {
injector
});
customElements.define('app-subscriber', subscriber);
}
ngDoBootstrap() {}
}
Things to notice
entryComponent
array defines the set of components you want to ship as custom elements.Notice there is no bootstrap array
bootstrap: [AppComponent]
; we will self-bootstrap theAppModule
, hence we havengDoBootstrap()
createCustomElement
turns an Angular component into a custom element and we delegate the root injector into it.customElements.define
method registers the custom element with a custom tag e.gapp-subscriber
Rendering the custom element
Inside Angular
You can render the component inside your index.html
with the following code,
<app-subscriber title="Newsletter!"></app-subscriber>
<script>
const subscriber = document.querySelector('app-subscriber');
subscriber.addEventListener('subscribe', (event) => {
console.log(`${event.detail}`);
})
</script>
To grab the emitted value from the subscribe
, we attached a vanilla js event listener with the element. Once we have some value popup in the subscribe
we can read it from the event.detail
property.
Inside other frameworks
Out of the Angular app to render the element, we can run the following build command to have published script of the element,
ng build --prod
It will emit the published script inside the dist
folder of your Angular app.
With other frameworks, (for me, it's a static HTML app) you can just add the generated scripts inside the <body>
tag as follows,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#output {
margin-top: 50px;
}
</style>
</head>
<body>
<app-subscriber title="Newsletter!"></app-subscriber>
<div id="output">
<p>Output:</p>
</div>
<script src="/NgElement/polyfills.c72d3210425a88b28b6d.js"></script>
<script src="/NgElement/runtime.6afe30102d8fe7337431.js"></script>
<script src="/NgElement/scripts.69c39fe5fecacc5138f1.js"></script>
<script src="/NgElement/main.954d51cb4b7f27e2e6f6.js"></script>
<script>
const subscriber = document.querySelector('app-subscriber');
subscriber.addEventListener('action', (event) => {
var content = document.createTextNode(event.detail);
var output = document.getElementById("output");
var br = document.createElement("br");
output.appendChild(content);
output.appendChild(br);
})
</script>
</body>
</html>
For a local development server, you can use the
http-server
package from npm. To install the package, run the following command,
npm i -g http-server
.You can initiate the server within your static HTML folder just by typing
http-server
from the console.
Running it will give you the following output
Repository
https://github.com/fiyazbinhasan/Angular-Elements