Created
July 30, 2021 19:53
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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