Skip to content

Instantly share code, notes, and snippets.

@Minigugus
Created July 30, 2021 19:53
Show Gist options
  • Save Minigugus/abc3460afc8a1c8a3309a5f3d0657ce3 to your computer and use it in GitHub Desktop.
Save Minigugus/abc3460afc8a1c8a3309a5f3d0657ce3 to your computer and use it in GitHub Desktop.
Dead simple (~3kB) declarative library supporting IE9 that emulate the Vue 3 `ref`-like API
// MIT License (C) Minigugus 2021
/**
* @template T
* @param {T} value
* @class
*/
function Ref(value, debounce = 0) {
if (typeof self === 'undefined')
return new Ref(value);
this._value = value;
this._nextValue = value;
this._debounce = debounce;
this._listeners = [];
this._triggerTimer = null;
}
Object.defineProperty(Ref.prototype, 'value', {
configurable: false,
enumerable: true,
get() {
return this._value;
},
set(v) {
this._nextValue = v instanceof Ref ? v.value : v;
if (this._triggerTimer === null) {
this._triggerTimer = setTimeout(() => {
let old = this._value;
this._triggerTimer = null;
this._value = this._nextValue;
if (old !== this.value)
this._listeners.forEach(l => l(old));
}, this._debounce);
}
}
});
/**
*
* @template T
* @param {{ value: T }} ref
* @param {(oldValue: T) => void} listener
*/
const watch = (ref, listener) => {
ref._listeners.push(listener);
return () => {
ref._listeners = ref._listeners.filter(l => l !== listener);
};
};
/**
*
* @template {Ref<unknown>[]} T
* @template {any} U
* @param {T} refs
* @param {(...refs: { [i in keyof T]: T[i] extends Ref<infer R> ? R : never }) => U} provider
* @returns {Ref<U>}
*/
const computed = (refs, provider) => {
const r = new Ref(provider.apply(null, refs.map(r => r.value)));
const update = () => (r.value = provider.apply(null, refs.map(r => r.value)));
for (let i = 0; i < refs.length; i++)
watch(refs[i], update);
return r;
};
/**
* @typedef HTMLElementTags
* @type {keyof HTMLElementTagNameMap}
*/
/**
*
* @template {HTMLElementTags} T
* @param {T} name
* @param {{ [key in keyof HTMLElement]: HTMLElementTagNameMap[T][key] | Ref<HTMLElementTagNameMap[T][key]> }} attributes
* @param {(Node | Ref<Node>)[]} children
* @returns {HTMLElementTagNameMap[T]}
*/
const h = (name, attributes = {}, children = []) => {
const el = document.createElement(name);
for (const key in attributes)
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
const valueOrRef = attributes[key];
if (!(valueOrRef instanceof Ref))
el[key] = valueOrRef;
else {
el[key] = valueOrRef.value;
watch(valueOrRef, () => (el[key] = valueOrRef.value));
}
}
for (let i = 0; i < children.length; i++) {
const childOrRef = children[i];
if (childOrRef == null) // == null <=> === null || === undefined
continue;
if (!(childOrRef instanceof Ref)) // string | Node
if (childOrRef instanceof Node) // Node
el.appendChild(childOrRef);
else // string
el.appendChild(document.createTextNode(String(childOrRef)));
else if (childOrRef.value instanceof Node) { // Ref<Node> (user should avoid mixing string & Node types in the same Ref...)
el.appendChild(childOrRef.value);
watch(childOrRef, old => el.replaceChild(childOrRef.value, old));
} else { // Ref<string>
const node = document.createTextNode(childOrRef.value);
el.appendChild(node);
watch(childOrRef, () => (node.textContent = childOrRef.value));
}
}
return el;
};
// example
(() => {
const counter = new Ref(0, 200);
const input = new Ref('');
const tab1 = new Ref(h('p', {}, [
computed([counter], counter => 'Tab 1: counter = ' + counter),
]));
const tab2 = new Ref(h('p', {}, [
computed([input], input => 'Tab 2: input = ' + input),
]));
const tab = new Ref(tab1.value);
const el = h('div', {}, [
h('p', {}, [computed([counter, input], (counter, input) => 'counter = ' + counter + ', input = ' + input)]),
h('input', { onkeyup() { input.value = this.value; } }, []),
h('button', { onclick: () => (counter.value += 1) }, ['Increment']),
h('hr'),
h('button', {
disabled: computed([tab], tab => tab === tab1.value),
onclick: () => (tab.value = tab1.value)
}, ['Go to tab 1']),
h('button', {
disabled: computed([tab], tab => tab === tab2.value),
onclick: () => (tab.value = tab2.value)
}, ['Go to tab 2']),
tab
]);
watch(counter, oldValue => console.log('counter: ' + oldValue + ' -> ' + counter.value));
document.body.appendChild(el);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment