@PascalPrecht

Angular Elements

Pascal Precht @PascalPrecht
@PascalPrecht
GDE for the Angular Team
pascalprecht.github.io/slides/angular-elements

Background

"Angular is ideal for building complete applications, and our tooling, documentation and infrastructure are primarily aimed at this case."
Rob Wormald, Angular Team™
"Angular is ideal for building complete applications, and our tooling, documentation and infrastructure are primarily aimed at this case."
Rob Wormald, Angular Team™
"...but it's quite challenging to use in scenarios that don't fit that specific Single Page Application model."
Rob Wormald, Angular Team™

Various use cases

  • Reusability - Reuse components across teams
  • CMS Pages - Embed standalone apps in CMS powered websites
  • Widgets - Use Angular components in other environments/projects/frameworks
  • ...

Web Components

Web Components

  • Templates - Templates in HTML
  • HTML Imports - Imports in HTML
  • Shadow DOM - DOM and Style Encapsulation
  • Custom Elements - Extend the browser vocabulary!

Web Components

  • Templates - Templates in HTML
  • HTML Imports - Imports in HTML
  • Shadow DOM - DOM and Style Encapsulation
  • Custom Elements - Extend the browser vocabulary!

Web Components

  • Templates - Templates in HTML
  • HTML Imports - Imports in HTML
  • Shadow DOM - DOM and Style Encapsulation
  • Custom Elements - Extend the browser vocabulary!

Web Components

  • Templates - Templates in HTML
  • HTML Imports - Imports in HTML
  • Shadow DOM - DOM and Style Encapsulation
  • Custom Elements - Extend the browser vocabulary!

Web Components

  • Templates - Templates in HTML
  • HTML Imports - Imports in HTML
  • Shadow DOM - DOM and Style Encapsulation
  • Custom Elements - Extend the browser vocabulary!

Web Components

  • Templates - Templates in HTML
  • HTML Imports - Imports in HTML
  • Shadow DOM - DOM and Style Encapsulation
  • Custom Elements - Extend the browser vocabulary!

Web Components

  • Templates - Templates in HTML
  • HTML Imports - Imports in HTML
  • Shadow DOM - DOM and Style Encapsulation
<loading-spinner></loading-spinner>

Custom Elements

Custom elements share the same API surface as native DOM objects:

  • Attributes
  • Properties
  • Methods
  • Events

Defining Custom Elements


            class LoadingSpinner extends HTMLElement {
              ...
            }

            customElements.define('loading-spinner', LoadingSpinner);
            

Custom elements are HTMLUnknownElement until upgraded.

Reactions


            class LoadingSpinner extends HTMLElement {
              ...
              connectedCallback() {
                ...
              }
              disconnectedCallback() {
                ...
              }
            }
            

Run code during interesting times of existence.

Attributes


            class LoadingSpinner extends HTMLElement {
              ...
              static get observedAttributes() {
                return ['mode'];
              }

              attributeChangedCallback(name, oldValue, newValue) {
                if (name === 'mode') {
                  // do something with newValue
                }
              }
            }
            

            <loading-spinner mode="indeterminate"></loading-spinner>
            

Properties


            class LoadingSpinner extends HTMLElement {
              ...
              get mode() {
                return this.getAttribute('mode');
              }

              set mode(val) {
                this.setAttribute('mode', val);
              }
            }
            

            let spinner = document.querySelector('loading-spinner');
            spinner.mode = 'indeterminate';
            

Custom Events


            class LoadingSpinner extends HTMLElement {
              ...
              emitModeChange() {
                this.dispatchEvent(new CustomEvent('mode-change', {
                  detail: this.mode
                }));
              }
            }
            

            let spinner = document.querySelector('loading-spinner');
            spinner.addEventListener('mode-change', event => { ... });
            

Custom Elements inside Angular


              <loading-spinner
                [mode]="mode"
                [attr.mode]="mode"
                (modeChange)="doSomething($event)"
                [value]="progress">
              </loading-spinner>
            

thumb_up Angular has been designed for this.

NG → CE

In fact, Angular and Custom Elements seem quite similar...

@HostBinding() Attributes
@Input() Properties
@Output() CustomEvent()
Lifecycle Hooks Reactions

What if...

Angular Elements

Angular Components glued together with Custom Elements.*

* kinda like Custom Elements on steroids; designed to work outside Angular apps

            @Component({
              selector: 'hello-world',
              template: `
                <h1>Hello {{name}}</h1>
                <input type="text" [(ngModel)]="name">
                <button (click)="changeName()">Save</button>
              `
            })
            class HelloWorld {
              @Input() name: string;
              @Output() nameChange = new EventEmitter();
              changeName() {
                this.nameChange.emit(this.name);
              }
            }
            

            import { NgModule } from '@angular/core';
            import { BrowserModule } from '@angular/platform-browser';
            import { HelloWorld } from './hello-world';

            @NgModule({
              imports: [BrowserModule, ...],
              declarations: [HelloWorld],
              entryComponents: [HelloWorld]
            })
            export class HelloWorldModule {
              ngDoBootstrap() {} // required in bootstrap module
            }
            

            import { HelloWorldModule } from './app';
            import { HelloWorld } from './hello-world';
            import { createCustomElement } from '@angular/elements';

            platformBrowserDynamic()
              .bootstrapModule(HelloWorldModule)
              .then(({injector}) => {
                const HelloWorldElement = createCustomElement(HelloWorld, {
                  injector
                });
                customElements.define('hello-world', HelloWorldElement);
              });
            

thumb_up Works in AoT as well!


            import { createCustomElement } from '@angular/elements';

            @NgModule({...})
            export class HelloWorldModule {
              constructor(private injector: Injector) {}
              ngDoBootstrap() {
                customElements.define(
                  'hello-world',
                  createCustomElement(HelloWorld, { injector: this.injector })
                );
              }
            }
            

            import { HelloWorldModule } from './app';
            import { HelloWorld } from './hello-world';

            platformBrowserDynamic().bootstrapModule(HelloWorldModule);
            
<hello-world name="Uphill Conf"></hello-world>

            const helloWorld = document.createElement('hello-world');
            document.body.appendChild(helloWorld);

            helloWorld.name = 'Pascal';
            helloWorld.setAttribute('name', 'Pascal');
            helloWorld.addEventListener('nameChange', event => {...});
            

            const HelloWorld = customElements.get('hello-world');
            const helloWorld = new HelloWorld();
            document.body.appendChild(helloWorld);

            helloWorld.addEventListener('nameChange', event => {...});
            

Angular Elements are the Host Element


            @Component({
              selector: 'hello-world',
              ...
            })
            export class HelloWorld {
              constructor(el: ElementRef) {
                el.nativeElement // <- Angular Element
              }

              @HostListener('click')
              onHostClick($event) { ... }

              @HostBinding('attr.someAttribute')
              someAttributeValue: string;
            }
            

Dependency Injection


            @NgModule({...})
            export class HelloWorldModule {
              constructor(private injector: Injector) {}
              ngDoBootstrap() {
                customElements.define(
                  'hello-world',
                  createCustomElement(HelloWorld, { injector: this.injector })
                );
              }
            }
            

Dependency Injection just works™

Content Projection


            <hello-world>
              <span>Some HTML right here</span>
            </hello-world>
            

            @Component({
              selector: 'hello-world',
              template: `
                <h1>Hello {{name}}!</h1>
                <ng-content></ng-content>
              `
            })
            export class HelloWorld {
              ...
            }
            

            <hello-world>
              <h1>Hello world!</h1>
              <span>Some HTML right here</span>
            </hello-world>
            

Slots + Shadow DOM


            @Component({
              selector: 'hello-world',
              template: `
                <h1>Hello {{name}}!</h1>
                <slot></slot>
              `,
              encapsulation: ViewEncapsulation.Native
            })
            export class HelloWorld {
              ...
            }
            

Experimental!

"Angular Component on the inside, Standards on the outside."
Rob Wormald, Angular Team™

Demo

Beyond v6 + Ivy


            @Component({
              selector: 'hello-world',
              template: '...',
              customElement: true
            })
            export class HelloWorld {
              ...
            }
            

Speculative API

Credits

  • Georgios Kalpakas
  • Rob Wormald
  • Andrei Antal

Thanks for your help!

Thank you.

Pascal Precht @PascalPrecht