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
78
79
80
81
82
83
84
85
86
87
88
89
|
export const snakeify = (str: string) => str
.replace(/([a-z])([A-Z])/g, "$1_$2")
.replaceAll("-", "_")
.toLowerCase()
export const kebabify = (str: string) => str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replaceAll("_", "-")
.toLowerCase()
export interface Subscribable<T = unknown> {
subscribe(callback: (value: T) => void): () => void
get(): T
[key: string]: any
}
export interface Connectable {
connect(signal: string, callback: (...args: any[]) => unknown): number
disconnect(id: number): void
[key: string]: any
}
export class Binding<Value> {
private transformFn = (v: any) => v
#emitter: Subscribable<Value> | Connectable
#prop?: string
static bind<
T extends Connectable,
P extends keyof T,
>(object: T, property: P): Binding<T[P]>
static bind<T>(object: Subscribable<T>): Binding<T>
static bind(emitter: Connectable | Subscribable, prop?: string) {
return new Binding(emitter, prop)
}
private constructor(emitter: Connectable | Subscribable<Value>, prop?: string) {
this.#emitter = emitter
this.#prop = prop && kebabify(prop)
}
toString() {
return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>`
}
as<T>(fn: (v: Value) => T): Binding<T> {
const bind = new Binding(this.#emitter, this.#prop)
bind.transformFn = (v: Value) => fn(this.transformFn(v))
return bind as unknown as Binding<T>
}
get(): Value {
if (typeof this.#emitter.get === "function")
return this.transformFn(this.#emitter.get())
if (typeof this.#prop === "string") {
const getter = `get_${snakeify(this.#prop)}`
if (typeof this.#emitter[getter] === "function")
return this.transformFn(this.#emitter[getter]())
return this.transformFn(this.#emitter[this.#prop])
}
throw Error("can not get value of binding")
}
subscribe(callback: (value: Value) => void): () => void {
if (typeof this.#emitter.subscribe === "function") {
return this.#emitter.subscribe(() => {
callback(this.get())
})
} else if (typeof this.#emitter.connect === "function") {
const signal = `notify::${this.#prop}`
const id = this.#emitter.connect(signal, () => {
callback(this.get())
})
return () => {
(this.#emitter.disconnect as Connectable["disconnect"])(id)
}
}
throw Error(`${this.#emitter} is not bindable`)
}
}
export const { bind } = Binding
export default Binding
|