diff options
-rw-r--r-- | src/lib/rx.ts | 138 |
1 files changed, 110 insertions, 28 deletions
diff --git a/src/lib/rx.ts b/src/lib/rx.ts index 3f3b8d9..c46c2f5 100644 --- a/src/lib/rx.ts +++ b/src/lib/rx.ts @@ -1,16 +1,16 @@ -// [1.1.0] 2023-02-13 +// Rx 2.0.0 // Html export type Html - = false - | undefined - | string - | number - | Tag - | WithVar<any> + = false + | undefined + | string + | number + | Tag + | WithState<any> | Array<Html> - | Rx<Html> + | Rx<Html> interface Tag { type: 'Tag' @@ -21,10 +21,10 @@ interface Tag { onunmount?: (element: Element) => void } -interface WithVar<A> { - type: 'WithVar' +interface WithState<A> { + type: 'WithState' init: A - getChildren: (v: Var<A>, update: (f: (value: A) => A) => void) => Html + getChildren: (v: Var<A>) => Html } interface Attributes { @@ -42,8 +42,8 @@ type AttributeValue function isHtml(x: any): x is Html { return (typeof x === 'string' || typeof x === 'number' - || isTag(x) - || isWithVar(x) + || isTag(x) + || isWithState(x) || isRx(x) || Array.isArray(x)) } @@ -90,9 +90,9 @@ export function h( } } -export function withVar<A>(init: A, getChildren: (v: Var<A>, update: (f: (value: A) => A) => void) => Html): WithVar<A> { +export function withState<A>(init: A, getChildren: (v: Var<A>) => Html): WithState<A> { return { - type: 'WithVar', + type: 'WithState', init, getChildren } @@ -112,14 +112,31 @@ export class Rx<A> { } } +class Pure<A> extends Rx<A> { + readonly type: 'Pure' + readonly value: A + + constructor(value: A) { + super() + this.type = 'Pure' + this.value = value + } +} + +export function pure<A>(value: A): Rx<A> { + return new Pure(value) +} + class Var<A> extends Rx<A> { readonly type: 'Var' readonly id: string + readonly update: (f: (value: A) => A) => void - constructor(id: string) { + constructor(id: string, update: (v: Var<A>) => ((f: ((value: A) => A)) => void)) { super() this.id = id this.type = 'Var' + this.update = update(this) } } @@ -149,6 +166,28 @@ class FlatMap<A, B> extends Rx<B> { } } +export function sequence<A>(xs: Array<Rx<A>>): Sequence<A> { + return new Sequence<A>(xs) +} + +export function sequence2<A>(xs: Array<Rx<A>>): Rx<Array<A>> { + return xs.reduce( + (acc: Rx<Array<A>>, x: Rx<A>) => acc.flatMap(ys => x.map(y => [y, ...ys])), + new Pure([]) + ) +} + +class Sequence<A> extends Rx<Array<A>> { + readonly type: 'Sequence' + readonly xs: Array<Rx<A>> + + constructor(xs: Array<Rx<A>>) { + super() + this.type = 'Sequence' + this.xs = xs + } +} + // Mount export function mount(html: Html): Cancelable { @@ -172,7 +211,7 @@ class State { } register<A>(initValue: A) { - const v = new Var(this.varCounter.toString()) + const v = new Var(this.varCounter.toString(), v => (f => this.update(v, f))) this.varCounter += BigInt(1) this.state[v.id] = { value: initValue, @@ -220,8 +259,15 @@ const voidRemove = () => {} // Rx run -function rxRun<A>(state: State, rx: Rx<A>, effect: (value: A) => void): Cancelable { - if (isVar(rx)) { +function rxRun<A>( + state: State, + rx: Rx<A>, + effect: (value: A) => void +): Cancelable { + if (isPure<A>(rx)) { + effect(rx.value) + return voidCancel + } else if (isVar(rx)) { const cancel = state.subscribe(rx, effect) effect(state.get(rx)) return cancel @@ -237,13 +283,39 @@ function rxRun<A>(state: State, rx: Rx<A>, effect: (value: A) => void): Cancelab 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<A>(x: any): x is Rx<A> { - return x !== undefined && x.type !== undefined && (x.type === "Var" || x.type === "Map" || x.type === "FlatMap") + return x !== undefined && x.type !== undefined && (x.type === "Var" || x.type === "Map" || x.type === "FlatMap" || x.type === 'Sequence' || x.type === 'Pure') +} + +function isPure<A>(x: any): x is Pure<A> { + return x.type === 'Pure' } function isVar<A>(x: any): x is Var<A> { @@ -251,11 +323,15 @@ function isVar<A>(x: any): x is Var<A> { } function isMap<A, B>(x: any): x is Map<A, B> { - return x.type === "Map" + return x.type === "Map" } function isFlatMap<A, B>(x: any): x is FlatMap<A, B> { - return x.type === "FlatMap" + return x.type === "FlatMap" +} + +function isSequence<A>(x: any): x is Sequence<A> { + return x.type === "Sequence" } // Append @@ -322,10 +398,10 @@ function appendChild(state: State, element: Element, child: Html, lastAdded?: No remove: () => element.removeChild(childElement), lastAdded: childElement, } - } else if (isWithVar(child)) { + } else if (isWithState(child)) { const { init, getChildren } = child const v = state.register(init) - const children = getChildren(v, f => state.update(v, f)) + const children = getChildren(v) const appendRes = appendChild(state, element, children) return { cancel: () => { @@ -374,8 +450,8 @@ 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 isWithState<A>(x: any): x is WithState<A> { + return x !== undefined && x.type === "WithState" } function appendNode(base: Element, node: Node, lastAdded?: Node) { @@ -387,11 +463,17 @@ function appendNode(base: Element, node: Node, lastAdded?: Node) { } function setAttribute(state: State, element: Element, key: string, attribute: AttributeValue) { - if (attribute === undefined || attribute === false) { + if (attribute === undefined) { // Do nothing } else if (attribute === true) { // @ts-ignore - element[key] = "true" + 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() |