Reactive
Type-safe
WebComponents
Martin Hochel
@martin_hotell
Hello Munich !
▪ @ngPartyCz meetup founder
▪ Author of ngMetadata
▪ Member of @skate_js, @ngParty
▪
▪
Martin Hochel
SE, Prague / Czech Republic
@martin_hotell
github.com/Hotell
Today’s talk
Will be all about
Skateboarding
This is how you do an Ollie
Problem set 1:
Create a reusable widget
Problem 1
Create
reusable widget
Library size
10.7 kB
4.9 kB
3.9 kB
Implementation size
4.2 kB
4.12 kB
2.2kB
MINIFIED + GZIPPED
32.7kB
MINIFIED + GZIPPED
Problem 1
Create
reusable widget
FUTURE JS
Problem set I1:
Create Interoperable Widget
Problem I1
Create
Interoperable widget ALPHA
BETA
GAMMA DELTA
Widget
WHY WHAT HOW
WebComponents
WHY
WebComponents
WHY
WebComponents
Reusable
Interoperable
Problem
I + I1
Solved
So can I write whole apps with
vanilla WebComponents ?
You can! but It will/may hurt
WHAT
is a WebComponent
“
Platform new primitive for building
Reusable
Interoperable
Encapsulated
Widgets
<sk-user name="Martin" age="100">
<img src="./assets/skate-deck.jpg">
</sk-user>
Custom
Elements
Shadow
DOM
JS
Modules
WebComponents
Templates
Custom
Elements
class User extends HTMLElement {}
// user.component.js
<sk-user></sk-user>
// index.html
// Global registry
window.customElements.define('sk-user', User)
window.customElements.get('sk-user') // User
Custom
Elements
API
HTML Attributes / DOM Properties
Events
Life Cycle hooks
HTML attribute vs. DOM property
<input type="text" value="Bob">
DO
// $0 === User instance
$0.setAttribute('age','18')
$0.age = 18
// Imperative (JS)
<sk-user age="100"></sk-user>
// Declarative (HTML)
Custom Elements
API
Attributes
&
Properties
Primitive
Data
export class User extends HTMLElement {
static get observedAttributes() {
return ['age']
}
_age = 0
set age(value) {
this._age = Number(value)
this.render()
}
get age() {
return this._age
}
attributeChangedCallback(name, oldValue, newValue) {
this.age = newValue
}
render() {}
}
// user.component.js
Custom Elements
API
Attributes
&
Properties
Primitive
Data
export class User extends HTMLElement {
static get observedAttributes() {
return ['age']
}
_age = 0
set age(value) {
this._age = Number(value)
this.render()
}
get age() {
return this._age
}
attributeChangedCallback(name, oldValue, newValue) {
this.age = newValue
}
render() {}
}
// user.component.js
$0.age = 18
$0.age
Custom Elements
API
Attributes
&
Properties
Primitive
Data
export class User extends HTMLElement {
static get observedAttributes() {
return ['age']
}
_age = 0
set age(value) {
this._age = Number(value)
this.render()
}
get age() {
return this._age
}
attributeChangedCallback(name, oldValue, newValue) {
this.age = newValue
}
render() {}
}
// user.component.js
$0.setAttribute('age','18')
Custom Elements
API
Attributes
&
Properties
Primitive
Data
// $0 === User instance
$0.tricks = [
{ name: 'ollie', difficulty: 'easy' },
{ name: 'kickflip', difficulty: 'medium' },
{ name: 'hardflip', difficulty: 'hard' },
]
// Imperative (JS)
<sk-user hobbies="[{
"name": "ollie",
"difficulty ": "easy"
}]"></sk-user>
// Declarative (HTML)
Custom Elements
API
Attributes
&
Properties
Rich
Data
export class User extends HTMLElement {
_tricks = []
set tricks(value) {
this._tricks = value
this.render()
}
get tricks() {
return this._tricks
}
render() {}
}
// user.component.js
Custom Elements
API
Attributes
&
Properties
Rich
Data
// $0 === User instance
$0.addEventListener('learntrick',(event)=>{ /* ... */ })
// Imperative (JS)
Nope
// Declarative (HTML)
Custom Elements
API
Events
export class User extends HTMLElement {
emitLearnTrick(trick) {
const eventConfig = {
bubble: true,
composed: false,
detail: trick
}
const event = new CustomEvent('learntrick', eventConfig)
this.dispatchEvent(event)
}
}
// user.component.js
Custom Elements
API
Events
export class User extends HTMLElement {
constructor() {
super()
}
attributeChangedCallback(name, oldValue, newValue) {
// do some stuff
}
connectedCallback() {
console.log('component mounted!')
}
disconnectedCallback() {
console.log('goodbye!')
}
render() {}
}
// user.component.js
Custom Elements
API
Life Cycle
Hooks
export class User extends HTMLElement {
constructor() {
super()
}
attributeChangedCallback(name, oldValue, newValue) {
// do some stuff
}
connectedCallback() {
console.log('component mounted!')
}
disconnectedCallback() {
console.log('goodbye!')
}
render() {}
}
// user.component.js
Custom Elements
API
Life Cycle
Hooks
export class User extends HTMLElement {
constructor() {
super()
}
attributeChangedCallback(name, oldValue, newValue) {
// do some stuff
}
connectedCallback() {
console.log('component mounted!')
}
disconnectedCallback() {
console.log('goodbye!')
}
render() {}
}
// user.component.js
Custom Elements
API
Life Cycle
Hooks
export class User extends HTMLElement {
constructor() {
super()
}
attributeChangedCallback(name, oldValue, newValue) {
// do some stuff
}
connectedCallback() {
console.log('component mounted!')
}
disconnectedCallback() {
console.log('goodbye!')
}
render() {}
}
// user.component.js
Custom Elements
API
Life Cycle
Hooks
export class User extends HTMLElement {
constructor() {
super()
}
attributeChangedCallback(name, oldValue, newValue) {
// do some stuff
}
connectedCallback() {
console.log('component mounted!')
}
disconnectedCallback() {
console.log('goodbye!')
}
render() {}
}
// user.component.js
Custom Elements
API
Life Cycle
Hooks
<template>
<template id="view">
<style>
:host { display: flex }
div { color: blue; }
</style>
<h2>Hello World!</h2>
<p>Yo what’s up dough?</p>
<slot></slot>
</template>
// user.template.html
<template>
export class User extends HTMLElement {
constructor() {
super()
const template = document.querySelector('#view')
const view = template.content.cloneNode(true)
this.appendChild(view)
}
}
// user.component.html
<template>
// template: HTMLTemplateElement
const view = template.content.cloneNode(true)
this.appendChild(view)
// user.template.html
// template: HTMLDivElement
const view = template.innerHTML = `Hello`
this.appendChild(view)
vs
Shadow
DOM
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
Shadow
DOM
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
<sk-user name="Martin" age="30">
#shadow-root(open)
<style></style>
<div>
<ul>
<li>Age</li>
</ul>
<slot>#refToImg</slot>
</div>
<img src="./assets/skate-deck.jpg">
</sk-user>
Shadow
DOM
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
<sk-user name="Martin" age="30">
#shadow-root(open)
<style></style>
<div>
<ul>
<li>Age</li>
</ul>
<slot>#refToImg</slot>
</div>
<img src="./assets/skate-deck.jpg">
</sk-user>
Shadow
DOM
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
<sk-user name="Martin" age="30">
#shadow-root(open)
<style></style>
<div>
<ul>
<li>Age</li>
</ul>
<slot>#refToImg</slot>
</div>
<img src="./assets/skate-deck.jpg">
</sk-user>
Shadow
DOM
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
<sk-user name="Martin" age="30">
#shadow-root(open)
<style></style>
<div>
<ul>
<li>Age</li>
</ul>
<slot>#refToImg</slot>
</div>
<img src="./assets/skate-deck.jpg">
</sk-user>
LIGHT
Shadow
DOM
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
<sk-user name="Martin" age="30">
#shadow-root(open)
<style></style>
<div>
<ul>
<li>Age</li>
</ul>
<slot>#refToImg</slot>
</div>
<img src="./assets/skate-deck.jpg">
</sk-user>
Shadow
DOM
<style></style>
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
<sk-user name="Martin" age="30">
#shadow-root(open)
<style></style>
<div>
<ul>
<li>Age</li>
</ul>
<slot>#refToImg</slot>
</div>
<img src="./assets/skate-deck.jpg">
</sk-user>
Shadow
DOM
<style></style>
<sk-user name="Martin" age="30">
<img src="./assets/skate-deck.jpg">
</sk-user>
<sk-user name="Martin" age="30">
#shadow-root(open)
<style></style>
<div>
<ul>
<li>Age</li>
</ul>
<slot>#refToImg</slot>
</div>
<img src="./assets/skate-deck.jpg">
</sk-user>
Shadow
DOM
constructor() {
super()
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.appendChild(template.content.cloneNode(true))
}
Shadow
DOM
Isolated sandbox
API -> projection -> <slot/>
scoped CSS
<script type="module">
import './src/app.js'
</script>
<my-app></my-app>
JS
Modules
WHAT ✅
is a WebComponent
Hey!
What about browser support?
Browser
support
HOW
frameworks/libraries Interop with
WebComponents
index.html
<sk-user
name="Martin"
age="100"
tricks=""[{"foo":"bar"}]""
></sk-user>
app.component.html
<sk-user
[attr.name]="model.name"
[attr.age]="model.age"
[tricks]="model.tricks"
></sk-user>
App.vue
<sk-user
:name="model.name"
:age="model.age"
:tricks.prop="model.tricks"
></sk-user>
App.tsx
<sk-user
name={this.state.user.name}
age={this.state.user.age}
tricks={this.state.user.tricks}
/>
https://custom-elements-everywhere.com
HOW
to build a WebComponent
<sk-app>
WC App
Demo
WC App
architecture
WC App
Reactive
data-flow
sk-app
sk-user
name: string
age: number
tricks: Array<Trick>
learntrick: CustomEvent<Trick>
removetrick: CustomEvent<Trick>
export type Trick = {
name: string
difficulty: 'easy' | 'medium' | 'hard'
}
sk-user
Implementation
Types, template
type Props = {
name: string
age: number
tricks: Array<Trick>
}
const template = document.createElement('template')
template.innerHTML = `
<style> :host { … } </style>
<header>
Hello <b id="name"></b>! Let's skate!
</header>
<div>
Only <b id="age"></b> years old? Time to learn new tricks!
</div>
<form>
<input name="trickName" value="">
<select id="trickDifficulty" class="form-control">
</form>
`
// user.component.ts
export class User extends HTMLElement implements Props {
set name(value: string) {}
get name() {}
set age(value: number) {}
get age() {}
set tricks(value: Array<Trick>) {}
get tricks() {
private _tricks: Array<Trick> = []
private view: {
form: HTMLFormElement
trickList: HTMLUListElement
age: HTMLElement
name: HTMLElement
}
}
sk-user
Implementation
Define Properties
window.customElements.define('sk-user', User)
// user.component.ts
export class User extends HTMLElement implements Props {
constructor() {
super()
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.appendChild(template.content.cloneNode(true))
this.view = {
form: shadowRoot.querySelector('form'),
trickList: shadowRoot.querySelector('#trick-list'),
age: shadowRoot.querySelector('#age'),
name: shadowRoot.querySelector('#name'),
}
this.view.form.addEventListener(
'submit',
this.handleNewTrickAddition
)
}
}
sk-user
Implementation
construction
// user.component.ts
export class User extends HTMLElement implements Props {
attributeChangedCallback(
name: Attrs, oldValue: string | null, newValue: string | null) {
this.render()
}
connectedCallback() {
this.render()
}
}
sk-user
Implementation
Reactions,
Rendering
// user.component.ts
“
So what’s the problem with
vanilla WebComponents ?
“
UNFORTUNATELY
THE DEVELOPER EXPERIENCE
OF BUILDING AN APPLICATION
WITH WEB COMPONENTS TODAY IS
QUITE PAINFUL
SAM SACCONE
HOW
to
WebComponents
?
“
WC + abstraction
=
Great DX
WC with
great DX
● Performant declarative renderer
● Declarative API
○ async reactive data flow
pipeline
○ state handling
○ extensibility
From Imperative
to Declarative + Performant
rendering
View Renderers
ES6 te l it sV O
Preact
Fast 3kB alternative to React
V O
class App extends HTMLElement {
render() {
const { tricks, logs } = this
const Root = () =>
<div>
<ul class="log">
{logs.map(item => <TrickLog {...item} />)}
</ul>
<sk-user
name="Martin"
age={30}
tricks={tricks}
onLearnTrick={this.handleLearnTrick}
onRemoveTrick={this.handleRemoveTrick}
/>
</div>
this.preactDOM = render(<Root/>, this.shadowRoot, this.preactDOM)
}
Preact
connectedCallback() {
console.log('App mounted')
this.render()
}
lit-html
HTML templates, via JS tagged template literals
ES6 te l it s
JS tagged template literals ?
https://appendto.com/2017/02/advanced-javascript-es2015-template-strings-with-tagged-templates/
Tag ( function html(strings, ...rest){} )
+
ES6 string literal (`hello`)
const sayHello = (name: string) =>
html`<div>Hello ${name}!</div>`
lit-html
const container = document.querySelector('#container')
render(sayHello('Steve'), container)
// renders -> <div>Hello Steve!</div> to container
class App extends HTMLElement {
render() {
const { tricks, logs } = this
const template = html`
<ul class="log">
${logs.map(item => CreateLogEvent(item))}
</ul>
<sk-user
name="Martin"
age="30"
tricks="${tricks}"
on-learnTrick="${this.handleNewTrick}"
on-removeTrick="${this.handleRemoveTrick}"
><sk-user>
`
render(template, this.shadowRoot)
}
}
lit-html
connectedCallback() {
console.log('App mounted')
this.render()
}
Declarative + Performant
rendering
✅
API
- reactive
- extensible
Reactive ?
props
View
Logic
render
Action
( click, keypress )
state
class mixins composition
http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
Extensible ?
const withShadow = <T extends Constructor>(Base: T) =>
class extends Base {
constructor(...args: Array<any>) {
super(...args)
this.attachShadow({ mode: 'open' })
}
}
mixins
class App extends withShadow(HTMLElement) { }
https://blog.mariusschulz.com/2017/05/26/typescript-2-2-mixin-classes
Mmm so let’s say something like...
export class User extends Component<Props> {
static readonly is = 'sk-user'
static readonly properties = {
age: {
type: Number,
reflectToAttribute: true,
value: 0,
},
tricks: {
type: Array,
value: [],
},
}
render() {
const { name, age, tricks } = this.props
return html`
<header>Hello <b id="name">${name}</b>! Let's skate!</header>
<div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div>
`
}
export class User extends Component<Props> {
static readonly is = 'sk-user'
static readonly properties = {
age: {
type: Number,
reflectToAttribute: true,
value: 0,
},
tricks: {
type: Array,
value: [],
},
}
render() {
const { name, age, tricks } = this.props
return html`
<header>Hello <b id="name">${name}</b>! Let's skate!</header>
<div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div>
`
}
export class User extends Component<Props> {
static readonly is = 'sk-user'
static readonly properties = {
age: {
type: Number,
reflectToAttribute: true,
value: 0,
},
tricks: {
type: Array,
value: [],
},
}
render() {
const { name, age, tricks } = this.props
return html`
<header>Hello <b id="name">${name}</b>! Let's skate!</header>
<div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div>
`
}
export class User extends Component<Props> {
static readonly is = 'sk-user'
static readonly properties = {
age: {
type: Number,
reflectToAttribute: true,
value: 0,
},
tricks: {
type: Array,
value: [],
},
}
render() {
const { name, age, tricks } = this.props
return html`
<header>Hello <b id="name">${name}</b>! Let's skate!</header>
<div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div>
`
}
export class User extends Component<Props> {
static readonly is = 'sk-user'
static readonly properties = {
age: {
type: Number,
reflectToAttribute: true,
value: 0,
},
tricks: {
type: Array,
value: [],
},
}
render() {
const { name, age, tricks } = this.props
return html`
<header>Hello <b id="name">${name}</b>! Let's skate!</header>
<div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div>
`
}
const Component = withComponent()
Component
const withComponent =
<P = {}>(Base = HTMLElement) =>
withProps<P>(
withRender(
withShadow(Base)
)
)
withComponent
export const withShadow =
<T>(Base: T) => class extends Base {
constructor(...args: Array<any>) {
super(...args)
this.attachShadow({ mode: 'open' })
}
}
withShadow
import { TemplateResult } from 'lit-html'
import { render } from 'lit-html/lib/lit-extended'
const withRender =
<T extends Constructor<CustomElement>>(Base: T) => {
abstract class WithRender extends Base {
private get renderRoot() {
return this.shadowRoot ? this.shadowRoot : this
}
abstract render(): TemplateResult
scheduleRender() {
this.renderer()
}
private renderer() {
render(
this.render && this.render(),
this.renderRoot
)
}
}
return WithRender
withRender
const withProps =
<P = {},T extends Constructor<CustomElement>>(Base: T) =>
class extends Base {
private static _properties: PropDefinitionMap
static get properties() {}
static set properties(props: PropDefinitionMap) {
this.setupProps(props)
}
scheduleRender?: () => void
attributeChangedCallback(name, newValue) {
this[name] = _deserializeValue(newValue)
}
connectedCallback() {
this.scheduleRender && this.scheduleRender()
}
get props(): P {...}
private static setupProps(properties) {}
withProps
private static setupProps(properties: PropDefinitionMap) {
const ctor = this
const propNames = Object.keys(properties)
propNames.forEach(name => {
const definition = properties[name]
const _value = Symbol(name)
const defaultValue = _deserializeValue(definition.value)
if (_shouldSetObservedAttributes(definition.type)) {
ctor.observedAttributes = [name]
}
Object.defineProperty(ctor.prototype, name, {
configurable: true,
set(this: any, val: any) {
this[_value] = _deserializeValue(val, definition.type)
this.scheduleRender && this.scheduleRender()
},
get(this: any) {
const val = this[_value]
return val == null ? defaultValue : val
},
})
})
}
withProps
Vanilla
vs
Component
Base Class
Vanilla With Component base class
Good news !
Skate JSSkate JS
What is
SkateJS
Reactive
Pluggable
WebComponents Micro-library
Current version: 5.0
Pluggable ?
- mixins
- utils
- custom renderers
- fine grained LC hooks
skateJS
Renderers
- renderer-preact
- renderer-react
- renderer-lit-html
- renderer-vue ( TODO )
- custom
Micro library ?
*supports the module field. You can get smaller sizes with tree shaking.
HOW
to build a WebComponent
with
skateJS
<sk-app>
github.com/Hotell/reactive-typesafe-webcomponents
What did
we learn ?
Component API
Are WebCompoents
The Future of web ?
The Future is now
KEEP CALM
and
USE
WEBCOMPONENTS
Thank you ! Martin Hochel
@martin_hotell

Reactive Type-safe WebComponents

  • 1.
  • 2.
    Hello Munich ! ▪@ngPartyCz meetup founder ▪ Author of ngMetadata ▪ Member of @skate_js, @ngParty ▪ ▪ Martin Hochel SE, Prague / Czech Republic @martin_hotell github.com/Hotell
  • 3.
    Today’s talk Will beall about Skateboarding
  • 4.
    This is howyou do an Ollie
  • 6.
    Problem set 1: Createa reusable widget
  • 7.
    Problem 1 Create reusable widget Librarysize 10.7 kB 4.9 kB 3.9 kB Implementation size 4.2 kB 4.12 kB 2.2kB MINIFIED + GZIPPED 32.7kB MINIFIED + GZIPPED
  • 8.
  • 9.
    Problem set I1: CreateInteroperable Widget
  • 10.
    Problem I1 Create Interoperable widgetALPHA BETA GAMMA DELTA Widget
  • 13.
  • 14.
  • 15.
  • 16.
    So can Iwrite whole apps with vanilla WebComponents ?
  • 17.
    You can! butIt will/may hurt
  • 18.
  • 19.
    “ Platform new primitivefor building Reusable Interoperable Encapsulated Widgets
  • 20.
    <sk-user name="Martin" age="100"> <imgsrc="./assets/skate-deck.jpg"> </sk-user>
  • 21.
  • 22.
    Custom Elements class User extendsHTMLElement {} // user.component.js <sk-user></sk-user> // index.html // Global registry window.customElements.define('sk-user', User) window.customElements.get('sk-user') // User
  • 23.
    Custom Elements API HTML Attributes /DOM Properties Events Life Cycle hooks
  • 24.
    HTML attribute vs.DOM property <input type="text" value="Bob"> DO
  • 25.
    // $0 ===User instance $0.setAttribute('age','18') $0.age = 18 // Imperative (JS) <sk-user age="100"></sk-user> // Declarative (HTML) Custom Elements API Attributes & Properties Primitive Data
  • 26.
    export class Userextends HTMLElement { static get observedAttributes() { return ['age'] } _age = 0 set age(value) { this._age = Number(value) this.render() } get age() { return this._age } attributeChangedCallback(name, oldValue, newValue) { this.age = newValue } render() {} } // user.component.js Custom Elements API Attributes & Properties Primitive Data
  • 27.
    export class Userextends HTMLElement { static get observedAttributes() { return ['age'] } _age = 0 set age(value) { this._age = Number(value) this.render() } get age() { return this._age } attributeChangedCallback(name, oldValue, newValue) { this.age = newValue } render() {} } // user.component.js $0.age = 18 $0.age Custom Elements API Attributes & Properties Primitive Data
  • 28.
    export class Userextends HTMLElement { static get observedAttributes() { return ['age'] } _age = 0 set age(value) { this._age = Number(value) this.render() } get age() { return this._age } attributeChangedCallback(name, oldValue, newValue) { this.age = newValue } render() {} } // user.component.js $0.setAttribute('age','18') Custom Elements API Attributes & Properties Primitive Data
  • 29.
    // $0 ===User instance $0.tricks = [ { name: 'ollie', difficulty: 'easy' }, { name: 'kickflip', difficulty: 'medium' }, { name: 'hardflip', difficulty: 'hard' }, ] // Imperative (JS) <sk-user hobbies="[{ "name": "ollie", "difficulty ": "easy" }]"></sk-user> // Declarative (HTML) Custom Elements API Attributes & Properties Rich Data
  • 30.
    export class Userextends HTMLElement { _tricks = [] set tricks(value) { this._tricks = value this.render() } get tricks() { return this._tricks } render() {} } // user.component.js Custom Elements API Attributes & Properties Rich Data
  • 31.
    // $0 ===User instance $0.addEventListener('learntrick',(event)=>{ /* ... */ }) // Imperative (JS) Nope // Declarative (HTML) Custom Elements API Events
  • 32.
    export class Userextends HTMLElement { emitLearnTrick(trick) { const eventConfig = { bubble: true, composed: false, detail: trick } const event = new CustomEvent('learntrick', eventConfig) this.dispatchEvent(event) } } // user.component.js Custom Elements API Events
  • 33.
    export class Userextends HTMLElement { constructor() { super() } attributeChangedCallback(name, oldValue, newValue) { // do some stuff } connectedCallback() { console.log('component mounted!') } disconnectedCallback() { console.log('goodbye!') } render() {} } // user.component.js Custom Elements API Life Cycle Hooks
  • 34.
    export class Userextends HTMLElement { constructor() { super() } attributeChangedCallback(name, oldValue, newValue) { // do some stuff } connectedCallback() { console.log('component mounted!') } disconnectedCallback() { console.log('goodbye!') } render() {} } // user.component.js Custom Elements API Life Cycle Hooks
  • 35.
    export class Userextends HTMLElement { constructor() { super() } attributeChangedCallback(name, oldValue, newValue) { // do some stuff } connectedCallback() { console.log('component mounted!') } disconnectedCallback() { console.log('goodbye!') } render() {} } // user.component.js Custom Elements API Life Cycle Hooks
  • 36.
    export class Userextends HTMLElement { constructor() { super() } attributeChangedCallback(name, oldValue, newValue) { // do some stuff } connectedCallback() { console.log('component mounted!') } disconnectedCallback() { console.log('goodbye!') } render() {} } // user.component.js Custom Elements API Life Cycle Hooks
  • 37.
    export class Userextends HTMLElement { constructor() { super() } attributeChangedCallback(name, oldValue, newValue) { // do some stuff } connectedCallback() { console.log('component mounted!') } disconnectedCallback() { console.log('goodbye!') } render() {} } // user.component.js Custom Elements API Life Cycle Hooks
  • 38.
    <template> <template id="view"> <style> :host {display: flex } div { color: blue; } </style> <h2>Hello World!</h2> <p>Yo what’s up dough?</p> <slot></slot> </template> // user.template.html
  • 39.
    <template> export class Userextends HTMLElement { constructor() { super() const template = document.querySelector('#view') const view = template.content.cloneNode(true) this.appendChild(view) } } // user.component.html
  • 40.
    <template> // template: HTMLTemplateElement constview = template.content.cloneNode(true) this.appendChild(view) // user.template.html // template: HTMLDivElement const view = template.innerHTML = `Hello` this.appendChild(view) vs
  • 41.
    Shadow DOM <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user>
  • 42.
    Shadow DOM <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user> <sk-user name="Martin" age="30"> #shadow-root(open) <style></style> <div> <ul> <li>Age</li> </ul> <slot>#refToImg</slot> </div> <img src="./assets/skate-deck.jpg"> </sk-user>
  • 43.
    Shadow DOM <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user> <sk-user name="Martin" age="30"> #shadow-root(open) <style></style> <div> <ul> <li>Age</li> </ul> <slot>#refToImg</slot> </div> <img src="./assets/skate-deck.jpg"> </sk-user>
  • 44.
    Shadow DOM <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user> <sk-user name="Martin" age="30"> #shadow-root(open) <style></style> <div> <ul> <li>Age</li> </ul> <slot>#refToImg</slot> </div> <img src="./assets/skate-deck.jpg"> </sk-user>
  • 45.
    Shadow DOM <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user> <sk-user name="Martin" age="30"> #shadow-root(open) <style></style> <div> <ul> <li>Age</li> </ul> <slot>#refToImg</slot> </div> <img src="./assets/skate-deck.jpg"> </sk-user> LIGHT
  • 46.
    Shadow DOM <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user> <sk-user name="Martin" age="30"> #shadow-root(open) <style></style> <div> <ul> <li>Age</li> </ul> <slot>#refToImg</slot> </div> <img src="./assets/skate-deck.jpg"> </sk-user>
  • 47.
    Shadow DOM <style></style> <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user> <sk-user name="Martin" age="30"> #shadow-root(open) <style></style> <div> <ul> <li>Age</li> </ul> <slot>#refToImg</slot> </div> <img src="./assets/skate-deck.jpg"> </sk-user>
  • 48.
    Shadow DOM <style></style> <sk-user name="Martin" age="30"> <imgsrc="./assets/skate-deck.jpg"> </sk-user> <sk-user name="Martin" age="30"> #shadow-root(open) <style></style> <div> <ul> <li>Age</li> </ul> <slot>#refToImg</slot> </div> <img src="./assets/skate-deck.jpg"> </sk-user>
  • 49.
    Shadow DOM constructor() { super() const shadowRoot= this.attachShadow({ mode: 'open' }) shadowRoot.appendChild(template.content.cloneNode(true)) }
  • 50.
    Shadow DOM Isolated sandbox API ->projection -> <slot/> scoped CSS
  • 51.
  • 52.
    WHAT ✅ is aWebComponent
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
    HOW to build aWebComponent
  • 59.
  • 60.
  • 61.
  • 62.
    WC App Reactive data-flow sk-app sk-user name: string age:number tricks: Array<Trick> learntrick: CustomEvent<Trick> removetrick: CustomEvent<Trick>
  • 63.
    export type Trick= { name: string difficulty: 'easy' | 'medium' | 'hard' } sk-user Implementation Types, template type Props = { name: string age: number tricks: Array<Trick> } const template = document.createElement('template') template.innerHTML = ` <style> :host { … } </style> <header> Hello <b id="name"></b>! Let's skate! </header> <div> Only <b id="age"></b> years old? Time to learn new tricks! </div> <form> <input name="trickName" value=""> <select id="trickDifficulty" class="form-control"> </form> ` // user.component.ts
  • 64.
    export class Userextends HTMLElement implements Props { set name(value: string) {} get name() {} set age(value: number) {} get age() {} set tricks(value: Array<Trick>) {} get tricks() { private _tricks: Array<Trick> = [] private view: { form: HTMLFormElement trickList: HTMLUListElement age: HTMLElement name: HTMLElement } } sk-user Implementation Define Properties window.customElements.define('sk-user', User) // user.component.ts
  • 65.
    export class Userextends HTMLElement implements Props { constructor() { super() const shadowRoot = this.attachShadow({ mode: 'open' }) shadowRoot.appendChild(template.content.cloneNode(true)) this.view = { form: shadowRoot.querySelector('form'), trickList: shadowRoot.querySelector('#trick-list'), age: shadowRoot.querySelector('#age'), name: shadowRoot.querySelector('#name'), } this.view.form.addEventListener( 'submit', this.handleNewTrickAddition ) } } sk-user Implementation construction // user.component.ts
  • 66.
    export class Userextends HTMLElement implements Props { attributeChangedCallback( name: Attrs, oldValue: string | null, newValue: string | null) { this.render() } connectedCallback() { this.render() } } sk-user Implementation Reactions, Rendering // user.component.ts
  • 68.
    “ So what’s theproblem with vanilla WebComponents ?
  • 69.
    “ UNFORTUNATELY THE DEVELOPER EXPERIENCE OFBUILDING AN APPLICATION WITH WEB COMPONENTS TODAY IS QUITE PAINFUL SAM SACCONE
  • 70.
  • 71.
  • 72.
    WC with great DX ●Performant declarative renderer ● Declarative API ○ async reactive data flow pipeline ○ state handling ○ extensibility
  • 73.
    From Imperative to Declarative+ Performant rendering
  • 74.
  • 75.
  • 76.
    class App extendsHTMLElement { render() { const { tricks, logs } = this const Root = () => <div> <ul class="log"> {logs.map(item => <TrickLog {...item} />)} </ul> <sk-user name="Martin" age={30} tricks={tricks} onLearnTrick={this.handleLearnTrick} onRemoveTrick={this.handleRemoveTrick} /> </div> this.preactDOM = render(<Root/>, this.shadowRoot, this.preactDOM) } Preact connectedCallback() { console.log('App mounted') this.render() }
  • 77.
    lit-html HTML templates, viaJS tagged template literals ES6 te l it s
  • 78.
    JS tagged templateliterals ? https://appendto.com/2017/02/advanced-javascript-es2015-template-strings-with-tagged-templates/ Tag ( function html(strings, ...rest){} ) + ES6 string literal (`hello`)
  • 79.
    const sayHello =(name: string) => html`<div>Hello ${name}!</div>` lit-html const container = document.querySelector('#container') render(sayHello('Steve'), container) // renders -> <div>Hello Steve!</div> to container
  • 80.
    class App extendsHTMLElement { render() { const { tricks, logs } = this const template = html` <ul class="log"> ${logs.map(item => CreateLogEvent(item))} </ul> <sk-user name="Martin" age="30" tricks="${tricks}" on-learnTrick="${this.handleNewTrick}" on-removeTrick="${this.handleRemoveTrick}" ><sk-user> ` render(template, this.shadowRoot) } } lit-html connectedCallback() { console.log('App mounted') this.render() }
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
    const withShadow =<T extends Constructor>(Base: T) => class extends Base { constructor(...args: Array<any>) { super(...args) this.attachShadow({ mode: 'open' }) } } mixins class App extends withShadow(HTMLElement) { } https://blog.mariusschulz.com/2017/05/26/typescript-2-2-mixin-classes
  • 86.
    Mmm so let’ssay something like...
  • 87.
    export class Userextends Component<Props> { static readonly is = 'sk-user' static readonly properties = { age: { type: Number, reflectToAttribute: true, value: 0, }, tricks: { type: Array, value: [], }, } render() { const { name, age, tricks } = this.props return html` <header>Hello <b id="name">${name}</b>! Let's skate!</header> <div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div> ` }
  • 88.
    export class Userextends Component<Props> { static readonly is = 'sk-user' static readonly properties = { age: { type: Number, reflectToAttribute: true, value: 0, }, tricks: { type: Array, value: [], }, } render() { const { name, age, tricks } = this.props return html` <header>Hello <b id="name">${name}</b>! Let's skate!</header> <div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div> ` }
  • 89.
    export class Userextends Component<Props> { static readonly is = 'sk-user' static readonly properties = { age: { type: Number, reflectToAttribute: true, value: 0, }, tricks: { type: Array, value: [], }, } render() { const { name, age, tricks } = this.props return html` <header>Hello <b id="name">${name}</b>! Let's skate!</header> <div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div> ` }
  • 90.
    export class Userextends Component<Props> { static readonly is = 'sk-user' static readonly properties = { age: { type: Number, reflectToAttribute: true, value: 0, }, tricks: { type: Array, value: [], }, } render() { const { name, age, tricks } = this.props return html` <header>Hello <b id="name">${name}</b>! Let's skate!</header> <div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div> ` }
  • 91.
    export class Userextends Component<Props> { static readonly is = 'sk-user' static readonly properties = { age: { type: Number, reflectToAttribute: true, value: 0, }, tricks: { type: Array, value: [], }, } render() { const { name, age, tricks } = this.props return html` <header>Hello <b id="name">${name}</b>! Let's skate!</header> <div>Only <b id="age">${age}</b> years old? Time to learn new tricks!</div> ` }
  • 92.
    const Component =withComponent() Component
  • 93.
    const withComponent = <P= {}>(Base = HTMLElement) => withProps<P>( withRender( withShadow(Base) ) ) withComponent
  • 94.
    export const withShadow= <T>(Base: T) => class extends Base { constructor(...args: Array<any>) { super(...args) this.attachShadow({ mode: 'open' }) } } withShadow
  • 95.
    import { TemplateResult} from 'lit-html' import { render } from 'lit-html/lib/lit-extended' const withRender = <T extends Constructor<CustomElement>>(Base: T) => { abstract class WithRender extends Base { private get renderRoot() { return this.shadowRoot ? this.shadowRoot : this } abstract render(): TemplateResult scheduleRender() { this.renderer() } private renderer() { render( this.render && this.render(), this.renderRoot ) } } return WithRender withRender
  • 96.
    const withProps = <P= {},T extends Constructor<CustomElement>>(Base: T) => class extends Base { private static _properties: PropDefinitionMap static get properties() {} static set properties(props: PropDefinitionMap) { this.setupProps(props) } scheduleRender?: () => void attributeChangedCallback(name, newValue) { this[name] = _deserializeValue(newValue) } connectedCallback() { this.scheduleRender && this.scheduleRender() } get props(): P {...} private static setupProps(properties) {} withProps
  • 97.
    private static setupProps(properties:PropDefinitionMap) { const ctor = this const propNames = Object.keys(properties) propNames.forEach(name => { const definition = properties[name] const _value = Symbol(name) const defaultValue = _deserializeValue(definition.value) if (_shouldSetObservedAttributes(definition.type)) { ctor.observedAttributes = [name] } Object.defineProperty(ctor.prototype, name, { configurable: true, set(this: any, val: any) { this[_value] = _deserializeValue(val, definition.type) this.scheduleRender && this.scheduleRender() }, get(this: any) { const val = this[_value] return val == null ? defaultValue : val }, }) }) } withProps
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
    Pluggable ? - mixins -utils - custom renderers - fine grained LC hooks
  • 103.
    skateJS Renderers - renderer-preact - renderer-react -renderer-lit-html - renderer-vue ( TODO ) - custom
  • 104.
    Micro library ? *supportsthe module field. You can get smaller sizes with tree shaking.
  • 105.
    HOW to build aWebComponent with skateJS
  • 106.
  • 107.
    What did we learn? Component API
  • 108.
  • 109.
  • 110.
  • 111.
    Thank you !Martin Hochel @martin_hotell