Skip to content

Instantly share code, notes, and snippets.

@piyushchauhan2011
Created November 25, 2019 06:19
Show Gist options
  • Select an option

  • Save piyushchauhan2011/d30bf4bce524e97f688c948887fbdb9d to your computer and use it in GitHub Desktop.

Select an option

Save piyushchauhan2011/d30bf4bce524e97f688c948887fbdb9d to your computer and use it in GitHub Desktop.
Vanilla Hooks with Web Components
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Web Component Hooks</title>
</head>
<body>
<my-counter></my-counter>
<script type="module">
import {
html,
render,
directive
} from "https://unpkg.com/lit-html/lit-html.js";
const phaseSymbol = Symbol.for("haunted.phase");
const hookSymbol = Symbol.for("haunted.hook");
const updateSymbol = Symbol.for("haunted.update");
const commitSymbol = Symbol.for("haunted.commit");
const effectsSymbol = Symbol.for("haunted.effects");
let current;
let currentId = 0;
function setCurrent(element) {
current = element;
}
function clear() {
current = null;
currentId = 0;
}
function notify() {
let id = currentId;
currentId++;
return id;
}
function scheduler() {
let tasks = [];
let id;
function runTasks() {
id = null;
let t = tasks;
tasks = [];
for (var i = 0, len = t.length; i < len; i++) {
t[i]();
}
}
return function(task) {
tasks.push(task);
if (id == null) {
id = requestAnimationFrame(runTasks);
}
};
}
const read = scheduler();
const write = scheduler();
class Container {
constructor(renderer, frag, host) {
this.renderer = renderer;
this.frag = frag;
this.host = host || frag;
this[hookSymbol] = new Map();
this[phaseSymbol] = null;
this._updateQueued = false;
}
update() {
if (this._updateQueued) return;
read(() => {
let result = this.handlePhase(updateSymbol);
write(() => {
this.handlePhase(commitSymbol, result);
if (this[effectsSymbol]) {
write(() => {
this.handlePhase(effectsSymbol);
});
}
});
this._updateQueued = false;
});
this._updateQueued = true;
}
handlePhase(phase, arg) {
this[phaseSymbol] = phase;
switch (phase) {
case commitSymbol:
return this.commit(arg);
case updateSymbol:
return this.render();
case effectsSymbol:
return this.runEffects(effectsSymbol);
}
this[phaseSymbol] = null;
}
commit(result) {
render(result, this.frag);
this.runEffects(commitSymbol);
}
render() {
setCurrent(this);
let result = this.args
? this.renderer.apply(this.host, this.args)
: this.renderer.call(this.host, this.host);
clear();
return result;
}
runEffects(symbol) {
let effects = this[symbol];
if (effects) {
setCurrent(this);
for (let effect of effects) {
effect.call(this);
}
clear();
}
}
teardown() {
let effects = this[effectsSymbol];
if (effects) {
for (let effect of effects) {
effect.teardown();
}
}
}
}
function component(renderer, BaseElement = HTMLElement) {
class Element extends BaseElement {
static get observedAttributes() {
return renderer.observedAttributes || [];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this._container = new Container(renderer, this.shadowRoot, this);
}
connectedCallback() {
this._container.update();
}
disconnectedCallback() {
this._container.teardown();
}
attributeChangedCallback(name, _, newValue) {
let val = newValue === "" ? true : newValue;
Reflect.set(this, name, val);
}
}
function reflectiveProp(initialValue) {
let value = initialValue;
return Object.freeze({
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
value = newValue;
this._container.update();
}
});
}
const proto = new Proxy(BaseElement.prototype, {
set(target, key, value, receiver) {
if (key in target) {
Reflect.set(target, key, value);
}
let desc;
if (typeof key === "symbol" || key[0] === "_") {
desc = {
enumerable: true,
configurable: true,
writable: true,
value
};
} else {
desc = reflectiveProp(value);
}
Object.defineProperty(receiver, key, desc);
if (desc.set) {
desc.set.call(receiver, value);
}
return true;
}
});
Object.setPrototypeOf(Element.prototype, proto);
return Element;
}
class Hook {
constructor(id, el) {
this.id = id;
this.el = el;
}
}
function use(Hook, ...args) {
let id = notify();
let hooks = current[hookSymbol];
let hook = hooks.get(id);
if (!hook) {
hook = new Hook(id, current, ...args);
hooks.set(id, hook);
}
return hook.update(...args);
}
function hook(Hook) {
return use.bind(null, Hook);
}
function setEffects(el, cb) {
if (!(effectsSymbol in el)) {
el[effectsSymbol] = [];
}
el[effectsSymbol].push(cb);
}
const useEffect = hook(
class extends Hook {
constructor(id, el) {
super(id, el);
this.values = false;
setEffects(el, this);
}
update(callback, values) {
this.callback = callback;
this.lastValues = this.values;
this.values = values;
}
call() {
if (this.values) {
if (this.hasChanged()) {
this.run();
}
} else {
this.run();
}
}
run() {
this.teardown();
this._teardown = this.callback.call(this.el);
}
teardown() {
if (this._teardown) {
this._teardown();
}
}
hasChanged() {
return (
this.lastValues === false ||
this.values.some((value, i) => this.lastValues[i] !== value)
);
}
}
);
const useState = hook(
class extends Hook {
constructor(id, el, initialValue) {
super(id, el);
this.updater = this.updater.bind(this);
this.makeArgs(initialValue);
}
update() {
return this.args;
}
updater(value) {
this.makeArgs(value);
this.el.update();
}
makeArgs(value) {
this.args = Object.freeze([value, this.updater]);
}
}
);
const useReducer = hook(
class extends Hook {
constructor(id, el, _, initialState) {
super(id, el);
this.dispatch = this.dispatch.bind(this);
this.state = initialState;
}
update(reducer) {
this.reducer = reducer;
return [this.state, this.dispatch];
}
dispatch(action) {
this.state = this.reducer(this.state, action);
this.el.update();
}
}
);
const useMemo = hook(
class extends Hook {
constructor(id, el, fn, values) {
super(id, el);
this.value = fn();
this.values = values;
}
update(fn, values) {
if (this.hasChanged(values)) {
this.values = values;
this.value = fn();
}
return this.value;
}
hasChanged(values) {
return values.some((value, i) => this.values[i] !== value);
}
}
);
class DirectiveContainer extends Container {
commit(result) {
this.host.setValue(result);
this.host.commit();
}
}
const map = new WeakMap();
function withHooks(renderer) {
function factory(...args) {
return part => {
let cont = map.get(part);
if (!cont) {
cont = new DirectiveContainer(renderer, part);
map.set(part, cont);
}
cont.args = args;
cont.update();
};
}
return directive(factory);
}
// End of haunted library
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "reset":
return initialState;
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return html`
Count: ${state.count}
<button @click=${() => dispatch({ type: "reset" })}>Reset</button>
<button @click=${() => dispatch({ type: "increment" })}>+</button>
<button @click=${() => dispatch({ type: "decrement" })}>-</button>
`;
}
customElements.define("my-counter", component(Counter));
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment