Web Components with Angular:
The What, the Why and the How.
@SherrryLst | @AnaCidre_
Hello!
I’m Sherry List
AKA Sherry Berry 🍓
Senior frontend developer, Nordea
Women Techmaker Lead
You can find me at @SherrryLst
Hi!
I’m Ana Cidre
AKA Ana Banana 🍌
Developer Advocate, Ultimate Angular
Women Techmaker Lead
You can find me at @AnaCidre_
Angular Components
Angular Components
Web
Components
Angular Components
Web
Components
Creating Web Components
With Angular
Angular Components
Web
Components
Creating Web Components
With Angular
Adding Web Components to Angular
Angular Components
What’s missing:
What’s missing:
What’s missing:
What’s missing:
What does have all these things?
What does?
Web Components
provides a lot of this
A component that is platform agnostic
Their main goal is to encapsulate the code for the components into a nice, reusable package for maximum interoperability.
Check out https://custom-elements-everywhere.com/
Web components consist of three main technologies:
<template> </template>
Web components consist of three main technologies:
1
index.html
<template id="red-strawberry-template">
<div class="template__container">
<img class="template__image">
<div class="template__info">
<span></span>
</div>
</div>
</template>
index.html
<div id="image-gallery" class="template__grid">
<!-- existing image containers will be included here -->
</div>
index.js
const template = document.querySelector('#red-strawberry-template');
index.js
const template = document.querySelector('#red-strawberry-template');
// Add the first image
const imgTag = template.content.querySelector("img");
imgTag.src = "strawberry.jpg";
imgTag.alt = "Strawberries on the table";
index.js
const template = document.querySelector('#red-strawberry-template');
// Add the first image
const imgTag = template.content.querySelector("img");
imgTag.src = "strawberry.jpg";
imgTag.alt = "Strawberries on the table";
// Add the first content
const info = template.content.querySelector("span");
info.textContent = "Stawberries from Sherry's garden";
index.js
// Clone the new gallery and insert the DOM
const imageGallery = document.querySelector("#image-gallery");
index.js
// Clone the new gallery and insert the DOM
const imageGallery = document.querySelector("#image-gallery");
const clone = document.importNode(template.content, true);
index.js
// Clone the new gallery and insert the DOM
const imageGallery = document.querySelector("#image-gallery");
const clone = document.importNode(template.content, true);
imageGallery.appendChild(clone);
Web components consist of three main technologies:
2
aka custom elephants 🐘
Naming your custom element
<red-button></red-button>
<my-unique-wc></my-unique-wc>
🐘
Naming the attributes
<my-unique-wc
title="I am a Title"
xyz="No idea what I am!">
</my-unique-wc>
🐘
Custom elements lifecycle
Source: https://youtu.be/j-5uodRMW_Q
Native | Angular | React | Vue |
constructor | ngOnInit | constructor | created |
connectedCallback | ngAfterContentChecked | componentDidMount | mounted |
disconnectedCallback | ngOnDestroy | componentWillUnmount | destroy |
attributeChangedCallback | ngOnChanges | componentDidUpdate | updated |
index.html
<bb-red-strawberry
img="strawberry.jpg"
description="Strawberries from Sherry's Garden">
</bb-red-strawberry>
index.js
class BBRedStrawberryElement extends HTMLElement {
constructor() {
super();
}
}
index.js
class BBRedStrawberryElement extends HTMLElement {
constructor() {
super();
}
}
// Define custom element
customElements.define("bb-red-strawberry", BBRedStrawberryElement);
index.js
connectedCallback() {
this.innerHTML = template;
this._$image = this.querySelector("#element-image");
this._$description = this.querySelector("#element-description");
this._render(this);
}
index.js
_render({ img, description }) {
description = description || "Description is missing";
this._$image.alt = description;
this._$image.src = img || "missing-image.jpg";
const figcaption = document.createElement("figcaption");
figcaption.textContent = description;
figcaption["aria-label"] = "product name";
this._$description.appendChild(figcaption);
}
index.js
…
static get observedAttributes() {
return ["description", "img"];
}
attributeChangedCallback(name, oldValue, newValue) {
this[name] = newValue;
}
...
index.html
...
<style>
...
p {
font-family: 'Lato', sans-serif;
font-size: 0.9em;
max-width: 760px;
line-height: 1.6em;
}
...
index.html
...
<style>
...
p {
font-family: 'Lato', sans-serif;
font-size: 0.9em;
max-width: 760px;
line-height: 1.6em;
color: red;
}
...
Web components consist of three main technologies:
3
index.js
class BBRedStrawberryElement extends HTMLElement {
constructor() {
super();
const template = document.createElement("template");
// Shadow DOM
this.attachShadow({ "mode": "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
Slots
index.js
...
<img id="shadow-image" class="shadow__image">
<div id="shadow-info" class="shadow__info">
<slot name="title" id="title" role="header" class="shadow__title"></slot>
<slot name="description" id="description" class="shadow__description"></slot>
</div>
...
index.html
<bb-red-strawberry
img="strawberry.jpg"
style="--background-color: #A11B38; --color:white">
<div slot="title"><i>Strawberries</i></div>
<div slot="description">Sherry's berries finest strawberries</div>
</bb-red-strawberry>
CSS Custom Properties
index.js
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.container {
width: 320px;
height: 280px;
background-color: var(--background-color, #fff);
}
index.html
<bb-red-strawberry
img="strawberry.jpg"
style="--background-color: #A11B38;">
<div slot="title"><i>Strawberries</i></div>
<div slot="description">Sherry's berries finest strawberries</div>
</bb-red-strawberry>
:host
index.js
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host(bb-red-strawberry) {
/* Applies if the host is a <bb-red-strawberry> element.*/
font-weight: bold;
width: 320px;
height: 280px;
background-color: var(--background-color, #fff);
box-shadow: 0 2px 2px 0 rgba (0, 0, 0, .14);
margin-bottom: 2%;
border-radius: 2px;
}
index.html
...
<style>
...
p {
font-family: 'Lato', sans-serif;
font-size: 0.9em;
max-width: 760px;
line-height: 1.6em;
color: red;
}
...
lit-html
Check out: https://youtu.be/Io6JjgckHbg
index.js
import { render, html } from 'lit-html';
class BbRedStrawberryElement extends HTMLElement {
constructor() {
super();
const template = document.createElement("template");
this.attachShadow({ "mode": "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
index.js
connectedCallback() {
render(this._render(this), this.shadowRoot);
}
index.js
connectedCallback() {
render(this._render(this), this.shadowRoot);
}
// Defines element markup
_render({ img, title, description, productPrice, color }) {
return html
index.js
<div class="main-container">
<div id="photo">
<img src=${ img || "missing.jpg" } alt=${ title || ‘N/A’ }></img>
</div>
<div id="info">
<h3 id="title" aria-label="product title">${ title || 'N/A' }</h3>
${ description ? html`<p id="description" aria-label="product description">${ description || 'N/A' }
</p>` : '' }
${ productPrice ? html` <p id="price" aria-label="product price">${ productPrice } / kilo</p>` : '' }
</div>
</div>
terminal
npm i -g @angular/cli
terminal
npm i -g @angular/cli
ng new bb-card --prefix bb --inline-template --style=scss
terminal
npm i -g @angular/cli
ng new bb-card --prefix bb --inline-template --style=scss
cd bb-card
terminal
npm i -g @angular/cli
ng new bb-card --prefix bb --inline-template --style=scss
cd bb-card
ng add @angular/elements
terminal
npm i -g @angular/cli
ng new bb-card --prefix bb --inline-template --style=scss
cd bb-card
ng add @angular/elements
npm install @webcomponents/custom-elements --save
polyfills.ts
import '@webcomponents/custom-elements/custom-elements.min';
tsconfig.json
{
"compileOnSave": false,
"compilerOptions": {
…
"target": "es2015",
….
}
}
}
terminal
ng generate component card
card.component.ts
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'bb-card',
template: ` ...`,
styleUrls: ['./bb-card.scss'],
})
card.component.ts
import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core';
@Component({
selector: 'bb-card',
template: ` ...`,
styleUrls: ['./bb-card.scss'],
encapsulation: ViewEncapsulation.ShadowDom,
})
ViewEncapsulation.ShadowDom
This encapsulation mode uses the Shadow DOM to scope styles only to this specific component.
Make our code Angular friendly
card.component.ts
template: `
<img id='shadow-image' class='shadow__image' src='{{ src }}' alt='title'>
<div id='shadow-info' class='shadow__info'>
<h1 name='title' id='title' role='header' class='shadow__title'>{{ title }}</h1>
<p name='description' id='description' class='shadow__description'>{{ description }}</p>
</div>
`
card.component.ts
export class CardComponent {
@Input() title?: string = 'default title';
@Input() description?: string = 'default description';
@Input() src?: string = 'https://goo.gl/STZhS6';
constructor() { }
ngOnInit() {}
}
card.component.scss
:host {
display: block;
width: 320px;
height: 300px;
background-color: var(--background-color, #fff);
[...]
.shadow__image {
[...]
}
.shadow__info {
[...]
}
}
Register our component in NgModule
app.module.ts
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
@NgModule({
declarations: [ CardComponent ],
imports: [ BrowserModule ],
entryComponents: [ CardComponent ],
})
app.module.ts
@NgModule({
declarations: [CardComponent],
imports: [BrowserModule],
entryComponents: [CardComponent],
})
export class AppModule {
constructor(private injector: Injector) {
const bbCard = createCustomElement(CardComponent, { injector });
customElements.define('bb-card', bbCard);
}
app.module.ts
@NgModule({
declarations: [CardComponent],
imports: [BrowserModule],
entryComponents: [CardComponent],
})
export class AppModule {
constructor(private injector: Injector) {
const bbCard = createCustomElement(CardComponent, { injector });
customElements.define('bb-card', bbCard);
}
ngDoBootstrap() {}
}
Wait a min
build
@ManfredSteyer
Ivy
Now our component is ready!
How do we use it?
(Existing project)
terminal
npm install @webcomponents/custom-elements --save
polyfills.ts
import '@webcomponents/custom-elements/custom-elements.min';
card.component.ts
import { Component } from '@angular/core';
import * as bbCard from '../web-components/bb-card/bb-card.js';
@Component({
selector: 'berry-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'berry-history';
constructor(){}
}
app.module.ts
@NgModule({
[…]
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
export class AppModule {
}
Web Components rock?
STABLE | STABLE | STABLE | STABLE | STABLE |
STABLE | STABLE | STABLE | STABLE | POLYFILL |
STABLE | STABLE | STABLE | STABLE | POLYFILL |
STABLE | STABLE | STABLE | STABLE | STABLE |
Angular ♥ Web Components
Sherry List
@SherrryLst
Ana Cidre
@AnaCidre_
http://bit.ly/web-components-jdays