// 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))
}
}