ANGULAR MEETUP - LEIPZIG #10
PRACTICAL INTRODUCTION TO ANGULAR ELEMENTS
Even Stack Overflow can't answer! 😱
Ajit Kumar Singh
Doing Software Stuff
Doing frontend stuff.
I ❤️ web with all its weirdness.
First time speaking in a Meetup. 😰
Hard to keep up.
Locked into, very hard upgrade path — causing application re-write.
The lifespan of frameworks.
Different teams, different needs, different frameworks.
Our components cannot be used by other teams not using the same framework.
Idea: To make it easier to build components that are reusable across frameworks.
Problem: It is impossible to make all different frameworks to arrive at and agree
on a common contract.
Proposal: Since all these frameworks rely on DOM, in order to be reusable across frameworks, our components should be indistinguishable from HTML/DOM elements.
What benefits would there be?
Is there any standard and non "hacky" way of writing components reusable across frameworks?
Umbrella term for collection of specifications:
<audio src="sound.mp3" controls></audio>Browsers already provide quite a few such reusable tags.
Source : https://www.webcomponents.org
The global scope of DOM and the Multiple-actor Problem (MAP):
(MAP) occurs when multiple bits of code assume that they are the only actor and these assumptions conflict with each other.
DOM, internal representation of a web page, is global in nature.
This lack of encapsulation results in conflicting CSS, overlapping IDs and so on.
In a nutshell, Shadow DOM enables local scoping for HTML & CSS.
Isolated DOM: A component's DOM is self-contained. Not accessible directly outside the shadow boundary.
// query over regular document
document.getElementById('inShadow'); // null
// Can only access via shadowRoot
const shadowRoot=document.getElementById('container').shadowRoot;
shadowRoot.getElementById('inShadow').innerText; // "Say Please?"Benefits:
Scoped CSS: CSS defined inside shadow DOM is scoped to it.
Benefits:
This is how browsers have been doing it:
<audio src="audio.mp3" controls></audio>The host to put all this in is still missing.
Where is the shadow host, how about some behaviours?
Create your own reusable custom HTML tags.
// in JS
class MyCustomTag extends HTMLElement {
...
}
window.customElements.define('my-custom-tag', MyCustomTag);Until the upgrade element gets parsed as HTMLUnknownElement.
The process of giving class definition to the tag is called "element upgrades".
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();
}
//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)});
// in JS
class MyCustomTag extends HTMLElement {
...
// Return array of strings where each string is the name of the attribute to observe.
// If attribute is NOT in this array,
// component will not respond to ADD/REMOVE/CHANGE of that attribute.
static get observedAttributes() {
return ['increment'];
}
// Fired when any of the attributes listed in "observedAttributes()" change.
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'increment') {
// do something with newValue
}
}
}Attributes:
<my-custom-tag id="myTag" increment="1"></my-custom-tag>// in JS
class MyCustomTag extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
this._time = new Date();
}
get time() {
return this._time;
}
set time(time) {
this._time = time;
}
}
Properties:
const myTag=document.getElementById('myTag');
myTag.time=new Date();Can we take advantage of these specs while working with Angular?
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
So how can we convert our angular component to custom element and able to use it outside Angular?
Source : https://i.gifer.com/KdY7.gif
Angular Element is a package, part of the Angular framework.
@angular/elements was introduced in Angular 6.
"Angular Component on the inside, standards on the outside."
Rob Wormald, Angular Team™
Convert your Angular component into a Custom Element.
Provides class and function that do the bridging between Angular and Custom Elements.
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
Build changes needed:
ng build command outputs 4 files:
We’d like to distribute our component as a single javascript file.
"build": "ng build --prod --output-hashing none && <Your concatenation script>"Angular component:
@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 {}
}@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);
}
}Method that transforms Angular component into Custom Element.
Angular component
Custom Element
Angular module:
DEMO TIME
@Component({
selector: 'hello-world',
template: '...',
})
export class HelloWorld {
...
}
customElement: true
SPECULATIVE
What were all the steps again?
What about the bundle size though?
Ivy: Angular's upcoming rendering engine.
Further down the rabbit hole:
Using Custom Elements:
Further down the rabbit hole:
Demo Code: https://jamb.it/ng-element-code