{"id":9410,"date":"2025-04-03T08:22:40","date_gmt":"2025-04-03T07:22:40","guid":{"rendered":"https:\/\/solidt.eu\/site\/?p=9410"},"modified":"2025-05-19T16:17:30","modified_gmt":"2025-05-19T15:17:30","slug":"typescript-mutableobservable-and-mutableobservablecollection","status":"publish","type":"post","link":"https:\/\/solidt.eu\/site\/typescript-mutableobservable-and-mutableobservablecollection\/","title":{"rendered":"Typescript MutableObservable and MutableObservableCollection"},"content":{"rendered":"\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import { BehaviorSubject, Observable } from \"rxjs\";\n\n\/\/ basically a wrapper for BehaviorSubject\nexport class MutableObservable&lt;T> {\n    private Subject: BehaviorSubject&lt;T>;\n    constructor(value: T) {\n        this.Subject = new BehaviorSubject&lt;T>(value);\n\n        this.getValue = this.getValue.bind(this);\n        this.setValue = this.setValue.bind(this);\n        this.asObservable = this.asObservable.bind(this);\n    }\n\n    getValue(): T {\n        return this.Subject.getValue();\n    }\n\n    setValue(value: T): void {\n        return this.Subject.next(value);\n    }\n\n    asObservable() {\n        return this.Subject.asObservable();\n    }\n}\n\nexport class MutableObservableCollection&lt;T extends { key: string | number }> {\n    private subject: BehaviorSubject&lt;T[]>;\n\n    constructor(initialItems: T[] = []) {\n        this.subject = new BehaviorSubject&lt;T[]>(initialItems);\n    }\n\n    getItems(): T[] {\n        return this.subject.getValue();\n    }\n\n    asObservable(): Observable&lt;T[]> {\n        return this.subject.asObservable();\n    }\n\n    setItems(newItems: T[]): void {\n        this.subject.next(newItems);\n    }\n\n    updateItem(updatedItem: T): void {\n        this.subject.next(\n            this.getItems().map(item => item.key === updatedItem.key ? updatedItem : item)\n        );\n    }\n\n    addItem(newItem: T): void {\n        this.subject.next([...this.getItems(), newItem]);\n    }\n\n    removeItem(key: string | number): void {\n        this.subject.next(this.getItems().filter(item => item.key !== key));\n    }\n\n    moveItemUp(key: string | number): void {\n        const items = this.getItems();\n        const index = items.findIndex(item => item.key === key);\n\n        \/\/ Als het item niet bestaat of al bovenaan staat, doe niets\n        if (index &lt;= 0) return;\n\n        \/\/ Verwissel het item met het item ervoor\n        const newItems = [...items];\n        [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];\n\n        this.subject.next(newItems);\n    }\n\n    moveItemDown(key: string | number): void {\n        const items = this.getItems();\n        const index = items.findIndex(item => item.key === key);\n\n        \/\/ Als het item niet bestaat of al onderaan staat, doe niets\n        if (index === -1 || index >= items.length - 1) return;\n\n        \/\/ Verwissel het item met het item erna\n        const newItems = [...items];\n        [newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]];\n\n        this.subject.next(newItems);\n    }\n\n    getChangedItems(originalItems: T[]): T[] {\n        return this.getItems().filter(item => {\n            const original = originalItems.find(o => o.key === item.key);\n            return original &amp;&amp; JSON.stringify(original) !== JSON.stringify(item);\n        });\n    }\n}\n<\/pre><\/div>\n\n\n\n<p>React<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"typescript\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">export function useMutableObservable&lt;T>(subjectInput: MutableObservable&lt;T>): [T, (value: T) => any, (changes: Partial&lt;T>) => any] {\n    const state = useMemo(() => ({\n        observable: subjectInput.asObservable(),\n        setValue: subjectInput.setValue,\n        getValue: subjectInput.getValue,\n        updateValue: (_changes: Partial&lt;T>) => 0\n    }), [subjectInput]);\n\n    const [result, setResult] = useState&lt;[T, (value: T) => any, (changes: Partial&lt;T>) => any]>([state.getValue(), state.setValue, state.updateValue]);\n    const resultRef = useRef(state.getValue());\n    resultRef.current = state.getValue();\n    useEffect(() => {\n        if (!state?.observable) return;\n        const subscription = state.observable.subscribe((x: any) => {\n            if (x === resultRef.current) return;\n            setResult([x, state.setValue, state.updateValue]);\n        });\n        return () => subscription.unsubscribe();\n    }, [state]);\n\n    return result;\n}\n\nexport function useObservable&lt;T>(subject: Observable&lt;T | undefined>, initialValue?: T): T | undefined {\n    const [data, setData] = useState&lt;T | undefined>(initialValue);\n    useEffect(() => {\n        if (!subject) { return; }\n        const subscription = subject.subscribe((x: any) => {\n            setData(x);\n        });\n        return () => subscription.unsubscribe();\n    }, [subject]);\n    return data;\n}<\/pre><\/div>\n\n\n\n<p>WIP<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"typescript\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">iimport { Observable, Subject, Subscription } from \"rxjs\";\nimport { AdvancedCollection } from \".\/MutableObservable\";\n\nexport interface ISubscribable&lt;T> {\n    subscribe(\n        next?: ((value: T) => void) | null,\n        error?: ((error: any) => void) | null,\n        complete?: (() => void) | null\n    ): Subscription;\n}\n\nexport interface ObservableValue&lt;T> extends ISubscribable&lt;T> {\n    getValue(): T;\n    setValue(fn: (old: T) => T): void;\n    setPartial(v: Partial&lt;T>): void;\n    asObservable(): Observable&lt;T>;\n}\n\n\/**\n * Maakt een gecontroleerde observable waarde met toegang tot de huidige waarde.\n * Emit geen waarde automatisch bij constructie (zoals BehaviorSubject zou doen),\n * maar houdt wel de laatste waarde bij.\n *\/\nexport function makeObservableValue&lt;T>(initialValue: T): ObservableValue&lt;T> {\n    let currentValue = initialValue;\n    const subject = new Subject&lt;T>();\n    const o = subject.asObservable();\n    const subscribe = o.subscribe.bind(o);\n    const setValue = (fn: (old: T) => T) => {\n        const newValue = fn(currentValue);\n        currentValue = newValue;\n        subject.next(newValue);\n    };\n    return {\n        getValue: () => currentValue,\n        setPartial: (v: Partial&lt;T>) => setValue(c => ({ ...c, ...v })),\n        setValue: setValue,\n        asObservable: () => subject.asObservable(),\n        subscribe: subscribe\n    };\n}\n\nexport function makeChildObservableValue&lt;T, K extends keyof T>(parent: ObservableValue&lt;T>, key: K): ObservableValue&lt;T[K]> {\n    return makeChildObservableValueFlexible(parent, (p) => p[key], (p, v) => p.setPartial({ [key]: v } as any));\n}\n\nexport function makeChildObservableValueFlexible&lt;T, C>(parent: ObservableValue&lt;T>, getValueFromParent: (parent: T) => C, setInParent: (parent: ObservableValue&lt;T>, childValue: C) => any): ObservableValue&lt;C> {\n    const child = makeObservableValue(getValueFromParent(parent.getValue()));\n\n    const originalSetValue = child.setValue;\n    child.setValue = (fn: (old: C) => C) => {\n        originalSetValue(fn);\n        const newVal = child.getValue();\n        setInParent(parent, newVal);\n    };\n    return child;\n}\n\nexport function makeChildAdvancedCollection&lt;TParent, TItem>(parent: ObservableValue&lt;TParent>, getArray: (p: TParent) => TItem[], setArray: (p: ObservableValue&lt;TParent>, newArray: TItem[]) => void, keyFn: (item: TItem) => string | number): AdvancedCollection&lt;TItem> {\n    const initialItems = getArray(parent.getValue());\n    const ac = new AdvancedCollection&lt;TItem>(keyFn, initialItems);\n\n    \/\/ Override setItems zodat het ook de parent update\n    const originalSetItems = ac.setItems.bind(ac);\n    ac.setItems = (newItems: TItem[]) => {\n        originalSetItems(newItems);\n        setArray(parent, newItems);\n    };\n\n    return ac;\n}\n\n\nexport interface IAdvancedCollection&lt;T> {\n    getItems(): T[];\n    asObservable(): Observable&lt;T[]>;\n    asObservableValue(): ObservableValue&lt;T[]>;\n    setItems(newItems: T[]): void;\n    updateItem(updatedItem: T): void;\n    addItem(newItem: T): void;\n    addItems(newItems: T[]): void;\n    removeItem(key: string | number): void;\n    getItem(key: string | number): T | undefined;\n    moveItemUp(key: string | number): void;\n    moveItemDown(key: string | number): void;\n    getChangedItems(originalItems: T[]): T[];\n}\n\nexport class AdvancedCollection&lt;T> implements IAdvancedCollection&lt;T> {\n    private ov: ObservableValue&lt;T[]>;\n    private keyFn: (o: T) => string | number;\n\n    constructor(keyFn: (o: T) => string | number, initialItems: T[] = []) {\n        this.ov = makeObservableValue(initialItems);\n        this.keyFn = keyFn;\n    }\n\n    getItems(): T[] {\n        return [...this.ov.getValue()];\n    }\n\n    asObservable(): Observable&lt;T[]> {\n        return this.ov.asObservable();\n    }\n\n    asObservableValue(): ObservableValue&lt;T[]> {\n        return this.ov;\n    }\n\n    setItems(newItems: T[]): void {\n        this.ov.setValue(_ => newItems);\n    }\n\n    updateItem(updatedItem: T): void {\n        this.setItems(this.getItems().map(item => this.keyFn(item) === this.keyFn(updatedItem) ? updatedItem : item));\n    }\n\n    addItem(newItem: T): void {\n        this.setItems([...this.getItems(), newItem]);\n    }\n\n    addItems(newItems: T[]): void {\n        this.setItems([...this.getItems(), ...newItems]);\n    }\n\n    removeItem(key: string | number): void {\n        const newItems = this.getItems().filter(item => this.keyFn(item) !== key);\n        this.setItems(newItems);\n    }\n\n    getItem(key: string | number): T | undefined {\n        return this.getItems().find(item => this.keyFn(item) === key);\n    }\n\n    moveItemUp(key: string | number): void {\n        const items = this.getItems();\n        const index = items.findIndex(item => this.keyFn(item) === key);\n\n        \/\/ Als het item niet bestaat of al bovenaan staat, doe niets\n        if (index &lt;= 0) return;\n\n        \/\/ Verwissel het item met het item ervoor\n        const newItems = [...items];\n        [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];\n\n        this.setItems(newItems);\n    }\n\n    moveItemDown(key: string | number): void {\n        const items = this.getItems();\n        const index = items.findIndex(item => this.keyFn(item) === key);\n\n        \/\/ Als het item niet bestaat of al onderaan staat, doe niets\n        if (index === -1 || index >= items.length - 1) return;\n\n        \/\/ Verwissel het item met het item erna\n        const newItems = [...items];\n        [newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]];\n\n        this.setItems(newItems);\n    }\n\n    getChangedItems(originalItems: T[]): T[] {\n        return this.getItems().filter(item => {\n            const original = originalItems.find(o => this.keyFn(o) === this.keyFn(item));\n            return original &amp;&amp; JSON.stringify(original) !== JSON.stringify(item);\n        });\n    }\n}\n\n\nexport class MutableObservableCollection&lt;T extends { key: string | number }> extends AdvancedCollection&lt;T> {\n    constructor(initialItems: T[] = []) {\n        super((o: T) => o.key, initialItems);\n    }\n}\n\n\n\/\/ react hooks:\n\nexport function useObservableValue&lt;T>(subject: ObservableValue&lt;T>): T {\n    const [, forceUpdate] = useReducer(x => x + 1, 0);\n    useEffect(() => {\n        if (!subject) { return; }\n        const subscription = subject.subscribe(forceUpdate);\n        return () => subscription.unsubscribe();\n    }, [subject]);\n    return subject.getValue();\n}\n\nexport function useObservableValueDebug&lt;T>(subject: ObservableValue&lt;T>, key: string): T {\n    const [, forceUpdate] = useReducer(x => x + 1, 0);\n    useEffect(() => {\n        if (!subject) { return; }\n        console.log('observable-value subscribe', key);\n        const subscription = subject.subscribe((x: T) => {\n            console.log('observable-value changed', key, x);\n            forceUpdate();\n        });\n        return () => {\n            console.log('observable-value unsubscribe', key);\n            subscription.unsubscribe();\n        };\n    }, [subject, key]);\n    console.log('observable-value render', key, subject.getValue());\n    return subject.getValue();\n}\n<\/pre><\/div>\n\n\n\n<p>Example<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">function DashboardFieldsTest(props: { dashboard: ObservableValue&lt;IDashboardDetailData> }) {\n    const { dashboard } = props;\n\n    const nameField = makeChildObservableValueFlexible(dashboard, d => d.name, (d, value) => d.setPartial({ name: value }));\n    const titleField = makeChildObservableValueFlexible(dashboard, d => d.title, (d, value) => d.setPartial({ title: value }));\n    const arrayField = makeChildAdvancedCollection(dashboard, d => d.filters, (d, value) => d.setPartial({ filters: value }), (x) => x.field);\n    return &lt;>\n        &lt;ObservableEditor editorName=\"TextEditor\" label={translate(\"DashboardDetailView.name\")} field={nameField} \/>\n        {\/* &lt;ObservableEditor editorName=\"TextEditor\" label={translate(\"DashboardDetailView.name\")} field={nameField} \/> *\/}\n        &lt;ArrayEditor field={arrayField} \/>\n        &lt;Button onClick={() => {\n            arrayField.addItem({ field: nanoid(), operator: ListViewFilterOperator.Between, values: [\"\"] });\n            console.log(\"dashboard.getValue().filters\", dashboard.getValue().filters);\n        }}>\n            Add item&lt;\/Button>\n        &lt;ObservableEditor editorName=\"TextEditor\" label={translate(\"DashboardDetailView.title\")} field={titleField} \/>\n    &lt;\/>;\n}\n\nfunction ArrayEditor(props: { field: IAdvancedCollection&lt;IWidgetFilterArg> }) {\n    const value = useObservableValue(props.field.asObservableValue());\n    return &lt;>{value.map(x => &lt;div key={x.field}>{x.field}  &lt;Button onClick={() => props.field.removeItem(x.field)}>delete&lt;\/Button>&lt;\/div>)}&lt;\/>\n\n}<\/pre><\/div>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"typescript\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import { nanoid } from \"nanoid\";\nimport { Observable, Subject, Subscription } from \"rxjs\";\nimport { AdvancedCollection } from \".\/MutableObservable\";\nimport { ensureTypedArray } from \".\/tshelper\";\n\nexport interface ISubscribable&lt;T> {\n    subscribe(\n        next?: ((value: T) => void) | null,\n        error?: ((error: any) => void) | null,\n        complete?: (() => void) | null\n    ): Subscription;\n}\n\nexport interface ObservableValue&lt;T> extends ISubscribable&lt;T> {\n    getValue(): T;\n    setValue(fn: (old: T) => T): void;\n    setPartial(v: Partial&lt;T>): void;\n    asObservable(): Observable&lt;T>;\n}\n\n\/**\n * Maakt een gecontroleerde observable waarde met toegang tot de huidige waarde.\n * Emit geen waarde automatisch bij constructie (zoals BehaviorSubject zou doen),\n * maar houdt wel de laatste waarde bij.\n *\/\nexport function makeObservableValue&lt;T>(initialValue: T): ObservableValue&lt;T> {\n    let currentValue = initialValue;\n    const subject = new Subject&lt;T>();\n    const o = subject.asObservable();\n    const subscribe = o.subscribe.bind(o);\n    const setValue = (fn: (old: T) => T) => {\n        const newValue = fn(currentValue);\n        currentValue = newValue;\n        subject.next(newValue);\n    };\n    return {\n        getValue: () => currentValue,\n        setPartial: (v: Partial&lt;T>) => setValue(c => ({ ...c, ...v })),\n        setValue: setValue,\n        asObservable: () => subject.asObservable(),\n        subscribe: subscribe\n    };\n}\n\nexport function makeChildObservableValue&lt;T, K extends keyof T>(parent: ObservableValue&lt;T>, key: K): ObservableValue&lt;T[K]> {\n    return makeChildObservableValueFlexible(parent, (p) => p[key], (p, v) => p.setPartial({ [key]: v } as any));\n}\n\nexport function makeChildObservableValueFlexible&lt;T, C>(parent: ObservableValue&lt;T>, getValueFromParent: (parent: T) => C, setInParent: (parent: ObservableValue&lt;T>, childValue: C) => any): ObservableValue&lt;C> {\n    const child = makeObservableValue(getValueFromParent(parent.getValue()));\n\n    const originalSetValue = child.setValue;\n    child.setValue = (fn: (old: C) => C) => {\n        originalSetValue(fn);\n        const newVal = child.getValue();\n        setInParent(parent, newVal);\n    };\n    child.setPartial = (v: Partial&lt;C>) => child.setValue(x => ({ ...x, ...v }));\n    return child;\n}\n\nexport function makeChildAdvancedCollection&lt;TParent, TItem>(parent: ObservableValue&lt;TParent>, getArray: (p: TParent) => TItem[], setArray: (p: ObservableValue&lt;TParent>, newArray: TItem[]) => void, keyFn: (item: TItem) => string | number): AdvancedCollection&lt;TItem> {\n    const initialItems = getArray(parent.getValue());\n    const ac = new AdvancedCollection&lt;TItem>(keyFn, initialItems);\n\n    \/\/ Override setItems zodat het ook de parent update\n    const originalSetItems = ac.setItems.bind(ac);\n    ac.setItems = (newItems: TItem[]) => {\n        originalSetItems(newItems);\n        setArray(parent, newItems);\n    };\n\n    return ac;\n}\n\nexport function addKeys&lt;T>(arr: T[]): (T &amp; { key: string })[] {\n    return ensureTypedArray(arr).map(x => ({\n        ...x,\n        key: addOrCreateKey(x)\n    }));\n}\n\nfunction addOrCreateKey(x: any): string {\n    if (!x?.key) return nanoid();\n    return x.key;\n}<\/pre><\/div>\n","protected":false},"excerpt":{"rendered":"<p>React WIP Example<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-9410","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/9410","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/comments?post=9410"}],"version-history":[{"count":10,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/9410\/revisions"}],"predecessor-version":[{"id":9504,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/9410\/revisions\/9504"}],"wp:attachment":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/media?parent=9410"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/categories?post=9410"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/tags?post=9410"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}