-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
77 lines (65 loc) · 2.39 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import type {Get} from 'type-fest'
import type {SetStateAction} from 'react'
import {nextStateFromAction} from 'misc-hooks'
import type {IBroadcastStream} from 'jdefer'
import {makeBroadcastStream} from 'jdefer'
import {get, set} from 'jmisc'
type FormGet<T, K extends string> = K extends '' ? T : Get<T, K>
export interface FormControl<T> {
// Field can be nested by having dot in between
get value(): T // return all. Same as get('')
get(): T // return all. Same as get('')
get<K extends string>(field: K): FormGet<T, K> // return single field. empty string to get root value, same as get()
set<K extends string>(field: K, action: SetStateAction<FormGet<T, K>>): FormGet<T, K> // set field. Use empty string to set root value
subscribe(cb: (newVal: T) => any): () => void // subscribe to all fields. return unsubscribe function
subscribe<K extends keyof T & string>(field: K, cb: (newVal: FormGet<T, K>) => any): () => void // subscribe to single field. return unsubscribe function
}
export default function makeForm<T>(defaultValue?: T): FormControl<T> {
let value: T = defaultValue
const streams: Partial<Record<keyof T, IBroadcastStream<T[keyof T]>>> = {}
const counts: Partial<Record<keyof T, number>> = {}
return {
get value() {
return value
},
get(field) {
return get(value, field === '' ? undefined : field)
},
set(field, action) {
// always fire even if value does not change
if (field === '') field = undefined
const newVal = nextStateFromAction(action, get(value, field))
value = set(value, field, newVal)
field ??= ''
// notify all children
for (const key of Object.keys(streams).slice().sort().reverse()) {
if (key.startsWith(field)) {
streams[key].next(get(value, key))
}
}
// notify all parents, including root
const parts = field.split('.')
for (let i = parts.length - 1; i >= 0; i--) {
const parentField = parts.slice(0, i).join('.')
streams[parentField]?.next(get(value, parentField || undefined))
}
return newVal
},
subscribe(field, cb) {
if (cb === undefined) {
cb = field
field = '' as keyof T
}
counts[field] = (counts[field] ?? 0) + 1
const unsubscribe = (streams[field] ??= makeBroadcastStream()).listen(cb)
return () => {
const cnt = counts[field] = (counts[field] ?? 0) - 1
if (cnt <= 0) {
delete streams[field]
delete counts[field]
}
unsubscribe()
}
},
}
}