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