AN INTRODUCTION TO WEB COMPONENTS WITH ANGULAR

Who am I?

Even Stack Overflow can't answer!

Ajit Kumar Singh

Doing Software Stuff  

I ❤️ the web with all its weirdness.

OUTLINE

  • Why Web Components?
  • Web Components
    • Shadow DOM
    • Custom Elements
  • Angular component as Web Component
    • Custom Elements and Angular
    • Angular Elements
    • Future

WHY WEB COMPONENTS?

  • Multiple rewrites of same components in different frameworks. e.g. date pickers
  • Consistency suffers e.g. multiple implementations of Material Design!

Different teams, different needs, different frameworks.

WEB COMPONENTS

WEB COMPONENTS

  • Custom elements: Creating and using new types of DOM elements.
  • Shadow DOM: Attach an encapsulated DOM tree to an element.
  • HTML Template: Markup remains inactive at page load, but can be instantiated later.
  • ES Module: Include and reuse of JS documents in other JS documents.

Umbrella term for collection of specifications:

WEB COMPONENTS

SHADOW DOM

SHADOW DOM

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

Shadow DOM enables local scoping for HTML & CSS.

SHADOW DOM

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.

  • No CSS bleeding.
  • Can have simple names.

CUSTOM ELEMENTS

Create your own reusable custom HTML tags.

// in JS
class MyCustomTag extends HTMLElement {
  ...
}

customElements.define('my-custom-tag', MyCustomTag);

Defined using an ES2015 class which extends HTMLElement.

CUSTOM ELEMENTS

// 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

CUSTOM ELEMENTS

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);
(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'});
            this.render();
        }

        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.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);
    }
)();

YEAH! BUT...

  • Too verbose API.
  • No data binding mechanism.
  • Expensive repaints.

YEAH! BUT...

Some pain points include:

YEAH! BUT...

Abstraction solutions:

Polymer / LitElement

SkateJS

Stencil.js

Angular

Vue.js

Svelte

CUSTOM ELEMENTS AND ANGULAR

Data Binding

Tooling

Dependency
Injection

Angular has to offer:

CUSTOM ELEMENTS AND ANGULAR

Angular Component and Custom Elements analogy:

Angular Component Custom Element
​@Input() ​Attributes / Properties
​@Output() ​CustomEvent()
Lifecycle Events Reactions

CUSTOM ELEMENTS AND ANGULAR

@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

ANGULAR ELEMENTS

ANGULAR ELEMENTS

Angular Element is a package, part of the Angular framework.

 

@angular/elements was introduced in Angular 6.

ANGULAR ELEMENTS

Convert your Angular component into a Custom Element.

  • NgElement - Abstract class which extends the HTMLElement class and implements the functionality needed for a custom element.
  • createCustomElement - Function that takes a component and returns its NgElement implementation.
createCustomElement<P>(component: Type<any>, config?: NgElementConfig): NgElementConstructor<P>

ANGULAR ELEMENTS

ng add @angular/elements

Adds elements & polyfill

Using Angular CLI

Getting dependencies:

  • Installs @angular/elements package
  • Installs document-register-element polyfill
  • Adds the polyfill to scripts in angular.json

ANGULAR ELEMENTS

Build changes needed:

// in tsconfig.json
{
  "compilerOptions": {
    ...
    "target": "es2015",
    ...
  }
}
"build": "ng build --prod --output-hashing none"

In package.json

ANGULAR ELEMENTS

@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:

ANGULAR ELEMENTS

@NgModule({
    declarations: [AppComponent],

    imports: [BrowserModule],

    // bootstrap: [AppComponent],

    entryComponents: [AppComponent]

})
export class AppModule {

}

Angular module:

ANGULAR ELEMENTS

import {createCustomElement} from "@angular/elements";
...

@NgModule({...})
export class AppModule implements DoBootstrap {

    constructor(private  injector: Injector) {}

    ngDoBootstrap() {

            const ngElement = createCustomElement(AppComponent, {injector: this.injector});

            customElements.define('ng-element', ngElement);
    }
}

Angular module:

ANGULAR ELEMENTS

DEMO TIME

THE FUTURE IS IVY.....

THE FUTURE IS IVY.....

import { Component, Input, Output, renderComponent } from '@angular/core';

@Component({
  selector: 'hello-world',
  template: `...`
})
class HelloWorld {
  @Input() name: string;
  @Output() nameChange = new EventEmitter();
  changeName = () => this.nameChange.emit(this.name);
}

renderComponent(HelloWorld);

THE FUTURE IS IVY.....

import { withNgComponent } from '@angular/elements';
import { HelloWorld } from './hello-world.component';

// create a Custom Element that wraps the Angular Component
const HelloWorldElement = withNgComponent(HelloWorld);

// register it
customElements.define('hello-world', HelloWorldElement);

THE FUTURE IS IVY.....

import { NgElement, withElement } from '@angular/elements';
...
@NgElement({
  selector: 'hello-world',
  template: `...`,
  providers: [SomeService],
  deps: [SomeDirective, SomePipe]
})
class HelloWorld extends withNgElement {}

THE FUTURE IS IVY.....

SO MUCH MORE, SO LESS TIME...

SO MUCH MORE, SO LESS TIME...

Further down the rabbit hole:

<thank-you></thank-you>

Made with Slides.com