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

@AngularConnect, November 2017
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,104