AN INTRODUCTION TO WEB COMPONENTS WITH ANGULAR
Even Stack Overflow can't answer! 😱
Ajit Kumar Singh
Doing Software Stuff
Doing frontend stuff.
I ❤️ the web with all its weirdness.
Interoperability
Umbrella term for collection of specifications:
Source : https://www.webcomponents.org
The scope problem:
DOM, internal representation of a web page, is global in nature.
This lack of encapsulation results in overlapping CSS and conflicting IDs.
Shadow DOM enables local scoping for HTML & CSS.
Isolated DOM: A component's DOM is self-contained. Not accessible directly outside the shadow boundary.
Benefits:
Scoped CSS: CSS defined inside shadow DOM is scoped to it.
Create your own reusable custom HTML tags.
// in JS
class MyCustomTag extends HTMLElement {
...
}window.customElements.define('my-custom-tag', MyCustomTag);Custom tag must be lower case with a hyphen.
Defined using an ES2015 class which extends HTMLElement.
// in JS
class MyCustomTag extends HTMLElement {
constructor() {
// If you define a constructor, always call super() first!
super();
}
// Return array of strings where each string is the name of the attribute to observe.
static get observedAttributes() {...}
//Fired every time the component is added anywhere in the DOM.
connectedCallback() {...}
//Fired every time the component is removed from the DOM.
disconnectedCallback() {...}
//Called when an observed attribute has been added, removed, updated.
attributeChangedCallback(name, oldValue, newValue) {...}
}Reactions: fired at different points in the element's lifecycle
Communication with custom elements:
Attributes
<my-custom-tag id="myTag" increment="1"></my-custom-tag>Properties
Methods
Events
const myTag=document.getElementById('myTag');
myTag.time=new Date();const myTag=document.getElementById('myTag');
myTag.incrementBy(10);const detail={increment: this.increment};
const event = new CustomEvent("doAdd", {detail});
this.dispatchEvent(event);
const myTag=document.getElementById('myTag')
myTag.addEventListener('doAdd',(e)=>{console.log(e)});
(function () {
class MyCustomTag extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
this._total = 0;
this._time = new Date();
this.attachShadow({mode: 'open'});
}
static get observedAttributes() {
return ['increment'];
}
get increment() {
return this.getAttribute('increment');
}
set increment(increment) {
this.setAttribute("increment", increment);
this.render();
}
get time() {
return this._time;
}
set time(time) {
this._time = time;
this.render(); // Each render is replacing the innerHTML, better get hold of individual DOM node that needs repaint ?
this.addEventListener(); // because whole innerHTML is replaced, eventlisteners added to any node are also gone.
}
incrementBy(increment) {
this._total += Number(increment);
this.render(); // Each render is replacing the innerHTML, better get hold of individual DOM node that needs repaint ?
this.addEventListener(); // because whole innerHTML is replaced, eventlisteners added to any node are also gone.
}
connectedCallback() {
this.render();
this.addEventListener();
}
addEventListener() {
this.addButton = this.shadowRoot.getElementById('add');
this.addButton.addEventListener('click', (e) => this.doAddAction(e));
}
// Only if the attribute that changed is currently being observed.
attributeChangedCallback(attrName, oldVal, newVal) {
console.log('attributeChangedCallback called for', {attrName, oldVal, newVal});
this.render();
}
// If for example we delete the node or a parent node in the DOM tree, this function will fire since inherently it will remove the element from the DOM.
disconnectedCallback() {
console.log('disconnectedCallback called');
}
doAddAction(e) {
const event = new CustomEvent("doAdd", {...e, detail: {increment: this.increment}});
this.dispatchEvent(event);
}
render() {
this.shadowRoot.innerHTML = `<style>
.wrapper {
text-align: center;
background: orange;
display: block;
border: 5px red double;
}
p {
font-size: 20px;
}
h1 {
font-size: 35px;
color: red;
}
</style>
<div class="wrapper">
<h2>Custom Element</h2>
<p>I Increment by : <span>${this.increment}</span></p>
<button id="add">Click Me</button>
<h1>${this._total}</h1>
<h3>Time is ${this._time.getHours()}:${this._time.getMinutes()}:${this._time.getSeconds()}</h3>
</div>
`
}
}
if (!window.customElements.get('my-custom-tag')) {
window.customElements.define('my-custom-tag', MyCustomTag);
}
)();
Some pain points include:
Abstraction solutions:
Polymer
SkateJS
Stencil.js
Angular
Vue.js
Data Binding
CLI
Renderer
Dependency
Injection
Animation
Zones
Angular has to offer:
Angular Component and Custom Elements analogy:
| Angular Component | Custom Element | |
|---|---|---|
| @Input() | Attributes / Properties | |
| @Output() | CustomEvent() | |
| Lifecycle Events | Reactions | |
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnChanges, OnDestroy {
@Input() increment: number = 1;
@Input() time: Date = new Date();
@Output() doAdd = new EventEmitter<number>();
constructor() {}
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges): void {}
ngOnDestroy(): void {}
}Reactions
Attribute
Property
Custom Event
Source : https://i.gifer.com/KdY7.gif
Angular Element is a package, part of the Angular framework.
@angular/elements was introduced in Angular 6.
Convert your Angular component into a Custom Element.
createCustomElement<P>(component: Type<any>, config?: NgElementConfig): NgElementConstructor<P>ng add @angular/elementsUsing Angular CLI
Getting dependencies:
Build changes needed:
// in tsconfig.json
{
"compilerOptions": {
...
"target": "es2015",
...
}
}
"build": "ng build --prod --output-hashing none"In package.json
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
encapsulation: ViewEncapsulation.ShadowDom
})
export class AppComponent implements OnInit, OnChanges, OnDestroy {
@Input() increment: number = 1;
@Input() time: Date = new Date();
@Output() doAdd = new EventEmitter<number>();
constructor() {}
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges): void {}
ngOnDestroy(): void {}
}Angular component:
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
// bootstrap: [AppComponent],
entryComponents: [AppComponent]
})
export class AppModule {
}
Angular module:
import {createCustomElement} from "@angular/elements";
...
@NgModule({...})
export class AppModule implements DoBootstrap {
constructor(private injector: Injector) {}
ngDoBootstrap() {
const ngElement = createCustomElement(AppComponent, {injector: this.injector});
window.customElements.define('ng-element', ngElement);
}
}Angular module:
DEMO TIME
Using Custom Elements:
Demo Code: https://jamb.it/ng-element-code