// Rx 2.0.1 // Html export type Html = false | undefined | null | string | number | Tag | WithState | WithState2 | WithState3 | WithState4 | WithState5 | WithState6 | Array | Rx interface Tag { type: 'Tag' tagName: string attributes: Attributes children?: Array onmount?: (element: Element) => void onunmount?: (element: Element) => void } interface WithState { type: 'WithState' init: A getChildren: (v: Var) => Html } interface WithState2 { type: 'WithState2' init: [A, B] getChildren: (v1: Var, v2: Var) => Html } interface WithState3 { type: 'WithState3' init: [A, B, C] getChildren: (v1: Var, v2: Var, v3: Var) => Html } interface WithState4 { type: 'WithState4' init: [A, B, C, D] getChildren: (v1: Var, v2: Var, v3: Var, v4: Var) => Html } interface WithState5 { type: 'WithState5' init: [A, B, C, D, E] getChildren: (v1: Var, v2: Var, v3: Var, v4: Var, v5: Var) => Html } interface WithState6 { type: 'WithState6' init: [A, B, C, D, E, F] getChildren: (v1: Var, v2: Var, v3: Var, v4: Var, v5: Var, v6: Var) => Html } export interface Attributes { [key: string]: Rx | AttributeValue } type AttributeValue = undefined | null | string | number | boolean | ((event: Event) => void) | ((element: Element) => void) function isHtml(x: any): x is Html { return (typeof x === 'string' || typeof x === 'number' || isTag(x) || isWithState(x) || isWithState2(x) || isWithState3(x) || isWithState4(x) || isRx(x) || Array.isArray(x)) } type ValueOrArray = T | Array> export function h( tagName: string, x?: Attributes | Html, ...children: Array ): Tag { if (x === undefined || x == null || x === false) { return { type: 'Tag', tagName, attributes: {} } } else if (isHtml(x)) { return { type: 'Tag', tagName, attributes: {}, children: [x, ...children], } } else { let attributes = x as Attributes let onmount, onunmount if ('onmount' in attributes) { onmount = attributes['onmount'] as (element: Element) => void delete attributes['onmount'] } if ('onunmount' in attributes) { onunmount = attributes['onunmount'] as (element: Element) => void delete attributes['onunmount'] } return { type: 'Tag', tagName, attributes, children, onmount, onunmount } } } export function withState(init: A, getChildren: (v: Var) => Html): WithState { return { type: 'WithState', init, getChildren } } export function withState2(init: [A, B], getChildren: (v1: Var, v2: Var) => Html): WithState2 { return { type: 'WithState2', init, getChildren } } export function withState3(init: [A, B, C], getChildren: (v1: Var, v2: Var, v3: Var) => Html): WithState3 { return { type: 'WithState3', init, getChildren } } export function withState4(init: [A, B, C, D], getChildren: (v1: Var, v2: Var, v3: Var, v4: Var) => Html): WithState4 { return { type: 'WithState4', init, getChildren } } export function withState5(init: [A, B, C, D, E], getChildren: (v1: Var, v2: Var, v3: Var, v4: Var, v5: Var) => Html): WithState5 { return { type: 'WithState5', init, getChildren } } export function withState6(init: [A, B, C, D, E, F], getChildren: (v1: Var, v2: Var, v3: Var, v4: Var, v5: Var, v6: Var) => Html): WithState6 { return { type: 'WithState6', init, getChildren } } // Rx export type RxAble = Rx | A export class Rx { map(f: (value: A) => B): Rx { return new Map(this, f) } flatMap(f: (value: A) => Rx): Rx { return new FlatMap(this, f) } } export function map2(rx: [Rx, Rx], fn: (a: A, b: B) => C): Rx { return rx[0].flatMap(a => rx[1].map(b => fn(a, b))) } export function map3(rx: [Rx, Rx, Rx], fn: (a: A, b: B, c: C) => D): Rx { return rx[0].flatMap(a => rx[1].flatMap(b => rx[2].map(c => fn(a, b, c)))) } export function map4(rx: [Rx, Rx, Rx, Rx], fn: (a: A, b: B, c: C, d: D) => E): Rx { return rx[0].flatMap(a => rx[1].flatMap(b => rx[2].flatMap(c => rx[3].map(d => fn(a, b, c, d))))) } export function map5(rx: [Rx, Rx, Rx, Rx, Rx], fn: (a: A, b: B, c: C, d: D, e: E) => F): Rx { return rx[0].flatMap(a => rx[1].flatMap(b => rx[2].flatMap(c => rx[3].flatMap(d => rx[4].map(e => fn(a, b, c, d, e)))))) } export function map6(rx: [Rx, Rx, Rx, Rx, Rx, Rx], fn: (a: A, b: B, c: C, d: D, e: E, f: F) => G): Rx { return rx[0].flatMap(a => rx[1].flatMap(b => rx[2].flatMap(c => rx[3].flatMap(d => rx[4].flatMap(e => rx[5].map(f => fn(a, b, c, d, e, f))))))) } class Pure extends Rx { readonly type: 'Pure' readonly value: A constructor(value: A) { super() this.type = 'Pure' this.value = value } } export function pure(value: A): Rx { return new Pure(value) } class Var extends Rx { readonly type: 'Var' readonly id: string readonly update: (f: (value: A) => A) => void constructor(id: string, update: (v: Var) => ((f: ((value: A) => A)) => void)) { super() this.id = id this.type = 'Var' this.update = update(this) } } class Map extends Rx { readonly type: 'Map' readonly rx: Rx readonly f: (value: A) => B constructor(rx: Rx, f: (value: A) => B) { super() this.type = 'Map' this.rx = rx this.f = f } } class FlatMap extends Rx { readonly type: 'FlatMap' readonly rx: Rx readonly f: (value: A) => Rx constructor(rx: Rx, f: (value: A) => Rx) { super() this.type = 'FlatMap' this.rx = rx this.f = f } } export function sequence(xs: Array>): Sequence { return new Sequence(xs) } export function sequence2(xs: Array>): Rx> { return xs.reduce( (acc: Rx>, x: Rx) => acc.flatMap(ys => x.map(y => [y, ...ys])), new Pure([]) ) } class Sequence extends Rx> { readonly type: 'Sequence' readonly xs: Array> constructor(xs: Array>) { super() this.type = 'Sequence' this.xs = xs } } // Mount export function mount(element: HTMLElement, html: Html): Cancelable { const state = new State() let appendRes = appendChild(state, element, html) return appendRes.cancel } interface StateEntry { value: A subscribers: Array<(value: A) => void> } class State { readonly state: {[key: string]: StateEntry} varCounter: bigint constructor() { this.state = {} this.varCounter = BigInt(0) } register(initValue: A) { const v = new Var(this.varCounter.toString(), v => (f => this.update(v, f))) this.varCounter += BigInt(1) this.state[v.id] = { value: initValue, subscribers: [] } return v } unregister(v: Var) { delete this.state[v.id] } get(v: Var) { return this.state[v.id].value } update(v: Var, f: (value: A) => A) { if (v.id in this.state) { const value = f(this.state[v.id].value) this.state[v.id].value = value this.state[v.id].subscribers.forEach(notify => { // Don’t notify if it has been removed from a precedent notifier if (this.state[v.id].subscribers.indexOf(notify) !== -1) { notify(value) } }) } } subscribe(v: Var, notify: (value: A) => void): Cancelable { this.state[v.id].subscribers.push(notify) return () => this.state[v.id].subscribers = this.state[v.id].subscribers.filter(n => n !== notify) } } // Cancelable type Cancelable = () => void const voidCancel = () => {} // Removable type Removable = () => void const voidRemove = () => {} // Rx run function rxRun( state: State, rx: Rx, effect: (value: A) => void ): Cancelable { if (isPure(rx)) { effect(rx.value) return voidCancel } else if (isVar(rx)) { const cancel = state.subscribe(rx, effect) effect(state.get(rx)) return cancel } else if (isMap(rx)) { return rxRun(state, rx.rx, value => effect(rx.f(value))) } else if (isFlatMap(rx)) { let cancel1 = voidCancel const cancel2 = rxRun(state, rx.rx, (value: A) => { cancel1() cancel1 = rxRun(state, rx.f(value), effect) }) return () => { cancel2() cancel1() } } else if (isSequence(rx)) { const cancels = Array(rx.xs.length).fill(voidCancel) const xs = Array(rx.xs.length).fill(undefined) let initEnded = false rx.xs.forEach((rxChild, i) => { cancels[i] = rxRun( state, rxChild, (value: A) => { xs[i] = value if (initEnded) { // @ts-ignore effect(xs) } } ) }) // @ts-ignore effect(xs) initEnded = true return () => cancels.forEach(cancel => cancel()) } else { throw new Error(`Unrecognized rx: ${rx}`) } } function isRx(x: any): x is Rx { return x != null && x.type !== undefined && (x.type === 'Var' || x.type === 'Map' || x.type === 'FlatMap' || x.type === 'Sequence' || x.type === 'Pure') } function isPure(x: any): x is Pure { return x.type === 'Pure' } function isVar(x: any): x is Var { return x.type === 'Var' } function isMap(x: any): x is Map { return x.type === 'Map' } function isFlatMap(x: any): x is FlatMap { return x.type === 'FlatMap' } function isSequence(x: any): x is Sequence { return x.type === 'Sequence' } // Append interface AppendResult { cancel: Cancelable remove: Removable lastAdded?: Node } function appendChild(state: State, element: Element, child: Html, lastAdded?: Node): AppendResult { if (Array.isArray(child)) { let cancels: Array = [] let removes: Array = [] child.forEach((o) => { const appendResult = appendChild(state, element, o, lastAdded) cancels.push(appendResult.cancel) removes.push(appendResult.remove) lastAdded = appendResult.lastAdded }) return { cancel: () => cancels.forEach((o) => o()), remove: () => removes.forEach((o) => o()), lastAdded } } else if (typeof child == 'string') { const node = document.createTextNode(child) appendNode(element, node, lastAdded) return { cancel: voidCancel, remove: () => element.removeChild(node), lastAdded: node } } else if (typeof child == 'number') { return appendChild(state, element, child.toString(), lastAdded) } else if (isTag(child)) { const { tagName, attributes, children, onmount, onunmount } = child const s = isSvg(tagName) const childElement = s ? document.createElementNS('http://www.w3.org/2000/svg', tagName) : document.createElement(tagName) const setAttr = s ? (key: any, value: any) => childElement.setAttribute(key, value) // @ts-ignore : (key: any, value: any) => childElement[key] = value const cancelAttributes = Object.entries(attributes).map(([key, value]) => { if (isRx(value)) { return rxRun(state, value, newValue => setAttribute(setAttr, childElement, key, newValue)) } else { setAttribute(setAttr, childElement, key, value) } }) const appendChildrenRes = appendChild(state, childElement, children) appendNode(element, childElement, lastAdded) if (onmount !== undefined) { // Wait for the element to be on the page window.setTimeout(() => onmount(childElement), 0) } return { cancel: () => { cancelAttributes.forEach(cancel => cancel !== undefined ? cancel() : {}) appendChildrenRes.cancel() if (onunmount !== undefined) { onunmount(childElement) } }, remove: () => element.removeChild(childElement), lastAdded: childElement, } } else if (isWithState(child)) { const { init, getChildren } = child const v = state.register(init) const children = getChildren(v) const appendRes = appendChild(state, element, children) return { cancel: () => { appendRes.cancel() state.unregister(v) }, remove: () => appendRes.remove(), lastAdded: appendRes.lastAdded } } else if (isWithState2(child)) { const { init, getChildren } = child const [ init1, init2 ] = init const v1 = state.register(init1) const v2 = state.register(init2) const children = getChildren(v1, v2) const appendRes = appendChild(state, element, children) return { cancel: () => { appendRes.cancel() state.unregister(v1) state.unregister(v2) }, remove: () => appendRes.remove(), lastAdded: appendRes.lastAdded } } else if (isWithState3(child)) { const { init, getChildren } = child const [ init1, init2, init3 ] = init const v1 = state.register(init1) const v2 = state.register(init2) const v3 = state.register(init3) const children = getChildren(v1, v2, v3) const appendRes = appendChild(state, element, children) return { cancel: () => { appendRes.cancel() state.unregister(v1) state.unregister(v2) state.unregister(v3) }, remove: () => appendRes.remove(), lastAdded: appendRes.lastAdded } } else if (isWithState4(child)) { const { init, getChildren } = child const [ init1, init2, init3, init4 ] = init const v1 = state.register(init1) const v2 = state.register(init2) const v3 = state.register(init3) const v4 = state.register(init4) const children = getChildren(v1, v2, v3, v4) const appendRes = appendChild(state, element, children) return { cancel: () => { appendRes.cancel() state.unregister(v1) state.unregister(v2) state.unregister(v3) state.unregister(v4) }, remove: () => appendRes.remove(), lastAdded: appendRes.lastAdded } } else if (isWithState5(child)) { const { init, getChildren } = child const [ init1, init2, init3, init4, init5 ] = init const v1 = state.register(init1) const v2 = state.register(init2) const v3 = state.register(init3) const v4 = state.register(init4) const v5 = state.register(init5) const children = getChildren(v1, v2, v3, v4, v5) const appendRes = appendChild(state, element, children) return { cancel: () => { appendRes.cancel() state.unregister(v1) state.unregister(v2) state.unregister(v3) state.unregister(v4) state.unregister(v5) }, remove: () => appendRes.remove(), lastAdded: appendRes.lastAdded } } else if (isWithState6(child)) { const { init, getChildren } = child const [ init1, init2, init3, init4, init5, init6 ] = init const v1 = state.register(init1) const v2 = state.register(init2) const v3 = state.register(init3) const v4 = state.register(init4) const v5 = state.register(init5) const v6 = state.register(init6) const children = getChildren(v1, v2, v3, v4, v5, v6) const appendRes = appendChild(state, element, children) return { cancel: () => { appendRes.cancel() state.unregister(v1) state.unregister(v2) state.unregister(v3) state.unregister(v4) state.unregister(v5) state.unregister(v6) }, remove: () => appendRes.remove(), lastAdded: appendRes.lastAdded } } else if (isRx(child)) { const rxBase = document.createTextNode('') appendNode(element, rxBase, lastAdded) let appendRes: AppendResult = { cancel: voidCancel, remove: voidRemove, lastAdded: rxBase } const cancelRx = rxRun(state, child, (value: Html) => { appendRes.cancel() appendRes.remove() appendRes = appendChild(state, element, value, rxBase) }) return { cancel: () => { appendRes.cancel() cancelRx() }, remove: () => { appendRes.remove() element.removeChild(rxBase) }, lastAdded: appendRes.lastAdded, } } else if (!child) { return { cancel: voidCancel, remove: voidRemove, lastAdded } } else { throw new Error(`Unrecognized child: ${child}`) } } const svgElements = ['svg', 'circle', 'polygon', 'line', 'rect', 'ellipse', 'text', 'path'] function isSvg(tagName: string): boolean { return !(svgElements.indexOf(tagName) === -1) } function isTag(x: any): x is Tag { return x != null && x.type === 'Tag' } function isWithState(x: any): x is WithState { return x != null && x.type === 'WithState' } function isWithState2(x: any): x is WithState2 { return x != null && x.type === 'WithState2' } function isWithState3(x: any): x is WithState3 { return x != null && x.type === 'WithState3' } function isWithState4(x: any): x is WithState4 { return x != null && x.type === 'WithState4' } function isWithState5(x: any): x is WithState5 { return x != null && x.type === 'WithState5' } function isWithState6(x: any): x is WithState6 { return x != null && x.type === 'WithState6' } function appendNode(base: Element, node: Node, lastAdded?: Node) { if (lastAdded !== undefined) { base.insertBefore(node, lastAdded.nextSibling) } else { base.append(node) } } function setAttribute(setAttr: (key: any, value: any) => void, element: Element, key: string, attribute: AttributeValue) { if (attribute === undefined) { // Do nothing } else if (attribute === true) { setAttr(key, 'true') } else if (attribute === false) { // @ts-ignore if (key in element) setAttr(key, false) } else if (typeof attribute === 'number') { setAttr(key, attribute.toString()) } else if (typeof attribute === 'string') { setAttr(key, attribute) } else { // @ts-ignore setAttr(key, (event: Event) => attribute(event)) } }