BUILD YOUR OWN FRONTEND FRAMEWORK IN 10 MINUTES

Lukas Gamper

Goal

  • Vue.js / MobX like framework
  • No external libraries, no virtual DOM
  • Chrome, Safari, Edge support
  • No bundler like Webpack needed

Agenda

  • Component System
  • Compiled Templates
  • Reactivity

Component System

<html>
  <head>
    <script>
      class HWElement extends HTMLElement {
        constructor() {
          super();
          this.innerHTML = 'Hello World'
        }
      }
      customElements.define('hello-world', HWElement);
    </script>
  </head>
  <body>
    <hello-world></hello-world>
  </body>
</html>

CustomElement

<html>
  <head>
    <script>
      class HWElement extends HTMLElement {
        constructor() {
          super();
          const shadowRoot = this.attachShadow({ mode: 'open' });
          shadowRoot.innerHTML = 'Hello World'
        }
      }
      customElements.define('hello-world', HWElement);
    </script>
  </head>
  <body>
    <hello-world></hello-world>
  </body>
</html>

Component System

Shadow DOM

Shadow DOM

  • Scoped CSS
  • named and default <slot>
export default function createComponent(tagName, content) {
  const HWElement = class extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: 'open' });
      shadowRoot.innerHTML = content;
    }
  };
  customElements.define(tagName, HWElement);
}
<html>
  <head>
    <script type="module">
      import createComponent from './createComponent.js';
      createComponent('hello-world', 'Hello World');
    </script>
  </head>
  <body>
    <hello-world></hello-world>
  </body>
</html>

index.html

createComponent.js

Component System

JavaScript Module

Component System

  • customElement
  • Shadow DOM
  • JavaScript Modules
export default function createComponent(tagName, templateSelector) {
  const HWElement = class extends HTMLElement {
    constructor() {
      super();
      const template = document.querySelector(templateSelector);
      const stampedTemplate = document.importNode(template.content, true);
      const shadowRoot = this.attachShadow({ mode: 'open' });
      shadowRoot.appendChild(stampedTemplate);
    }
  };
  customElements.define(tagName, HWElement);
}
<html>
  <head>
    <template id="hw-tempalte">Hello World</template>
    <script type="module">
      import createComponent from './createComponent.js';
      createComponent('hello-world', '#hw-tempalte');
    </script>
  </head>
  <body>
    <hello-world></hello-world>
  </body>
</html>

index.html

createComponent.js

Compiled Tempaltes

<template> Tag

  • <script> tags not executed 
  • ressources not fetched, e.g <img>, <video>
  • custom elements not evaluated
export default function createComponent(tagName, templateSelector, ComponentCls) {
  const HWElement = class extends HTMLElement {
    super();
    const scope = new ComponentCls();
    // ...
  };
  customElements.define(tagName, HWElement);
}
<html>
  <head>
    <template id="hw-tempalte">Hello ${name}</template>
    <script type="module">
      import createComponent from './createComponent.js';
      class HelloWorld {
        constructor() {
          this.name = "Jon";
        }
      }
      createComponent('hello-world', '#hw-tempalte', HelloWorld);
    </script>
  </head>
  <body>
    <hello-world></hello-world>
  </body>
</html>

index.html

createComponent.js

Compiled Tempaltes

export default function createComponent(tagName, templateSelector, ComponentCls) {
  const HWElement = class extends HTMLElement {
    constructor() {
      super();
      const scope = new ComponentCls();
      const template = document.querySelector(templateSelector);
      const stampedTemplate = document.importNode(template.content, true);
      this.compileContentExpressions(scope, template);
      const shadowRoot = this.attachShadow({ mode: 'open' });
      shadowRoot.appendChild(stampedTemplate);
    }
    compileContentExpressions(scope, template) {
      const xPath = '//text()[contains(.,"${") and contains(substring-after(.,"${"),"}")]';
      for (let textNode of this.evaluateXPaht(template, xPath)) {
        const compiledValue = new Function('', `with (this) return \`${textNode.nodeValue}\`;`);
        textNode.nodeValue = compiledValue.call(scope);
      }
    }
    *evaluateXPaht(template, xPath) {
      const xPathResult = document.evaluate(
        xPath, template.firstElementChild, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null
      );
      for (let index = 0; index < xPathResult.snapshotLength; index += 1) {
        yield xPathResult.snapshotItem(index);
      }
    }
  };
  customElements.define(tagName, HWElement);
}

createComponent.js

Compiled Tempaltes

Compiled Templates

  • <tempalte> tag
  • xPath
  • Generator
  • String Template

Reactivity

  • Track which property was called
  • Track which expression depends on which property

 

Update Expression

Update  Property

  • for each depending expression
  • check if property was called
  • if true, update expression
let activeExpression = null;
export default class Expression {
  constructor(scope, callable, observer) {
    this.observer = observer;
    this.scope = scope;
    this.callable = callable;
    this.update();
  }
  get() {
    this.dependsOn = new WeakSet();
    activeExpression = this;
    let value = this.callable.call(this.scope);
    activeExpression = null;
    return value;
  }
  update() {
    let value = this.get();
    this.observer(value);
  }
  static active() {
    return activeExpression;
  }
}

Expression.js

Reactivity

import ComponentProxyHandler from './ComponentProxyHandler.js'
import Expression from './Expression.js';
export default function createComponent(tagName, templateSelector, componentFactory) {
  // ...
  const ComponentElement = class extends HTMLElement {
    // ...
    compileContentExpressions(scope, template) {
      const xPath = '//text()[contains(.,"${") and contains(substring-after(.,"${"),"}")]';
      for (let textNode of this.evaluateXPaht(template, xPath)) {
        const callable = new Function('', `with (this) return \`${textNode.nodeValue}\`;`);
        const observer = value => { textNode.nodeValue = value; };
        new Expression(scope, callable, observer);
      }
    }
  };
  customElements.define(tagName, ComponentElement);
}

createComponent.js

Reactivity

import ComponentProxyHandler from './ComponentProxyHandler.js'
import Expression from './Expression.js';
export default function createComponent(tagName, templateSelector, componentFactory) {
  let Base = class {
    constructor() {
      let proxyHandler = new ComponentProxyHandler();
      return new Proxy(this, proxyHandler);
    }
  };
  let ComponentClass = componentFactory(Base);
  const ComponentElement = class extends HTMLElement {
    // ...
  };
  customElements.define(tagName, ComponentElement);
}

createComponent.js

Reactivity

<script type="module">
  import createComponent from './createComponent.js';
  function helloWorldFactory(Base) {
    return class extends Base {
      constructor() {
        super();
        this.name = "Jon";
      }
    };
  }
  createComponent('hello-world', '#hw-tempalte', helloWorldFactory);
</script>

index.html

import Expression from './Expression.js';
export default class ComponentProxyHandler {
  constructor() {
    this.calledBy = new Set();
  }
  get(target, key, receiver) {
    if (Expression.active() instanceof Expression) {
      this.calledBy.add(Expression.active();
      Expression.active().dependsOn.add(this);
    }
    return Reflect.get(target, key, receiver);
  }
  set(target, key, value, receiver) {
    this.calledBy.forEach(expression => {
      if (expression.dependsOn.has(this)) {
        expression.update();
      }
    });
    return Reflect.set(target, key, value, receiver);
  }
}

ComponentProxy.js

Reactivity

Reactivity

  • ObjectProxy
  • Reflect
  • Set / WeakSet

Result

https://github.com/usystems/reactive-web-components-tutorial

Reactive Web Components Tutorial

Build your own Frontend Framework in 10 minutes

By gamperl

Build your own Frontend Framework in 10 minutes

  • 441