// Rx 2.0.0
// Html
export type Html
= false
| undefined
| string
| number
| Tag
| WithState
| 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 Attributes {
[key: string]: Rx | AttributeValue
}
type AttributeValue
= undefined
| 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)
|| isRx(x)
|| Array.isArray(x))
}
type ValueOrArray = T | Array>
export function h(
tagName: string,
x?: Attributes | Html,
...children: Array
): Tag {
if (x === undefined || 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
}
}
// 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)
}
}
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(html: Html): Cancelable {
const state = new State()
let appendRes = appendChild(state, document.body, 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) {
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 !== undefined && 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 childElement = document.createElement(tagName)
const cancelAttributes = Object.entries(attributes).map(([key, value]) => {
if (isRx(value)) {
return rxRun(state, value, newValue => setAttribute(state, childElement, key, newValue))
} else {
setAttribute(state, childElement, key, value)
}
})
const appendChildrenRes = appendChild(state, childElement, children)
appendNode(element, childElement, lastAdded)
if (onmount !== undefined) {
onmount(childElement)
}
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 (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 === undefined || child === false) {
return {
cancel: voidCancel,
remove: voidRemove,
lastAdded
}
} else {
throw new Error(`Unrecognized child: ${child}`)
}
}
function isTag(x: any): x is Tag {
return x !== undefined && x.type === "Tag"
}
function isWithState(x: any): x is WithState {
return x !== undefined && x.type === "WithState"
}
function appendNode(base: Element, node: Node, lastAdded?: Node) {
if (lastAdded !== undefined) {
base.insertBefore(node, lastAdded.nextSibling)
} else {
base.append(node)
}
}
function setAttribute(state: State, element: Element, key: string, attribute: AttributeValue) {
if (attribute === undefined) {
// Do nothing
} else if (attribute === true) {
// @ts-ignore
element[key] = 'true'
} else if (attribute === false) {
// @ts-ignore
if (key in element) {
// @ts-ignore
element[key] = false
}
} else if (typeof attribute === "number") {
// @ts-ignore
element[key] = attribute.toString()
} else if (typeof attribute === "string") {
// @ts-ignore
element[key] = attribute
} else {
// @ts-ignore
element[key] = (event: Event) => attribute(event)
}
}