Core concepts of the hybrids library

Dominik Lubański

smalluban

Custom Elements API

Shadow DOM

HTML Templates

ES Modules

What are web components?

class or this syntax   ●  complex lifecycle callbacks  ●  stateful architecture

How can we create web components?

Can we do it simpler?

class MyElement extends HTMLElement {
  constructor() {
    super();
    ...
  }

  get text() {
    return 'Fresha' || this._text;
  }
    
  set text(val) {
    this._text = val;
  }
    
  someMethod() { ... }
}

customElements.define('my-element', MyElement);

computed property

method

class syntax

Step 1: Use Custom Elements API

class MyElement extends HTMLElement {}

Object.defineProperty(MyElement.prototype, 'text', {
  get: function get() {
    return 'Fresha' || this._text;
  },
  set: function set(val) {
    this._text = val;
  },
  configurable: true,
});

Object.defineProperty(MyElement.prototype, 'someMethod', {
  get: () => function someMethod() { ... },
  configurable: true,
});

customElements.define('my-element', MyElement);

get and set

methods

method as getter, which returns
a function

property is defined on the prototype

Step 2: Desugar class syntax using prototype

const MyElement = {
  text: {
    get: function get() {
      return 'Fresha' || this._text;
    },
    set: function set(val) {
      this._text = val;
    },
  },
  someMethod: {
    get: () => function someMethod() { ... },
  },
};

defineElement('my-element', MyElement);

descriptors with keys as property names

plain object

custom function, which creates class, applies descriptors and defines custom element

Step 3: Hide redundant code into the custom definition

const MyElement = {
  text: {
    get: ({ _text }) => 'Fresha' || _text,
    set: (host, val) => { host._text = val; },
  },
  someMethod: {
    get: (host) => () => { ... },
  },
};

defineElement('my-element', MyElement);

"this" replaced with
the first argument (element instance)

arrow functions - context is no longer required

Step 4: Get rid of "this"

const MyElement = {
  text: {
    get: (host, lastValue = 'Fresha') => lastValue,
    set: (host, newVal, lastValue) => newVal,
  },
  someMethod: {
    get: (host) => () => { ... },
  },
};

defineElement('my-element', MyElement);

get and set with the last saved value

set method returns new value

Step 5: Add middleware to save property value

function myFactory(defaultValue) {
  return {
    get: (host, lastValue = defaultValue) => lastValue,
    set: (host, newVal, lastValue) => newVal,
  };
}

const MyElement = {
  text: myFactory('Fresha'),
  otherText: myFactory('It is super cool!'),
  someMethod: {
    get: (host) => () => { ... },
  },
};

defineElement('my-element', MyElement);

function creates property definition dynamically

Step 6: Introduce property factory

factories can be reused for a number of properties

const MyElementOrig = {
  text: myFactory('Fresha'),
  someMethod: {
    get: (host) => () => { ... },
  },
};

const MyElementCompact = {
  text: 'Fresha',
  someMethod: (host) => () => { ... },
};

defineElement('my-element', MyElementCompact);

the type is used
to translate passed value to property factory or prefilled descriptor

Step 7: Introduce property translation

class MyElement extends HTMLElement {
  constructor() {
    super();
    ...
  }

  get text() {
    return 'Fresha' || this._text;
  }
    
  set text(val) {
    this._text = val;
  }
    
  someMethod() { ... }
}

customElements.define('my-element', MyElement);
const MyElement = {
  text: 'Fresha',
  someMethod: (host) => () => { ... },
};

defineElement('my-element', MyElement);

Complete definition syntax

What about lifecycles?

connect

disconnect

state

calculation

render

side effects

Simplified component lifecycle

connectedCallback() & disconnectedCallback()

Cache mechanism & change detection

const GithubStars = {
  user: 'hybridsjs',
  repo: 'hybrids',
  
  stars: ({ user, repo }) => {
    return getGitHubStars(user, repo);
  },
  
  render: ({ stars }) => {...},
};

defineElement('github-stars', GithubStars);

Cache mechanism by example (initial render)

  1. render requires stars property

  2. Cache calls stars getter, which requires user and repo properties

  3. Cache calls user and repo getters, and save them as dependencies
    of stars getter

  4. Cache returns stars getter result

const GithubStars = {
  user: 'hybridsjs',
  repo: 'hybrids',
  
  stars: ({ user, repo }) => {
    return getGitHubStars(user, repo);
  },
  
  render: ({ stars }) => {...},
};

defineElement('github-stars', GithubStars);

Cache mechanism by example (sequential render)

  1. render requires stars property

  2. Cache checks if dependencies (user or repo) cache state has changed

  3. Yes - cache clears dependencies, calls stars getter, saves new dependencies and returns result

  4. No - cache returns last calculated stars getter result

import { getGithubStars } from 'github-super-api';

const GithubStars = {
  user: 'hybridsjs',
  repo: 'hybrids',
  stars: ({ user, repo }) => {
    return getGithubStars(user, repo);
  },
  
  render: renderFactory(({ stars }) => {
    // do stuff to update DOM
  }),
};

User or repo property is changed

Change detection

renderFactory has to be notified about stars invalidation

observe() method of the property setup watchable property notified about the change in RAF queue

Next call for stars will return new value

const MyElement = {
  propertyName: {
    get: (host, lastValue) => {...},
    set: (host, newValue, lastValue) => {...},

    connect: (host, key, invalidate) => {
      // initalize code

      return () => {
        // clean up code
      };
    },
    observe: (host, value, lastValue) {...},
  },
};

connectedCallback() & disconnectedCallback()

callback function for manual cache invalidation

disconnect is
a function returned from connect callback

connect callback
is defined in the scope of property definition

import store from './my-redux-store';

function connect(store, mapState) {
  return {
    get: mapState
      ? () => mapState(store.getState()) 
      : () => store.getState(),

    connect: (host, key, invalidate) => {
      return store.subscribe(invalidate);
    },
  };
};

const MyElement = {
  value: connect(store, ({ value }) => value),
};

invalidate is passed to subscribe method

value property connected to
the redux store

Example using invalidate by the external library (redux)

subscribe returns unsubscribe callback

npm i hybrids
  • No class and this syntax
  • Easily re-use, merge or split definitions
  • No global lifecycle callbacks
  • Super fast value recalculation
  • Template engine based on tagged template literals
  • Hot module replacement support in dev mode

Key features of the hybrids library

Can we see it now?

Thank you.

Core concepts of the hybrids library

By Dominik Lubański

Core concepts of the hybrids library

Internal presentation for the Fresha team folks about my hybrids library based on a presentation created for ConFrontJS 2018.

  • 892