Last active
January 27, 2025 11:30
-
-
Save trickypr/3b67cbe7b92523fb37a46ddf2bcaff42 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| import { signal, effect, batch } from 'https://cdn.jsdelivr.net/npm/@preact/signals-core@1.8.0/dist/signals-core.mjs' | |
| /** @import { Signal } from '@preact/signal-core' */ | |
| /** | |
| * @param {string} initialStateDef | |
| * @param { {name: string, value: string}[] } attributes | |
| */ | |
| function createEntry(initialStateDef, attributes) { | |
| const state = Object.fromEntries( | |
| Object | |
| .entries(eval?.(`(${initialStateDef})`)) | |
| .map(([key, value]) => [key, signal(value)]) | |
| ) | |
| /** | |
| * @type {Record<string, string | ((el: HTMLElement, ...args: any[]) => any)>} | |
| */ | |
| const functions = Object.fromEntries( | |
| [...attributes] | |
| .filter(({name}) => name.startsWith('.')) | |
| .map(({name, value}) => [name.substring(1), value]) | |
| ) | |
| const proxied = new Proxy( | |
| { | |
| state, | |
| functions, | |
| withContext(/** @type {string} */ expr) { | |
| if (!expr.includes('=>')) { | |
| expr = '() => {' + expr + '}' | |
| } | |
| const fnCreator = new Function('proxy', `with(proxy){return el => ${expr}}`) | |
| const fn = fnCreator(proxied) | |
| return (el, ...args) => fn(el)(...args) | |
| } | |
| }, { | |
| has(target, prop) { | |
| if (typeof prop == 'symbol') return false | |
| return prop == 'withContext' || prop in target.state || prop in target.functions | |
| }, | |
| get(target, prop, reciver) { | |
| if (typeof prop == 'symbol') return false | |
| if (prop in target.state) { | |
| return target.state[prop].value | |
| } | |
| if (prop == 'withContext') return target.withContext | |
| if (!target.functions[prop]) return | |
| // Functions are stored as strings up until they are first | |
| // evaluated. | |
| if (typeof target.functions[prop] === 'string') { | |
| target.functions[prop] = target.withContext(target.functions[prop]) | |
| } | |
| return (...args) => target.functions[prop](null, ...args) | |
| }, | |
| set(target, prop, value, reciver) { | |
| if (prop in target.state) { | |
| target.state[prop].value = value | |
| } else { | |
| target.state[prop] = signal(value) | |
| } | |
| return true | |
| } | |
| } | |
| ) | |
| return proxied | |
| } | |
| /** @typedef {ReturnType<createEntry>} Context */ | |
| /** | |
| * @param {HTMLElement} el | |
| * @param {string} nodeName | |
| * @param {string} nodeValue | |
| * @param {Context} context | |
| */ | |
| function tryAtAttribute(el, nodeName, nodeValue, context) { | |
| if (!nodeName.startsWith('@') && !nodeName.startsWith('x')) return | |
| const executor = context.withContext(nodeValue || '') | |
| el.removeAttribute(nodeName) | |
| el.addEventListener(nodeName.substring(1), (event) => { | |
| batch(() => executor(el, event)) | |
| }) | |
| } | |
| /** | |
| * @param {HTMLElement} el | |
| * @param {Context} context | |
| */ | |
| function effectAttribute(el, context) { | |
| const script = el.getAttribute('h-effect') || '' | |
| el.removeAttribute('h-effect') | |
| const run = context.withContext(script) | |
| effect(() => run(el)) | |
| } | |
| /** | |
| * @param {HTMLElement} el | |
| * @param {Context} context | |
| */ | |
| function showAttribute(el, context) { | |
| const script = el.getAttribute('h-show') || '' | |
| el.removeAttribute('h-show') | |
| const run = context.withContext(`return ${script}`) | |
| const {display} = window.getComputedStyle(el) | |
| effect(() => { | |
| const shouldShow = run(el) | |
| el.style.display = shouldShow ? display : 'none' | |
| }) | |
| } | |
| /** | |
| * @param {HTMLElement} node | |
| */ | |
| function construct(node) { | |
| const data = createEntry(node.getAttribute('h-data') || '{}', node.attributes) | |
| node.removeAttribute('h-data') | |
| function handleNode(/** @type {ChildNode} */ node) { | |
| // Nodes that we will not process | |
| if ([2, 4, 7, 8, 9, 10, 11].includes(node.nodeType)) return | |
| if (node.nodeType === node.TEXT_NODE) { | |
| const templateText = (node.textContent || '').replaceAll('`', '`') | |
| const executor = data.withContext(`return \`${templateText}\`.replaceAll('`', '\`')`) | |
| effect(() => { | |
| node.textContent = executor(node) | |
| }) | |
| return | |
| } | |
| // Must be an element | |
| /** @type {HTMLElement} */ | |
| const el = node | |
| for (const { nodeName, nodeValue } of el.attributes) { | |
| tryAtAttribute(el, nodeName, nodeValue, data) | |
| } | |
| if (el.hasAttribute('h-effect')) effectAttribute(el, data) | |
| if (el.hasAttribute('h-show')) showAttribute(el, data) | |
| if (el.hasAttribute('h-opaque')) return | |
| el.childNodes.forEach(handleNode) | |
| } | |
| handleNode(node) | |
| } | |
| document.querySelectorAll('[h-data]').forEach(construct) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment