Last active
May 15, 2025 16:51
-
-
Save amatiasq/4832355e944212713a4d56777fd77bcd 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 { effect, isSignal } from './signal'; | |
export abstract class AmqElement extends HTMLElement { | |
#shadow: ShadowRoot; | |
#isInitialized = false; | |
#cleanup: (() => unknown)[] = []; | |
constructor() { | |
super(); | |
this.#shadow = this.attachShadow({ mode: 'open' }); | |
} | |
connectedCallback() { | |
this.#isInitialized = false; | |
this.#shadow.innerHTML = ''; | |
this.#render(); | |
} | |
disconnectedCallback() { | |
this.#cleanup.forEach((fn) => fn()); | |
this.#cleanup = []; | |
} | |
#render() { | |
if (this.#isInitialized) return; | |
this.#isInitialized = true; | |
this.#shadow.appendChild(this.render()); | |
} | |
render(): DocumentFragment { | |
throw new Error('Method not implemented.'); | |
} | |
html(strings: TemplateStringsArray, ...values: unknown[]) { | |
const html = strings.reduce( | |
(a, s, i) => (i < values.length ? `${a}${s}AMQ_${i}_` : `${a}${s}`), | |
'' | |
); | |
const template = document.createElement('template'); | |
template.innerHTML = html; | |
const fragment = template.content.cloneNode(true); | |
for (const node of iterateNodes(fragment)) { | |
for (const attr of Array(...node.attributes)) { | |
if (!attr.value.startsWith('AMQ_')) continue; | |
const index = parseInt(attr.value.split('_')[1], 10); | |
const value = values[index]; | |
if (isSignal(value)) { | |
this.#cleanup.push( | |
effect(() => { | |
if (value() == null) node.removeAttribute(attr.name); | |
else node.setAttribute(attr.name, String(value())); | |
}) | |
); | |
} else if (typeof value === 'function') { | |
node.removeAttribute(attr.name); | |
node.addEventListener(attr.name, value as any); | |
this.#cleanup.push(() => | |
node.removeEventListener(attr.name, value as any) | |
); | |
} else if (!value) { | |
node.removeAttribute(attr.name); | |
} else { | |
node.setAttribute(attr.name, String(value)); | |
} | |
} | |
} | |
return fragment as DocumentFragment; | |
} | |
} | |
function* iterateNodes(fragment: Node) { | |
const iterator = document.createNodeIterator( | |
fragment, | |
NodeFilter.SHOW_ELEMENT, | |
{ | |
acceptNode: (node) => | |
node instanceof HTMLElement | |
? NodeFilter.FILTER_ACCEPT | |
: NodeFilter.FILTER_REJECT, | |
} | |
); | |
let node: any; | |
while ((node = iterator.nextNode())) { | |
if (node) yield node as HTMLElement; | |
} | |
} |
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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Test</title> | |
</head> | |
<body> | |
<amq-button>TEST</amq-button> | |
<script type="module" src="./AmqButton.ts"></script> | |
</body> | |
</html> |
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
type SignalListener<T> = (updatedValue: T) => unknown; | |
export type Signal<T> = { | |
(): T; | |
set: (newValue: T) => void; | |
map: <U>(fn: (input: T) => U) => Signal<U>; | |
// subscribe: (fn: SignalListener<T>) => () => void; | |
}; | |
const mark = Symbol('isSignal'); | |
const stack: (() => unknown)[] = []; | |
const subscriptions: (null | (() => unknown))[][] = []; | |
export function isSignal<T = unknown>(value: unknown): value is Signal<T> { | |
return typeof value === 'function' && mark in value; | |
} | |
export function computed<T>(fn: () => T): Signal<T> { | |
const me = signal<T>(undefined!); | |
effect(() => me.set(fn())); | |
return me; | |
} | |
export function effect<T>(fn: () => unknown) { | |
stack.unshift(fn); | |
subscriptions.unshift([]); | |
fn(); | |
stack.shift(); | |
const registered = subscriptions.shift()!.filter(Boolean) as (() => unknown)[]; | |
if (!registered.length) throw new Error('Effect registered no signals'); | |
return () => registered.forEach((fn) => fn()); | |
} | |
export function signal<T>(value: T): Signal<T> { | |
const listeners = new Set<SignalListener<T>>(); | |
return Object.assign(get, { | |
[mark]: true, | |
set, | |
map, | |
// subscribe, | |
}); | |
function map<U>(fn: (input: T) => U) { | |
return computed<U>(() => fn(get())); | |
} | |
function get() { | |
if (stack.length) { | |
subscriptions[0].push(subscribe(stack[0])); | |
} | |
return value; | |
} | |
function set(newValue: T) { | |
if (value === newValue) return; | |
value = newValue; | |
listeners.forEach((listener) => listener(newValue)); | |
} | |
function subscribe(fn: SignalListener<T>) { | |
if (listeners.has(fn)) return null; | |
console.log('subscribe'); | |
listeners.add(fn); | |
return () => { | |
console.log('unsubscribe'); | |
listeners.delete(fn); | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment