Skip to content

Instantly share code, notes, and snippets.

@ebrensi
Last active April 15, 2022 19:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ebrensi/c636410e07f587b032653523fe6a9a76 to your computer and use it in GitHub Desktop.
Save ebrensi/c636410e07f587b032653523fe6a9a76 to your computer and use it in GitHub Desktop.
JavaScript (TypeScript) Object extended with onChange method
/**
* An Object with an onChange method and debounced callbacks.
* Example:
* ```
* type P = {name: string, age: number, birthday: Date}
* const live_obj = const watch<P>({ name: "Doctor Foo", age: 43 })
* live_obj.onChange("name", (newVal: string) => { console.log("yo! name=${newVal} now!") })
* ```
* !!! In order for types to work correctly, instantiate a LiveParams object using the `watch<P>()`
* factory function rather than `new LiveParams<P>()`.
*/
type CallbackFunction<V> = (newval: V) => void
type Binding<V> = [V, CallbackFunction<V>[]]
type Bindings<T> = {
[K in keyof T]?: [T[K], CallbackFunction<T[K]>[]]
}
/**
* An Object with an onChange method and debounced callbacks.
*/
class LiveParams<Params> {
#bindings: Bindings<Params>
constructor(obj: Params) {
Object.assign(this, obj)
this.#bindings = {}
}
onChange<K extends keyof Params>(
this: Params & LiveParams<Params>,
key: K,
callback: CallbackFunction<Params[K]>,
trigger = true
): void {
type V = Params[K]
let binding = this.#bindings[key]
if (binding) {
const [val, callbacks] = binding
callbacks.push(callback)
if (trigger) callback(val)
} else {
const val = this[key]
delete this[key]
binding = this.#bindings[key] = <Binding<V>>[val, [callback]]
Object.defineProperty(this, key, {
get: () => this.#bindings[key][0],
set: (newValue: V) => {
const binding: Binding<V> = this.#bindings[key]
binding[0] = newValue
debounce(() => {
const callbacks = binding[1]
for (let i=0; i < callbacks.length; i++) callbacks[i](newValue)
}, CALLBACK_DEBOUNCE_DELAY)
},
enumerable: true,
})
if (trigger) callback(val)
}
}
}
const CALLBACK_DEBOUNCE_DELAY = 20
function debounce(func: () => void, timeout: number) {
let timer: number
return () => {
clearTimeout(timer)
timer = setTimeout(() => func(), timeout)
}
}
export type Live<T> = T & LiveParams<T>
export function watch<T>(obj: T) {
return new LiveParams<T>(obj) as Live<T>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment