Skip to content

Instantly share code, notes, and snippets.

@amatiasq
Last active May 15, 2025 16:51
Show Gist options
  • Save amatiasq/4832355e944212713a4d56777fd77bcd to your computer and use it in GitHub Desktop.
Save amatiasq/4832355e944212713a4d56777fd77bcd to your computer and use it in GitHub Desktop.
import { AmqElement } from './AmqElement';
import { signal } from './signal';
export class AmqButton extends AmqElement {
static {
customElements.define('amq-button', this);
}
render() {
const type = signal<string>('buttonbutton');
const derived = type.map(x => x.slice(0, x.length / 2));
setTimeout(() => {
type.set('submitsubmit');
}, 1000);
return this.html`
<button
type=${derived}
class=${['a', undefined].filter(Boolean).join(' ')}
click=${this.onClick.bind(this)}
>
<slot></slot>
</button>
`;
}
onClick(event: MouseEvent) {
console.log('Button clicked', event);
}
}
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;
}
}
<!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>
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