|
type DOMNode = Node; |
|
const element = Symbol('Element'); |
|
export namespace JSX { |
|
export interface Element { |
|
[element]: typeof element |
|
|
|
render(core: {}): DOMNode; |
|
} |
|
|
|
export type IntrinsicElements = { [ElementName in string]: Partial<StandardProps> }; |
|
|
|
export type StandardProps = { |
|
class: string | string[], |
|
key: keyof any, |
|
} & { [Listener in `on:${string}`]: EventHandler<Listener> }; |
|
|
|
export type Props<P extends object> = Partial<StandardProps> & P; |
|
export type PropsTyped<T, P extends object> = Props<P> & { children: T | T[] }; |
|
|
|
type EventHandler<Listener extends `on:${string}`> = Listener extends `on:${infer EventName}` |
|
? EventName extends keyof HTMLElementEventMap ? ((e: HTMLElementEventMap[EventName]) => void) |
|
: ((e: Event) => void) |
|
: never; |
|
|
|
export type Node = <Props extends StandardProps>(props: Props) => JSX.Element; |
|
|
|
type ChildSingular = JSX.Element | Node | Reactor<any> | string | number | null | undefined; |
|
export type Child = ChildSingular | Iterable<ChildSingular>; |
|
export type Children = Child | Child[]; |
|
} |
|
|
|
const reactive = Symbol("reactive"); |
|
|
|
export type Element = JSX.Element; |
|
export type IntrinsicElements = JSX.IntrinsicElements; |
|
|
|
const isEventListener = (str: string): str is `on:${string}` => str.startsWith("on:"); |
|
|
|
interface ReactiveState { |
|
instance: number, |
|
ctx: Core[] |
|
active: Core, |
|
} |
|
|
|
export interface Core { |
|
id: number, |
|
values: Reactor<any>[], |
|
|
|
} |
|
|
|
declare global { |
|
interface Window { |
|
[reactive]: ReactiveState; |
|
} |
|
} |
|
|
|
export class Reactor<T> { |
|
#value: T; |
|
#watches: ((value: T) => void )[] = []; |
|
|
|
constructor(value: T) { |
|
this.#value = value; |
|
} |
|
|
|
get(): T { |
|
return this.#value; |
|
} |
|
|
|
set(value: T): void { |
|
this.#value = value; |
|
this.#notify(); |
|
} |
|
|
|
update(value: (prev: T) => T): void { |
|
this.set(value(this.#value)); |
|
} |
|
|
|
#notify(): void { |
|
this.#watches.forEach(i => i(this.#value)); |
|
} |
|
|
|
onUpdate(dependent: (value: T) => void): Reactor<T> { |
|
this.#watches.push(dependent); |
|
return this; |
|
} |
|
|
|
map<R>(predicate: (i: T) => R): Reactor<R> { |
|
const reactor = new Reactor(predicate(this.get())); |
|
|
|
this.onUpdate(newValue => reactor.set(predicate(newValue))); |
|
|
|
return reactor; |
|
} |
|
} |
|
|
|
export function value<T>(value: T): Reactor<T> { |
|
return new Reactor(value); |
|
} |
|
|
|
function newReactiveState(): ReactiveState { |
|
return Object.defineProperty({ |
|
instance: 0, |
|
ctx: [] as Core[], |
|
}, 'active', { |
|
get(): Core { |
|
return window[reactive].ctx[window[reactive].instance]; |
|
} |
|
}) as ReactiveState; |
|
} |
|
|
|
export function renderer(root: HTMLElement, app: JSX.Element): { unmount: () => void } { |
|
const id = ++(window[reactive] ??= newReactiveState()).instance; |
|
|
|
let core: Core; |
|
window[reactive].ctx.push(core = { |
|
id, |
|
values: [] |
|
}); |
|
|
|
root.append(app.render(core)); |
|
|
|
return { |
|
unmount: () => root |
|
} |
|
} |
|
|
|
type DeReactor<P extends object> = { |
|
[Prop in keyof P]: P[Prop] extends Reactor<infer O> ? O : P[Prop] |
|
}; |
|
|
|
|
|
function* renderChildSingular(child: JSX.Child): Generator<Node> { |
|
const core = window[reactive].active; |
|
|
|
if (!child) |
|
return; |
|
|
|
if (typeof child === 'string' || typeof child == 'number') |
|
yield document.createTextNode(child.toString()); |
|
|
|
else if (Symbol.iterator in child) |
|
for (const subchild of child) |
|
yield* renderChildSingular(subchild); |
|
|
|
else if (child instanceof Node) |
|
yield child; |
|
|
|
else if (child instanceof Reactor) { |
|
const parent = document.createDocumentFragment(); |
|
|
|
renderChild(parent, child); |
|
|
|
yield parent; |
|
} |
|
|
|
else if (element in child) |
|
yield child.render(core); |
|
|
|
else |
|
yield document.createTextNode(String(child)); |
|
|
|
} |
|
|
|
function renderChild(parent: ParentNode, child: JSX.Child) { |
|
if (child instanceof Reactor) { |
|
let children = Array.from(renderChildSingular(child.get())); |
|
|
|
child.onUpdate(value => { |
|
for (const child of children) parent.removeChild(child); |
|
parent.append(...children = Array.from(renderChildSingular(value))); |
|
}); |
|
|
|
parent.append(...children); |
|
} else |
|
parent.append(...renderChildSingular(child)); |
|
} |
|
|
|
export function jsx(el: string | JSX.Node, props: DeReactor<JSX.StandardProps & { children?: JSX.Children }>): JSX.Element { |
|
return { |
|
[element]: element, |
|
render(core: {}) { |
|
if (typeof el == 'string') { |
|
const node = document |
|
.createElement(el); |
|
|
|
for (const [listener, handler] of Object.entries(props)) |
|
if (isEventListener(listener)) |
|
node.addEventListener(listener.slice(3) as unknown as any, handler as unknown as any); |
|
|
|
if (props.children) |
|
for (const child of [props.children].flat()) |
|
renderChild(node, child); |
|
|
|
return node; |
|
} else |
|
return el(props) |
|
.render(core); |
|
} |
|
} |
|
} |
|
|
|
export const jsxs = jsx; |