From 1d0a86697db2e86019fa6d1d4c4e25b111571356 Mon Sep 17 00:00:00 2001 From: Muthu Kumar Date: Sun, 12 Nov 2023 22:23:13 +0530 Subject: [PATCH] feat(state): better state functions --- hyper/domutils.ts | 19 +++++++----- hyper/state.ts | 75 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/hyper/domutils.ts b/hyper/domutils.ts index 7fe5b73..7a86d32 100644 --- a/hyper/domutils.ts +++ b/hyper/domutils.ts @@ -1,13 +1,16 @@ import type { HyperNode } from "./node.ts"; import { State } from "./state.ts"; +import { HTMLInputElement } from "./vendor/dom.slim.ts"; + +export const bind = , S extends State>(node: N, state: S): N => { + const oldRef = node.attrs.ref || (() => {}); + + node.attrs.ref = (el: HTMLInputElement) => { + el.value = state.value; + state.listen(value => (el.value = value)); + el.addEventListener("input", e => state.publish((e?.target as unknown as { value: string }).value)); + oldRef(el as any); + }; -export const bind = (node: N, state: State): N => { - // const oldRef = node.attrs.ref || (() => {}); - // // @ts-expect-error do element-aware init and publish - // node.attrs.value = state.init; - // node.attrs.ref = el => { - // oldRef(el); - // el.addEventListener("input", e => state.publish((e?.target as unknown as { value: string }).value)); - // }; return node; }; diff --git a/hyper/state.ts b/hyper/state.ts index 5c58210..21fe183 100644 --- a/hyper/state.ts +++ b/hyper/state.ts @@ -1,42 +1,69 @@ -export type Subscriber = (val: T) => void; +import { Keyof as K } from "./util.ts"; -export class ReadonlyRef { +type StateEntries> = { [k in K]: { key: k; value: Obj[k]["value"] } }[K]; +type MapEntries = { [k in keyof Rs]: { key: k; value: Rs[k]["value"] } }[number]; + +export type Subscriber = (value: T, unused?: unknown) => void; +export type KeyedSubscriber = (value: K["value"], key: K["key"]) => void; +export type StateType = R extends State ? U : never; + +export class ReadonlyState { protected subscribers: Subscriber[] = []; constructor(public value: T) {} - listen(f: Subscriber) { - this.subscribers.push(f); + static isState(x: X): x is Extract { + return x instanceof ReadonlyState; + } + + listen(listener: Subscriber) { + this.subscribers.push(listener); } map(mapper: (t: T) => U) { - const s = new Ref(mapper(this.value)); + const s = new State(mapper(this.value)); // publish mapped changes when value changes this.listen(value => s.publish(mapper(value))); // return readonly so mapped state can't be published into return s.readonly(); } - effect(effector: (t: T) => void) { - // trigger effect when value changes - this.listen(effector); - } - - into(state: Ref) { + into(state: State) { this.listen(value => state.publish(value)); } } -export class Ref extends ReadonlyRef { +export class State extends ReadonlyState { constructor(value: T) { super(value); } - static isRef(x: X): x is Extract { - return x instanceof ReadonlyRef; + /** + * Merge multiple refs into a single Ref + */ + static merge(...states: [State, ...State[]]): MergedState[]>>; + + static merge(refs: RefMap): MergedState>; + + static merge( + ...refs: [State | RefMap, ...State[]] + ): State | MergedState[]>> | MergedState> { + if (State.isState(refs[0])) { + const ref = new MergedState[]>>(refs[0].value); + for (let i = 0; i < refs.length; i++) { + const r = refs[i] as State; + r.listen(x => ref.publish(x, i)); + } + return ref; + } else { + const ref = new MergedState>(null); + const rs = refs[0]; + for (const r in rs) rs[r].listen(c => ref.publish(c, r)); + return ref; + } } - publish(next: T | Promise) { + publish(next: T | Promise, unused?: unknown) { return Promise.resolve(next).then(val => { this.value = val; this.subscribers.forEach(subscriber => subscriber(val)); @@ -44,6 +71,22 @@ export class Ref extends ReadonlyRef { } readonly() { - return new ReadonlyRef(this.value); + return new ReadonlyState(this.value); + } +} + +export class MergedState extends State { + // @ts-expect-error + protected subscribers: KeyedSubscriber[]; + + listen(listener: KeyedSubscriber): void { + this.subscribers.push(listener); + } + + publish(value: T["value"] | Promise, key: T["key"]) { + return Promise.resolve(value).then(val => { + this.value = val; + this.subscribers.forEach(subscriber => subscriber(val, key)); + }); } }