aboutsummaryrefslogtreecommitdiff
path: root/src/view/client/lib/rx.ts
diff options
context:
space:
mode:
authorJoris2023-09-17 12:23:47 +0200
committerJoris2023-09-17 12:23:47 +0200
commit1ebc55c72a1a17293bbf4ad86e0177a10a794750 (patch)
tree5fce0ea3a011ccbae85b0d3927f8ac33099585fb /src/view/client/lib/rx.ts
parentc236facb4d4c277773c83f1a4ee85b48833d7e67 (diff)
Make app packageable
Diffstat (limited to 'src/view/client/lib/rx.ts')
-rw-r--r--src/view/client/lib/rx.ts404
1 files changed, 0 insertions, 404 deletions
diff --git a/src/view/client/lib/rx.ts b/src/view/client/lib/rx.ts
deleted file mode 100644
index bf01b6d..0000000
--- a/src/view/client/lib/rx.ts
+++ /dev/null
@@ -1,404 +0,0 @@
-// [1.0.1] 2023-02-13
-
-// Html
-
-export type Html
- = false
- | undefined
- | string
- | number
- | Tag
- | WithVar<any>
- | Array<Html>
- | Rx<Html>
-
-interface Tag {
- type: 'Tag'
- tagName: string
- attributes: Attributes
- children?: Array<Html>
- onmount?: (element: Element) => void
- onunmount?: (element: Element) => void
-}
-
-interface WithVar<A> {
- type: 'WithVar'
- init: A
- getChildren: (v: Var<A>, update: (f: (value: A) => A) => void) => Html
-}
-
-interface Attributes {
- [key: string]: Rx<AttributeValue> | AttributeValue
-}
-
-type AttributeValue
- = 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)
- || isWithVar(x)
- || isRx(x)
- || Array.isArray(x))
-}
-
-type ValueOrArray<T> = T | Array<ValueOrArray<T>>
-
-export function h(
- tagName: string,
- x?: Attributes | Html,
- ...children: Array<Html>
-): 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 withVar<A>(init: A, getChildren: (v: Var<A>, update: (f: (value: A) => A) => void) => Html): WithVar<A> {
- return {
- type: 'WithVar',
- init,
- getChildren
- }
-}
-
-// Rx
-
-export type RxAble<A> = Rx<A> | A
-
-export class Rx<A> {
- map<B>(f: (value: A) => B): Rx<B> {
- return new Map<A, B>(this, f)
- }
-
- flatMap<B>(f: (value: A) => Rx<B>): Rx<B> {
- return new FlatMap<A, B>(this, f)
- }
-}
-
-class Var<A> extends Rx<A> {
- readonly type: 'Var'
- readonly id: string
-
- constructor(id: string) {
- super()
- this.id = id
- this.type = 'Var'
- }
-}
-
-class Map<A, B> extends Rx<B> {
- readonly type: 'Map'
- readonly rx: Rx<A>
- readonly f: (value: A) => B
-
- constructor(rx: Rx<A>, f: (value: A) => B) {
- super()
- this.type = 'Map'
- this.rx = rx
- this.f = f
- }
-}
-
-class FlatMap<A, B> extends Rx<B> {
- readonly type: 'FlatMap'
- readonly rx: Rx<A>
- readonly f: (value: A) => Rx<B>
-
- constructor(rx: Rx<A>, f: (value: A) => Rx<B>) {
- super()
- this.type = 'FlatMap'
- this.rx = rx
- this.f = f
- }
-}
-
-// Mount
-
-export function mount(html: Html): Cancelable {
- const state = new State()
- let appendRes = appendChild(state, document.body, html)
- return appendRes.cancel
-}
-
-interface StateEntry<A> {
- value: A
- subscribers: Array<(value: A) => void>
-}
-
-class State {
- readonly state: {[key: string]: StateEntry<any>}
- varCounter: bigint
-
- constructor() {
- this.state = {}
- this.varCounter = BigInt(0)
- }
-
- register<A>(initValue: A) {
- const v = new Var(this.varCounter.toString())
- this.varCounter += BigInt(1)
- this.state[v.id] = {
- value: initValue,
- subscribers: []
- }
- return v
- }
-
- unregister<A>(v: Var<A>) {
- delete this.state[v.id]
- }
-
- get<A>(v: Var<A>) {
- return this.state[v.id].value
- }
-
- update<A>(v: Var<A>, 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<A>(v: Var<A>, 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<A>(state: State, rx: Rx<A>, effect: (value: A) => void): Cancelable {
- if (isVar(rx)) {
- const cancel = state.subscribe(rx, effect)
- effect(state.get(rx))
- return cancel
- } else if (isMap<A, any>(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 {
- throw new Error(`Unrecognized rx: ${rx}`)
- }
-}
-
-function isRx<A>(x: any): x is Rx<A> {
- return x !== undefined && x.type !== undefined && (x.type === "Var" || x.type === "Map" || x.type === "FlatMap")
-}
-
-function isVar<A>(x: any): x is Var<A> {
- return x.type === "Var"
-}
-
-function isMap<A, B>(x: any): x is Map<A, B> {
- return x.type === "Map"
-}
-
-function isFlatMap<A, B>(x: any): x is FlatMap<A, B> {
- return x.type === "FlatMap"
-}
-
-// 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<Cancelable> = []
- let removes: Array<Removable> = []
- 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<AttributeValue>(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 (isWithVar(child)) {
- const { init, getChildren } = child
- const v = state.register(init)
- const children = getChildren(v, f => state.update(v, f))
- 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<A>(x: any): x is Tag {
- return x !== undefined && x.type === "Tag"
-}
-
-function isWithVar<A>(x: any): x is WithVar<A> {
- return x !== undefined && x.type === "WithVar"
-}
-
-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 (typeof attribute == "boolean") {
- if (attribute) {
- // @ts-ignore
- element[key] = "true"
- }
- } 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)
- }
-}