Skip to content

Commit

Permalink
feat(state): better state functions
Browse files Browse the repository at this point in the history
  • Loading branch information
MKRhere committed Nov 12, 2023
1 parent bbd67be commit 1d0a866
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 24 deletions.
19 changes: 11 additions & 8 deletions hyper/domutils.ts
Original file line number Diff line number Diff line change
@@ -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 = <N extends HyperNode<"input">, 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 = <N extends HyperNode, State extends State.Simple>(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;
};
75 changes: 59 additions & 16 deletions hyper/state.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,92 @@
export type Subscriber<T> = (val: T) => void;
import { Keyof as K } from "./util.ts";

export class ReadonlyRef<T = any> {
type StateEntries<Obj extends Record<string, State>> = { [k in K<Obj>]: { key: k; value: Obj[k]["value"] } }[K<Obj>];
type MapEntries<Rs extends State[]> = { [k in keyof Rs]: { key: k; value: Rs[k]["value"] } }[number];

export type Subscriber<T> = (value: T, unused?: unknown) => void;
export type KeyedSubscriber<K extends { key: unknown; value: unknown }> = (value: K["value"], key: K["key"]) => void;
export type StateType<R extends State> = R extends State<infer U> ? U : never;

export class ReadonlyState<T = any> {
protected subscribers: Subscriber<T>[] = [];

constructor(public value: T) {}

listen(f: Subscriber<T>) {
this.subscribers.push(f);
static isState<X>(x: X): x is Extract<X, State | ReadonlyState> {
return x instanceof ReadonlyState;
}

listen(listener: Subscriber<T>) {
this.subscribers.push(listener);
}

map<U>(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<T>) {
into(state: State<T>) {
this.listen(value => state.publish(value));
}
}

export class Ref<T = any> extends ReadonlyRef<T> {
export class State<T = any> extends ReadonlyState<T> {
constructor(value: T) {
super(value);
}

static isRef<X>(x: X): x is Extract<X, Ref | ReadonlyRef> {
return x instanceof ReadonlyRef;
/**
* Merge multiple refs into a single Ref
*/
static merge<T>(...states: [State<T>, ...State<T>[]]): MergedState<MapEntries<State<T>[]>>;

static merge<RefMap extends { [k: string]: State }>(refs: RefMap): MergedState<StateEntries<RefMap>>;

static merge<T, RefMap extends { [k: string]: State }>(
...refs: [State<T> | RefMap, ...State<T>[]]
): State<T> | MergedState<MapEntries<State<T>[]>> | MergedState<StateEntries<RefMap>> {
if (State.isState(refs[0])) {
const ref = new MergedState<MapEntries<State<T>[]>>(refs[0].value);
for (let i = 0; i < refs.length; i++) {
const r = refs[i] as State<T>;
r.listen(x => ref.publish(x, i));
}
return ref;
} else {
const ref = new MergedState<StateEntries<RefMap>>(null);
const rs = refs[0];
for (const r in rs) rs[r].listen(c => ref.publish(c, r));
return ref;
}
}

publish(next: T | Promise<T>) {
publish(next: T | Promise<T>, unused?: unknown) {
return Promise.resolve(next).then(val => {
this.value = val;
this.subscribers.forEach(subscriber => subscriber(val));
});
}

readonly() {
return new ReadonlyRef(this.value);
return new ReadonlyState(this.value);
}
}

export class MergedState<T extends { key: string | number; value: unknown }> extends State<T["value"]> {
// @ts-expect-error
protected subscribers: KeyedSubscriber<T>[];

listen(listener: KeyedSubscriber<T>): void {
this.subscribers.push(listener);
}

publish(value: T["value"] | Promise<T["value"]>, key: T["key"]) {
return Promise.resolve(value).then(val => {
this.value = val;
this.subscribers.forEach(subscriber => subscriber(val, key));
});
}
}

0 comments on commit 1d0a866

Please sign in to comment.