Skip to content

Instantly share code, notes, and snippets.

@trickypr
Last active January 27, 2025 11:30
Show Gist options
  • Select an option

  • Save trickypr/3b67cbe7b92523fb37a46ddf2bcaff42 to your computer and use it in GitHub Desktop.

Select an option

Save trickypr/3b67cbe7b92523fb37a46ddf2bcaff42 to your computer and use it in GitHub Desktop.
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('`', '&grave;')
const executor = data.withContext(`return \`${templateText}\`.replaceAll('&grave;', '\`')`)
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