Angular Elements
and friends
Andrei Antal
ngBucharest
Netcentric Meetup, Bucharest, 20 Feb 2019
slides: bit.ly/2BF1cSX
Frontend Developer, ThisDot Labs
organizer for ngBucharest
@ngBucharest
groups/angularjs.bucharest
<template>
<h1> Hello <h1>
<div>
...content
</div>
</template>
<my-app>
<#shadow-root>
<div class="main">
<h1 class="title">
Hello
</h1>
...
<div>
</shadow-root>
</my-app>
<html>
<head>
<link
rel="import"
href="file.html">
</link>
</head>
</html>
Dropped in favor of ES Module spec
<my-app>
<custom-header />
<main-content />
</my-app>
<my-datepicker></my-datepicker>
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());
class MyElement extends HTMLElement {
set someValue(value) { ... }
get someValue() { return ... }
}
Properties
const myPicker = document.querySelector('my-dateicker');
elem.somevalue = "...";
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 */});
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:
Encapsulates
const myPicker = document.querySelector('my-dateicker');
elem.addEventListener('date-change', ev => { /* change */});
<angular-app>
...
<my-datepicker
[attr.locale]="someLocale"
[date]="someDate"
(dateChange)="changeDate()"
>
</my-datepicker>
...
</angular-app>
<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 Components packaged
as Web Components -
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
Bridging Angular and DOM APIs
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
@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}?.`);
}
}
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() {
}
}
import { platformBrowserDynamic } from '@angular/platform...';
import { enableProdMode } from '@angular/core';
import { CustomElementsModule } from './app';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic()
.bootstrapModule(CustomElementModule)
<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>
<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>
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
fingers crossed!
The magical, reusable web component compiler
The good
The not so good
What is it?
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>
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
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>
)
}
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>
);
}
}
Methods
import { Method } from '@stencil/core';
...
export class TodoList {
@Method()
showPrompt() {
// show a prompt
}
}
const todoListElement = document.querySelector('todo-list');
todoListElement.showPrompt();
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
}
}
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*/})
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);
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);
credits @julienrenaux
credits @julienrenaux
@andrei_antal
Reach me at:
antal.a.andrei@gmail.com
#community4thewin