Web components as a compile target

Angular Elements

and friends

Andrei Antal

ngBucharest

Netcentric Meetup, Bucharest, 20 Feb 2019

slides: bit.ly/2BF1cSX

Hello World!

Andrei Antal

@andrei_antal

  • frontend engineer, since i can remember
  • web & JS technologies enthusiast
  • perpetual learner

Frontend Developer, ThisDot Labs

organizer for ngBucharest

@ngBucharest

groups/angularjs.bucharest

Angular Labs

@AngularMIX, October 2017

http://youtu.be/ljsOPm4MMEo

@AngularConnect, November 2017

http://youtu.be/ljsOPm4MMEo

There's no excuse for not using Angular on your next project...

...but it might get challenging to use if your app is not a SPA

Rob Wormald, Angular Team (paraphrase)

Angular Components can be tough to use outside Angular

Rob Wormald, Angular Team

Why components outside of Angular?

Web Components Standard

HTML Templates
Shadow DOM

HTML Imports
Custom Elements

Web Components Standard

-> HTML Templates
Shadow DOM
HTML Imports
Custom Elements

<template>
  <h1> Hello <h1>
  <div> 
    ...content 
  </div>
</template>

Web Components Standard

HTML Templates
-> Shadow DOM
HTML Imports
Custom Elements

<my-app>
  <#shadow-root>
    <div class="main">
      <h1 class="title">
        Hello
      </h1>
      ...
    <div>
  </shadow-root>
</my-app>

Web Components Standard

HTML Templates
Shadow DOM
-> HTML Imports
Custom Elements

<html>
  <head>
    <link
      rel="import"
      href="file.html">
    </link>
  </head>
</html>

Dropped in favor of ES Module spec

Web Components Standard

HTML Templates
Shadow DOM
HTML Imports
-> Custom Elements

<my-app>
  <custom-header />
  <main-content />
</my-app>

The Web Componet promise

<my-datepicker></my-datepicker>

Web Components and the DOM

class MyDatepicker extends HTMLElement {
    
  static observedAttributes = ['my-date'];

  attributeChangedCallback(oldvalue, newvalue, key) {
    // update the DOM
  }
}

Attributes

<my-datepicker date="7/12/2017"> </my-datepicker>
const myPicker = document.querySelector('my-dateicker');
myPicker.setAttribute('my-date', new Date().toString());

Web Components and the DOM

class MyElement extends HTMLElement {
 
  set someValue(value) { ... }
 
  get someValue() { return ... }
}

Properties

const myPicker = document.querySelector('my-dateicker');

elem.somevalue = "...";

Web Components and the DOM

class MyDatepicker extends HTMLElement {
    
  emitDateChange() {
    let datechangeEv = 
      new CustomEvent('date-change', {dateDetails});

    this.dispatchEvent(datechangeEv);
  }
}

Events

const myPicker = document.querySelector('my-dateicker');

elem.addEventListener('date-change',() => { /* change */});

Web Components and the DOM

class MyDatepicker extends HTMLElement {
   
  connectedCallback() { ... }
  
  disconnectedCallback() { ... }
  
  attributeChangedCallback(
    attributeName,
    oldValue,
    newValue,
    namespace ) { ... }
  
  adoptedCallback(oldDocument, newDocument)
}

Lifecycle hooks

<body>
....
<my-datepicker date="02/02/2018"></my-datepicker>
...
</body>

Exposes:

  • attributes/properties
  • bindable events

Encapsulates

  • template (structure)
  • styles
  • logic
const myPicker = document.querySelector('my-dateicker');

elem.addEventListener('date-change', ev => { /* change */});

Web Components work with Angular out of the box

<angular-app>
  ...
  <my-datepicker
    [attr.locale]="someLocale"
    [date]="someDate"
    (dateChange)="changeDate()"
  >
  </my-datepicker>
  ...
</angular-app>

Why not web components everywhere?

Why not Polymer?

<iron-ajax
    auto
    url="https://www.googleapis.com/youtube/v3/search"
    params='{"part":"snippet", "q":"polymer", "key": "YOUTUBE_API_KEY", "type": "video"}'
    handle-as="json"
    on-response="handleResponse"
    debounce-duration="300"></iron-ajax>

Angular ecosystem

Angular Components can be tough to use outside Angular   

Angular Elements

- Angular Components packaged

as Web Components -

How does it work?

  • Hosting Angular Components inside Custom Elements (NgElement) - "Angular on the inside, standards on the outside"

  • Bridge between Angular Components and DOM

    • @Inputs - properties

    • @HostBinding - attributes

    • @Outputs - events

    • Lifecycle hooks

  • Generate a bundle.js file than you need to include in your app

  • Use it in any application - Angular, static HTML, React, Vue, etc.

  • Self-bootstrapping - drop the element on the page and it works

@Component()
NgElement
@HostBinding
@Input()
@Output()
Lifecycle Hooks
Attributes
Properties
Events
Reactions

"compile"

register as Custom Element

How does it work?

Bridging Angular and DOM APIs

How does it work?

How does it work?

Creating an Angular Elements project

ng new custom-element
ng add @angular/elements --project=custom-element

1. Create a new project with the Angular CLI

2. Add the Angular Elements Schematics

ng generate component NgComponent

3. Generate a simple component

npm install @webcomponents/webcomponentsjs

4. (Optional) Install web components polyfill

Write your average Angular Component

@Component({
  selector: 'ng-component',
  template: `
    <h1> Hello World, my name is {{myName}} ! </h1>
    <button (click)="onClick()">Hi !</button>
  `,
  styles: [...],
  encapsulation: ViewEncapsulation.Native,
})
export class NgComponent implements OnInit {
  @Input() myName;
  @Output() sayHi = new EventEmitter<string>();

  ngOnInit() {
    this.myName = 'Andrei'
  }

  onClick() {
    this.sayHi.emit(`Yo, wassup ${this.myName}?.`);
  }
}

Create the Components Module

import { MyNgComponent } from './ng-component';
import { createCustomElement } from '@angular/elements';

@NgModule({
  imports: [BrowserModule],
  declarations: [MyNgComponent],
  entryComponents: [MyNgComponent]
})
export class CustomElementsModule {
  constructor(private injector: Injector) {
    const el = createCustomElement(MyRatingComponent,
      {injector : this.injector});
    customElements.define('my-rating', el);
  }
  ngDoBootstrap() {
  }
}

Register the custom elements


import { platformBrowserDynamic } from '@angular/platform...';
import { enableProdMode } from '@angular/core';
import { CustomElementsModule } from './app';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic()
    .bootstrapModule(CustomElementModule)

Consume the custom element

<head>
  ...
  <script src="my-ngComponent.bundle.js">
  </script>
  ...
</head>
<body>
  ...
  <ng-component></ng-component>
  ...
</body>
<head>
  ...
  <script src="mini-angular.js"></script>
  <script src="my-ngComponent.js"></script>
  ...
</head>
<body>
  ...
  <my-ngComponent></my-ngComponent>
  ...
</body>

Looks familiar?

<head>
  ...
  <script src="jquery.min.js"></script>
  <script src="jquery.my-datepicker.js">
  </script>
  ...
</head>
<body>
  <div class="datepicker"></div>
  <script>
    $('.datepicker').myDatepicker({...})
  </script>
</body>

Using ngx-build-plus

ng add ngx-build-plus

1. Add the ngx-build-plus library

"build:elements": "ng build --prod 
   --output-hashing none 
   --single-bundle true 
   --keep-polyfills"

2. Add a npm script to build the elements

3. Use generated files

Using ngx-build-plus

DEMO TIME!

fingers crossed!

The magical, reusable web component compiler

Ionic Framework

Ionic Framework

The good

  • Open source, cross-platform UI framework
  • Based 100% on web technologies (HTML5, CSS, JS)
  • Used to develop Native mobile apps, alongside Cordova

The not so good

  • Only uses Angular
  • Big bundles - problematic for websites or PWA

STENCIL

What is it?

  • Stencil is a compiler that generates Web Components (Custom elements)
  • Some features:
    • Written in TypeScript
    • Uses Virtual DOM
    • Async rendering (inspired by React Fiber)
    • Reactive data-binding
    • JSX for templating

STENCIL

A simple component

import { Component, Prop } from '@stencil/core';

@Component({
  tag: 'my-component',
  styleUrl: 'my-first-component.scss'
})
export class MyComponent {
  @Prop() name: string;

  render() {
    return (
      <p>
        Hello {this.name}
      </p>
    );
  }
}
<my-component name="World"></my-component>

STENCIL

Using JSX

render() {
  return (
    <div>Hello {this.name}</div>
  )
}
render() {
  return (
    <div>
    {this.name
      ? <p>Hello {this.name}</p>
      : <p>Hello World</p>
    }
    </div>
  );
}

JSX - Conditional rendering

STENCIL

JSX - Slots

// child component

render(){
  return [
    <slot name="item-start" />,
    <h1>Here is my main content</h1>,
    <slot name="item-end" />
  ]
}
// parent component

render(){
  return(
    <my-component>
      <p slot="item-start">I'll be placed before the h1</p>
      <p slot="item-end">I'll be placed after the h1</p>
    </my-component>
  )
}

STENCIL

JSX - Events

export class MyComponent {
  
  handleClick(event: UIEvent) {
    alert('Received the button click!');
  }

  render() {
    return (
      <button onClick={this.handleClick(event).bind(this)}>
        Click Me!
      </button>
    );
  }
}

STENCIL

Methods

import { Method } from '@stencil/core';

...
export class TodoList {

  @Method()
  showPrompt() {
    // show a prompt
  }
}
const todoListElement = document.querySelector('todo-list');
todoListElement.showPrompt();

STENCIL

State

import { State } from '@stencil/core';

...
export class TodoList {
  @State() completedTodos: Todo[];

  completeTodo(todo: Todo) {
    
    this.completedTodos = [...this.completedTodos, todo]; 
  }

  render() {
    // render the todo list
  }
}

STENCIL

Events

import { Event, EventEmitter } from '@stencil/core';

...
export class TodoList {

  @Event() completed: EventEmitter;

  completedHandler(todo: Todo) {
    this.todoCompleted.emit(todo);
  }
}
const todoListElement = document.querySelector('todo-list');
todoListElement.addEventListener('completed', (ev) => {/*handle*/})

STENCIL

Styling

@Component({
  tag: 'shadow-component',
  styleUrl: 'shadow-component.css',
  shadow: true
})
export class ShadowComponent {

}
this.el.shadowRoot.querySelector()
// global styles
:root {
  --app-primary-color: #488aff;
}

// shadow-component.css
h1 {
  color: var(--app-primary-color)
}

SkateJS

Effortless custom elements for modern view libraries.

import { props, withComponent } from 'skatejs';
import withReact from '@skatejs/renderer-react';
import React from 'react';

class WithReact extends withComponent(withReact()) {
  static get props() {
    return {
      name: props.string
    };
  }
  render({ name }) {
    return <span>Hello, {name}!</span>;
  }
}

customElements.define('with-react', WithReact);

SkateJS React renderer

import { props, withComponent } from 'skatejs';
import withLitHtml from '@skatejs/renderer-lit-html';
import { html } from 'lit-html';

class WithLitHtml extends withComponent(withLitHtml()) {
  static get props() {
    return {
      name: props.string
    };
  }
  render({ name }) {
    return html`Hello, ${name}!`;
  }
}

customElements.define('with-lit-html', WithLitHtml);

SkateJS  lit-html  renderer

credits @julienrenaux

credits @julienrenaux

What's next?

What's next?

The future?

Microfrontends

Thanks!

@andrei_antal

Reach me at:

antal.a.andrei@gmail.com

#community4thewin

Web Components as a compile target

By Andrei Antal

Web Components as a compile target

20 Feb 2019 @Netcentric meetup, Bucharest

  • 1,051