1 of 105

Web Components with Angular:

The What, the Why and the How.

@SherrryLst | @AnaCidre_

2 of 105

Hello!

I’m Sherry List

AKA Sherry Berry 🍓

Senior frontend developer, Nordea

Women Techmaker Lead

You can find me at @SherrryLst

3 of 105

Hi!

I’m Ana Cidre

AKA Ana Banana 🍌

Developer Advocate, Ultimate Angular

Women Techmaker Lead

You can find me at @AnaCidre_

4 of 105

Angular Components

5 of 105

Angular Components

Web

Components

6 of 105

Angular Components

Web

Components

Creating Web Components

With Angular

7 of 105

Angular Components

Web

Components

Creating Web Components

With Angular

Adding Web Components to Angular

8 of 105

Angular Components

9 of 105

10 of 105

11 of 105

What’s missing:

  • Actual style encapsulation (nothing leaks in or out without being explicitly allowed)

12 of 105

What’s missing:

  • Actual style encapsulation (nothing leaks in or out without being explicitly allowed)
  • Ways of allowing some styling of these elements

13 of 105

What’s missing:

  • Actual style encapsulation (nothing leaks in or out without being explicitly allowed)
  • Ways of allowing some styling of these elements
  • Be able to use elements across teams using different frameworks (or none)

14 of 105

What’s missing:

  • Actual style encapsulation (nothing leaks in or out without being explicitly allowed)
  • Ways of allowing some styling of these elements
  • Be able to use elements across teams using different frameworks (or none)
  • Create elements declaratively, but still use JS when needed (to filter lists etc) or hook up bindings - maybe something like JSX - we want flexibility, and not another templating language

15 of 105

What does have all these things?

What does?

16 of 105

Web Components

provides a lot of this

17 of 105

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.

18 of 105

Web components consist of three main technologies:

  • HTML template
  • Custom Elements
  • Shadow DOM
  • HTML imports

19 of 105

  • HTML template

<template> </template>

Web components consist of three main technologies:

1

20 of 105

21 of 105

index.html

<template id="red-strawberry-template">

<div class="template__container">

<img class="template__image">

<div class="template__info">

<span></span>

</div>

</div>

</template>

22 of 105

index.html

<div id="image-gallery" class="template__grid">

<!-- existing image containers will be included here -->

</div>

23 of 105

index.js

const template = document.querySelector('#red-strawberry-template');

24 of 105

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";

25 of 105

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";

26 of 105

index.js

// Clone the new gallery and insert the DOM

const imageGallery = document.querySelector("#image-gallery");

27 of 105

index.js

// Clone the new gallery and insert the DOM

const imageGallery = document.querySelector("#image-gallery");

const clone = document.importNode(template.content, true);

28 of 105

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);

29 of 105

30 of 105

  • HTML Template
  • Custom Elements

Web components consist of three main technologies:

2

aka custom elephants 🐘

31 of 105

Naming your custom element

<red-button></red-button>

<my-unique-wc></my-unique-wc>

🐘

32 of 105

Naming the attributes

<my-unique-wc

title="I am a Title"

xyz="No idea what I am!">

</my-unique-wc>

🐘

33 of 105

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

34 of 105

index.html

<bb-red-strawberry

img="strawberry.jpg"

description="Strawberries from Sherry's Garden">

</bb-red-strawberry>

35 of 105

index.js

class BBRedStrawberryElement extends HTMLElement {

constructor() {

super();

}

}

36 of 105

index.js

class BBRedStrawberryElement extends HTMLElement {

constructor() {

super();

}

}

// Define custom element

customElements.define("bb-red-strawberry", BBRedStrawberryElement);

37 of 105

index.js

connectedCallback() {

this.innerHTML = template;

this._$image = this.querySelector("#element-image");

this._$description = this.querySelector("#element-description");

this._render(this);

}

38 of 105

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);

}

39 of 105

index.js

static get observedAttributes() {

return ["description", "img"];

}

attributeChangedCallback(name, oldValue, newValue) {

this[name] = newValue;

}

...

40 of 105

41 of 105

42 of 105

index.html

...

<style>

...

p {

font-family: 'Lato', sans-serif;

font-size: 0.9em;

max-width: 760px;

line-height: 1.6em;

}

...

43 of 105

index.html

...

<style>

...

p {

font-family: 'Lato', sans-serif;

font-size: 0.9em;

max-width: 760px;

line-height: 1.6em;

color: red;

}

...

44 of 105

45 of 105

  • HTML Template
  • Custom Elements
  • Shadow DOM

Web components consist of three main technologies:

3

46 of 105

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));

}

47 of 105

Slots

48 of 105

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>

...

49 of 105

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>

50 of 105

CSS Custom Properties

51 of 105

index.js

connectedCallback() {

this.shadowRoot.innerHTML = `

<style>

.container {

width: 320px;

height: 280px;

background-color: var(--background-color, #fff);

}

52 of 105

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>

53 of 105

:host

54 of 105

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;

}

55 of 105

56 of 105

57 of 105

index.html

...

<style>

...

p {

font-family: 'Lato', sans-serif;

font-size: 0.9em;

max-width: 760px;

line-height: 1.6em;

color: red;

}

...

58 of 105

59 of 105

lit-html

60 of 105

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));

}

}

61 of 105

index.js

connectedCallback() {

render(this._render(this), this.shadowRoot);

}

62 of 105

index.js

connectedCallback() {

render(this._render(this), this.shadowRoot);

}

// Defines element markup

_render({ img, title, description, productPrice, color }) {

return html

63 of 105

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>

64 of 105

65 of 105

66 of 105

67 of 105

terminal

npm i -g @angular/cli

68 of 105

terminal

npm i -g @angular/cli

ng new bb-card --prefix bb --inline-template --style=scss

69 of 105

terminal

npm i -g @angular/cli

ng new bb-card --prefix bb --inline-template --style=scss

cd bb-card

70 of 105

terminal

npm i -g @angular/cli

ng new bb-card --prefix bb --inline-template --style=scss

cd bb-card

ng add @angular/elements

71 of 105

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

72 of 105

polyfills.ts

import '@webcomponents/custom-elements/custom-elements.min';

73 of 105

tsconfig.json

{

"compileOnSave": false,

"compilerOptions": {

"target": "es2015",

….

}

}

}

74 of 105

terminal

ng generate component card

75 of 105

card.component.ts

import { Component, OnInit, Input } from '@angular/core';

@Component({

selector: 'bb-card',

template: ` ...`,

styleUrls: ['./bb-card.scss'],

})

76 of 105

card.component.ts

import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core';

@Component({

selector: 'bb-card',

template: ` ...`,

styleUrls: ['./bb-card.scss'],

encapsulation: ViewEncapsulation.ShadowDom,

})

77 of 105

ViewEncapsulation.ShadowDom

This encapsulation mode uses the Shadow DOM to scope styles only to this specific component.

78 of 105

Make our code Angular friendly

79 of 105

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>

`

80 of 105

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() {}

}

81 of 105

card.component.scss

:host {

display: block;

width: 320px;

height: 300px;

background-color: var(--background-color, #fff);

[...]

.shadow__image {

[...]

}

.shadow__info {

[...]

}

}

82 of 105

Register our component in NgModule

83 of 105

app.module.ts

import { NgModule, Injector } from '@angular/core';

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

@NgModule({

declarations: [ CardComponent ],

imports: [ BrowserModule ],

entryComponents: [ CardComponent ],

})

84 of 105

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);

}

85 of 105

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() {}

}

86 of 105

87 of 105

Wait a min

88 of 105

build

89 of 105

@ManfredSteyer

90 of 105

Ivy

91 of 105

Now our component is ready!

92 of 105

93 of 105

How do we use it?

(Existing project)

94 of 105

terminal

npm install @webcomponents/custom-elements --save

95 of 105

polyfills.ts

import '@webcomponents/custom-elements/custom-elements.min';

96 of 105

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(){}

}

97 of 105

98 of 105

app.module.ts

@NgModule({

[…]

schemas: [

CUSTOM_ELEMENTS_SCHEMA

]

})

export class AppModule {

}

99 of 105

100 of 105

101 of 105

Web Components rock?

102 of 105

STABLE

STABLE

STABLE

STABLE

STABLE

STABLE

STABLE

STABLE

STABLE

POLYFILL

STABLE

STABLE

STABLE

STABLE

POLYFILL

STABLE

STABLE

STABLE

STABLE

STABLE

103 of 105

Angular Web Components

104 of 105

Sherry List

@SherrryLst

Ana Cidre

@AnaCidre_

http://bit.ly/web-components-jdays

105 of 105

Sources