Skip to content

Instantly share code, notes, and snippets.

@hyrious
Created April 14, 2025 05:49
Show Gist options
  • Save hyrious/a052cbb8d1915307aaf757a68209a7cc to your computer and use it in GitHub Desktop.
Save hyrious/a052cbb8d1915307aaf757a68209a7cc to your computer and use it in GitHub Desktop.
Value Enhancer TSX
import { compute, isVal, setValue, subscribe, type ComputeGet, type ReadonlyVal } from 'value-enhancer';
type Value<T> = T | ReadonlyVal<T>;
type ValueOrList<T> = Value<T> | ValueOrList<T>[];
type ValueOrList2<T> = ValueOrList<T> | ValueOrList<ValueOrList<T>>;
type Element = HTMLElement | SVGElement;
type Attributes<T> = Partial<{
[K in keyof T]: T[K] extends Function ? never : T[K] extends object ? Attributes<T[K]> : Value<number | T[K] | undefined | null>;
}>;
type Children = ValueOrList2<Element | string | ObserverNode | undefined>;
function hasVal(value: ValueOrList<unknown>): boolean {
if (isVal(value)) {
return true;
}
if (Array.isArray(value)) {
return value.some(v => hasVal(v));
}
return false;
}
const _noop_ = () => void 0;
const _get_: ComputeGet = (v: unknown) => isVal(v) ? v.value : v;
const _str_ = (x: unknown): string => {
if (typeof x === 'string') return x;
if (x == null) return "";
try {
return JSON.stringify(x, null, 2);
} catch {
// Insane case is not handled: x = { toString: () => { throw x } }
return x + "";
}
};
function resolve<T>(value: ValueOrList<T>, get: ComputeGet, cb: (val: T) => void): void {
if (isVal(value)) {
cb(get(value));
return;
}
if (Array.isArray(value)) {
for (const v of value) {
resolve(v, get, cb);
}
return;
}
cb(value);
}
function getClassName(className: ValueOrList<string | undefined | false> | undefined, get: ComputeGet): string {
let result = '';
resolve(className, get, v => {
if (v) {
if (result.length === 0) {
result = v;
} else {
result += ' ' + v;
}
}
});
return result;
}
function getWindow(node: Node): Window & typeof globalThis {
return node.ownerDocument?.defaultView?.window || window;
}
function isSVGElement(dom: Element): dom is SVGElement {
return dom instanceof SVGElement || dom instanceof getWindow(dom).SVGElement;
}
function setClassName(dom: Element, className: string): void {
if (isSVGElement(dom)) {
dom.setAttribute('class', className);
} else {
dom.className = className;
}
}
function hyphenate(str: string): string {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
function convertCssValue(value: any): string {
return typeof value === 'number' ? value + 'px' : value;
}
function setOrRemoveAttribute(dom: Element, key: string, value: any): void {
if (value == null) {
dom.removeAttribute(key);
} else {
dom.setAttribute(hyphenate(key), String(value));
}
}
class ObserverNode<T extends Element = Element> {
private readonly _computes: ((get: ComputeGet) => void)[] = [];
private readonly _element: T;
constructor(
tag: string,
ns: string | undefined,
className: ValueOrList<string | undefined | false> | undefined,
attributes: Attributes<T>,
children: Children
) {
this._element = (ns ? document.createElementNS(ns, tag) : document.createElement(tag)) as unknown as T;
if (className) {
if (hasVal(className)) {
this._computes.push(get => {
setClassName(this._element, getClassName(className, get));
})
} else {
setClassName(this._element, getClassName(className, _get_));
}
}
for (const [key, value] of Object.entries(attributes)) {
if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
const key = hyphenate(cssKey);
if (isVal(cssValue)) {
this._computes.push(get => {
this._element.style.setProperty(key, convertCssValue(get(cssValue)))
})
} else {
this._element.style.setProperty(key, convertCssValue(cssValue));
}
}
} else if (key === 'tabIndex') {
if (isVal(value)) {
this._computes.push(get => {
this._element.tabIndex = get(value);
})
} else {
this._element.tabIndex = value;
}
} else if (key.startsWith('on')) {
this._element[key] = value;
} else if (key === 'bind:value') {
if (isVal(value)) {
this._computes.push(get => {
const v = get(value);
if (v !== (this._element as any).value) {
(this._element as any).value = v;
}
})
this._element.oninput = (e) => {
setValue(value, (e.target as any).value)
};
} else {
this._element[key] = value;
}
} else {
if (isVal(value)) {
this._computes.push(get => {
setOrRemoveAttribute(this._element, key, get(value));
})
} else {
setOrRemoveAttribute(this._element, key, value);
}
}
}
if (children) {
function getChildren(children: Children, get: ComputeGet): (Element | string)[] {
if (isVal(children)) {
return getChildren(get(children), get);
}
if (Array.isArray(children)) {
return children.flatMap((child: Children) => getChildren(child, get));
}
if (children instanceof ObserverNode) {
children.compute(get);
return [children._element];
}
const str = _str_(children);
return str ? [str] : [];
}
const compute = (get: ComputeGet) => {
this._element.replaceChildren(...getChildren(children, get));
};
this._computes.push(compute);
if (!hasVal(children)) {
compute(_get_);
}
}
}
get element(): T {
return this._element;
}
compute(get: ComputeGet): void {
this._computes.forEach(compute => compute(get));
}
toLiveElement(): LiveElement<T> {
return new LiveElement(this._element, subscribe(compute(get => this.compute(get)), _noop_));
}
}
class LiveElement<T extends Element = Element> {
constructor(readonly element: T, readonly dispose: () => void) { }
}
export function h(
tag: string,
attributes: Attributes<HTMLElement> & { class?: ValueOrList<string | false | undefined> } | null,
...children: any[]
): ObserverNode<Element> {
if (attributes) {
const className = attributes.class;
delete attributes.className;
return new ObserverNode(tag, undefined, className, attributes, children);
} else {
return new ObserverNode(tag, undefined, undefined, {}, children);
}
}
export function render(
node: ObserverNode<Element>,
container: HTMLElement,
): () => void {
const liveElement = node.toLiveElement();
container.replaceChildren(liveElement.element);
return () => {
liveElement.dispose();
container.replaceChildren();
}
}
export namespace h.JSX {
export type Element = ObserverNode<HTMLElement | SVGElement>;
export type ElementClass = ObserverNode<HTMLElement | SVGElement>;
export interface IntrinsicElements { [tag: string]: any; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment