Skip to content

Commit 1d0a866

Browse files
committed
feat(state): better state functions
1 parent bbd67be commit 1d0a866

File tree

2 files changed

+70
-24
lines changed

2 files changed

+70
-24
lines changed

hyper/domutils.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { HyperNode } from "./node.ts";
22
import { State } from "./state.ts";
3+
import { HTMLInputElement } from "./vendor/dom.slim.ts";
4+
5+
export const bind = <N extends HyperNode<"input">, S extends State>(node: N, state: S): N => {
6+
const oldRef = node.attrs.ref || (() => {});
7+
8+
node.attrs.ref = (el: HTMLInputElement) => {
9+
el.value = state.value;
10+
state.listen(value => (el.value = value));
11+
el.addEventListener("input", e => state.publish((e?.target as unknown as { value: string }).value));
12+
oldRef(el as any);
13+
};
314

4-
export const bind = <N extends HyperNode, State extends State.Simple>(node: N, state: State): N => {
5-
// const oldRef = node.attrs.ref || (() => {});
6-
// // @ts-expect-error do element-aware init and publish
7-
// node.attrs.value = state.init;
8-
// node.attrs.ref = el => {
9-
// oldRef(el);
10-
// el.addEventListener("input", e => state.publish((e?.target as unknown as { value: string }).value));
11-
// };
1215
return node;
1316
};

hyper/state.ts

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,92 @@
1-
export type Subscriber<T> = (val: T) => void;
1+
import { Keyof as K } from "./util.ts";
22

3-
export class ReadonlyRef<T = any> {
3+
type StateEntries<Obj extends Record<string, State>> = { [k in K<Obj>]: { key: k; value: Obj[k]["value"] } }[K<Obj>];
4+
type MapEntries<Rs extends State[]> = { [k in keyof Rs]: { key: k; value: Rs[k]["value"] } }[number];
5+
6+
export type Subscriber<T> = (value: T, unused?: unknown) => void;
7+
export type KeyedSubscriber<K extends { key: unknown; value: unknown }> = (value: K["value"], key: K["key"]) => void;
8+
export type StateType<R extends State> = R extends State<infer U> ? U : never;
9+
10+
export class ReadonlyState<T = any> {
411
protected subscribers: Subscriber<T>[] = [];
512

613
constructor(public value: T) {}
714

8-
listen(f: Subscriber<T>) {
9-
this.subscribers.push(f);
15+
static isState<X>(x: X): x is Extract<X, State | ReadonlyState> {
16+
return x instanceof ReadonlyState;
17+
}
18+
19+
listen(listener: Subscriber<T>) {
20+
this.subscribers.push(listener);
1021
}
1122

1223
map<U>(mapper: (t: T) => U) {
13-
const s = new Ref(mapper(this.value));
24+
const s = new State(mapper(this.value));
1425
// publish mapped changes when value changes
1526
this.listen(value => s.publish(mapper(value)));
1627
// return readonly so mapped state can't be published into
1728
return s.readonly();
1829
}
1930

20-
effect(effector: (t: T) => void) {
21-
// trigger effect when value changes
22-
this.listen(effector);
23-
}
24-
25-
into(state: Ref<T>) {
31+
into(state: State<T>) {
2632
this.listen(value => state.publish(value));
2733
}
2834
}
2935

30-
export class Ref<T = any> extends ReadonlyRef<T> {
36+
export class State<T = any> extends ReadonlyState<T> {
3137
constructor(value: T) {
3238
super(value);
3339
}
3440

35-
static isRef<X>(x: X): x is Extract<X, Ref | ReadonlyRef> {
36-
return x instanceof ReadonlyRef;
41+
/**
42+
* Merge multiple refs into a single Ref
43+
*/
44+
static merge<T>(...states: [State<T>, ...State<T>[]]): MergedState<MapEntries<State<T>[]>>;
45+
46+
static merge<RefMap extends { [k: string]: State }>(refs: RefMap): MergedState<StateEntries<RefMap>>;
47+
48+
static merge<T, RefMap extends { [k: string]: State }>(
49+
...refs: [State<T> | RefMap, ...State<T>[]]
50+
): State<T> | MergedState<MapEntries<State<T>[]>> | MergedState<StateEntries<RefMap>> {
51+
if (State.isState(refs[0])) {
52+
const ref = new MergedState<MapEntries<State<T>[]>>(refs[0].value);
53+
for (let i = 0; i < refs.length; i++) {
54+
const r = refs[i] as State<T>;
55+
r.listen(x => ref.publish(x, i));
56+
}
57+
return ref;
58+
} else {
59+
const ref = new MergedState<StateEntries<RefMap>>(null);
60+
const rs = refs[0];
61+
for (const r in rs) rs[r].listen(c => ref.publish(c, r));
62+
return ref;
63+
}
3764
}
3865

39-
publish(next: T | Promise<T>) {
66+
publish(next: T | Promise<T>, unused?: unknown) {
4067
return Promise.resolve(next).then(val => {
4168
this.value = val;
4269
this.subscribers.forEach(subscriber => subscriber(val));
4370
});
4471
}
4572

4673
readonly() {
47-
return new ReadonlyRef(this.value);
74+
return new ReadonlyState(this.value);
75+
}
76+
}
77+
78+
export class MergedState<T extends { key: string | number; value: unknown }> extends State<T["value"]> {
79+
// @ts-expect-error
80+
protected subscribers: KeyedSubscriber<T>[];
81+
82+
listen(listener: KeyedSubscriber<T>): void {
83+
this.subscribers.push(listener);
84+
}
85+
86+
publish(value: T["value"] | Promise<T["value"]>, key: T["key"]) {
87+
return Promise.resolve(value).then(val => {
88+
this.value = val;
89+
this.subscribers.forEach(subscriber => subscriber(val, key));
90+
});
4891
}
4992
}

0 commit comments

Comments
 (0)