Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Reactive Type safe Webcomponents with skateJS

Martin Hochel
September 22, 2017

Reactive Type safe Webcomponents with skateJS

You know the drill right? new cool framework/library appears... boom! new Datepicker in that framework follows and soon enough whole UI libraries, again and again....

It's 2017 and it's time to stop this madness once and for all! How you ask?

In this talk I will do an overview of component creation in terms of re-usability followed up with real life examples how to create performant, reactive, small and type-safe web components with tiny superpowered library called SkateJS.

Write once, use everywhere by using the platform.

Source code: https://github.com/Hotell/reactive-typesafe-webcomponents
SkateJS: https://github.com/skatejs/skatejs

Martin Hochel

September 22, 2017
Tweet

More Decks by Martin Hochel

Other Decks in Programming

Transcript

  1. Martin Hochel SE, Prague / Czech Republic @martin_hotell github.com/Hotell Hello

    Warsaw ! ▪ @ngPartyCz meetup founder ▪ Author of ngMetadata ▪ Member of @skate_js ▪ ▪
  2. Problem 1 Create reusable widget Library size 10.0 kB 4.0

    kB 4.0 kB Implementation size 4.0 kB 4.0 kB
  3. 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
  4. // $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
  5. 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
  6. 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
  7. 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
  8. // $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
  9. 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
  10. 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
  11. 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
  12. // $0 === User instance $0.addEventListener('learntrick',(event)=>{ /* ... */ })

    // Imperative (JS) Nope // Declarative (HTML) Custom Elements API Events
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. Templates <template> <style> :host { display: flex } ul {

    margin: 1rem; } </style> <div> <ul> <li>Name: <b id="name"></b></li> <li>Age: <b id="age"></b></li> </ul> <div> <slot></slot> </div> </div> </template> // user.template.html // user.component.js const template = document.querySelector('template') const view = template.content.cloneNode(true)
  20. 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>
  21. 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>
  22. 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>
  23. 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>
  24. 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>
  25. 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>
  26. 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>
  27. Shadow DOM constructor() { super() const shadowRoot = this.attachShadow({ mode:

    'open' }) shadowRoot.appendChild(template.content.cloneNode(true)) }
  28. WC App Reactive data-flow sk-app sk-user name: string age: number

    tricks: Array<Trick> learntrick: CustomEvent<Trick> removetrick: CustomEvent<Trick>
  29. export type Trick = { name: string difficulty: 'easy' |

    'medium' | 'hard' } sk-user Implementation Types, template type Attrs = 'name' | 'age' 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 autocomplete="off"> <input name="trickName" value=""> <select id="trickDifficulty" class="form-controll"> </form> ` // user.component.ts
  30. 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
  31. 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
  32. 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
  33. index.html <sk-user name="Martin" age="100" ></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} />
  34. import { withComponent } from 'skatejs'; import withPreact from '@skatejs/renderer-preact';

    export const Component = withComponent(withPreact()) Choose renderer
  35. import { withComponent } from 'skatejs'; import withPreact from '@skatejs/renderer-preact';

    export const Component = withComponent(withPreact()) Choose renderer
  36. import { withComponent } from 'skatejs'; import withPreact from '@skatejs/renderer-preact';

    export const Component = withComponent(withPreact()) Choose renderer
  37. import { h } from 'preact' import { props }

    from 'skatejs' import { Component } from './base' type Props = { name: string } class Hello extends Component<Props> { static readonly props = { name: props.string, } renderCallback() { const { name } = this.props return <span>Hello, {name}!</span> } } customElements.define('x-hello', Hello) Implement Component
  38. import { h } from 'preact' import { props }

    from 'skatejs' import { Component } from './base' type Props = { name: string } class Hello extends Component<Props> { static readonly props = { name: props.string, } renderCallback() { const { name } = this.props return <span>Hello, {name}!</span> } } customElements.define('x-hello', Hello) Implement Component
  39. import { h } from 'preact' import { props }

    from 'skatejs' import { Component } from './base' type Props = { name: string } class Hello extends Component<Props> { static readonly props = { name: props.string, } renderCallback() { const { name } = this.props return <span>Hello, {name}!</span> } } customElements.define('x-hello', Hello) Implement Component
  40. import { h } from 'preact' import { props }

    from 'skatejs' import { Component } from './base' type Props = { name: string } class Hello extends Component<Props> { static readonly props = { name: props.string, } renderCallback() { const { name } = this.props return <span>Hello, {name}!</span> } } customElements.define('x-hello', Hello) Implement Component
  41. import { h } from 'preact' import { props }

    from 'skatejs' import { Component } from './base' type Props = { name: string } class Hello extends Component<Props> { static readonly props = { name: props.string, } renderCallback() { const { name } = this.props return <span>Hello, {name}!</span> } } customElements.define('x-hello', Hello) Implement Component
  42. import { h, props } from 'skatejs' import { Component

    } from './base' type Props = { name: string } class Hello extends Component<Props> { } declare global { namespace JSX { interface IntrinsicElements { 'x-hello': Props } } } Implement Component
  43. Skate Roadmap - 5.0 stable - No default renderer in

    core ( choose what you want ) ✅ - Mixins for everything - ✅ - API for turning off ShadowDOM - ✅ - Server side rendering - ✅ - Testing with Jest - ✅