import { BehaviorSubject, Observable } from "rxjs"; // basically a wrapper for BehaviorSubject export class MutableObservable<T> { private Subject: BehaviorSubject<T>; constructor(value: T) { this.Subject = new BehaviorSubject<T>(value); this.getValue = this.getValue.bind(this); this.setValue = this.setValue.bind(this); this.asObservable = this.asObservable.bind(this); } getValue(): T { return this.Subject.getValue(); } setValue(value: T): void { return this.Subject.next(value); } asObservable() { return this.Subject.asObservable(); } } export class MutableObservableCollection<T extends { key: string | number }> { private subject: BehaviorSubject<T[]>; constructor(initialItems: T[] = []) { this.subject = new BehaviorSubject<T[]>(initialItems); } getItems(): T[] { return this.subject.getValue(); } asObservable(): Observable<T[]> { return this.subject.asObservable(); } setItems(newItems: T[]): void { this.subject.next(newItems); } updateItem(updatedItem: T): void { this.subject.next( this.getItems().map(item => item.key === updatedItem.key ? updatedItem : item) ); } addItem(newItem: T): void { this.subject.next([...this.getItems(), newItem]); } removeItem(key: string | number): void { this.subject.next(this.getItems().filter(item => item.key !== key)); } moveItemUp(key: string | number): void { const items = this.getItems(); const index = items.findIndex(item => item.key === key); // Als het item niet bestaat of al bovenaan staat, doe niets if (index <= 0) return; // Verwissel het item met het item ervoor const newItems = [...items]; [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]]; this.subject.next(newItems); } moveItemDown(key: string | number): void { const items = this.getItems(); const index = items.findIndex(item => item.key === key); // Als het item niet bestaat of al onderaan staat, doe niets if (index === -1 || index >= items.length - 1) return; // Verwissel het item met het item erna const newItems = [...items]; [newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]]; this.subject.next(newItems); } getChangedItems(originalItems: T[]): T[] { return this.getItems().filter(item => { const original = originalItems.find(o => o.key === item.key); return original && JSON.stringify(original) !== JSON.stringify(item); }); } }
React
export function useMutableObservable<T>(subjectInput: MutableObservable<T>): [T, (value: T) => any, (changes: Partial<T>) => any] { const state = useMemo(() => ({ observable: subjectInput.asObservable(), setValue: subjectInput.setValue, getValue: subjectInput.getValue, updateValue: (_changes: Partial<T>) => 0 }), [subjectInput]); const [result, setResult] = useState<[T, (value: T) => any, (changes: Partial<T>) => any]>([state.getValue(), state.setValue, state.updateValue]); const resultRef = useRef(state.getValue()); resultRef.current = state.getValue(); useEffect(() => { if (!state?.observable) return; const subscription = state.observable.subscribe((x: any) => { if (x === resultRef.current) return; setResult([x, state.setValue, state.updateValue]); }); return () => subscription.unsubscribe(); }, [state]); return result; } export function useObservable<T>(subject: Observable<T | undefined>, initialValue?: T): T | undefined { const [data, setData] = useState<T | undefined>(initialValue); useEffect(() => { if (!subject) { return; } const subscription = subject.subscribe((x: any) => { setData(x); }); return () => subscription.unsubscribe(); }, [subject]); return data; }
WIP
iimport { Observable, Subject, Subscription } from "rxjs"; import { AdvancedCollection } from "./MutableObservable"; export interface ISubscribable<T> { subscribe( next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null ): Subscription; } export interface ObservableValue<T> extends ISubscribable<T> { getValue(): T; setValue(fn: (old: T) => T): void; setPartial(v: Partial<T>): void; asObservable(): Observable<T>; } /** * Maakt een gecontroleerde observable waarde met toegang tot de huidige waarde. * Emit geen waarde automatisch bij constructie (zoals BehaviorSubject zou doen), * maar houdt wel de laatste waarde bij. */ export function makeObservableValue<T>(initialValue: T): ObservableValue<T> { let currentValue = initialValue; const subject = new Subject<T>(); const o = subject.asObservable(); const subscribe = o.subscribe.bind(o); const setValue = (fn: (old: T) => T) => { const newValue = fn(currentValue); currentValue = newValue; subject.next(newValue); }; return { getValue: () => currentValue, setPartial: (v: Partial<T>) => setValue(c => ({ ...c, ...v })), setValue: setValue, asObservable: () => subject.asObservable(), subscribe: subscribe }; } export function makeChildObservableValue<T, K extends keyof T>(parent: ObservableValue<T>, key: K): ObservableValue<T[K]> { return makeChildObservableValueFlexible(parent, (p) => p[key], (p, v) => p.setPartial({ [key]: v } as any)); } export function makeChildObservableValueFlexible<T, C>(parent: ObservableValue<T>, getValueFromParent: (parent: T) => C, setInParent: (parent: ObservableValue<T>, childValue: C) => any): ObservableValue<C> { const child = makeObservableValue(getValueFromParent(parent.getValue())); const originalSetValue = child.setValue; child.setValue = (fn: (old: C) => C) => { originalSetValue(fn); const newVal = child.getValue(); setInParent(parent, newVal); }; return child; } export function makeChildAdvancedCollection<TParent, TItem>(parent: ObservableValue<TParent>, getArray: (p: TParent) => TItem[], setArray: (p: ObservableValue<TParent>, newArray: TItem[]) => void, keyFn: (item: TItem) => string | number): AdvancedCollection<TItem> { const initialItems = getArray(parent.getValue()); const ac = new AdvancedCollection<TItem>(keyFn, initialItems); // Override setItems zodat het ook de parent update const originalSetItems = ac.setItems.bind(ac); ac.setItems = (newItems: TItem[]) => { originalSetItems(newItems); setArray(parent, newItems); }; return ac; } export interface IAdvancedCollection<T> { getItems(): T[]; asObservable(): Observable<T[]>; asObservableValue(): ObservableValue<T[]>; setItems(newItems: T[]): void; updateItem(updatedItem: T): void; addItem(newItem: T): void; addItems(newItems: T[]): void; removeItem(key: string | number): void; getItem(key: string | number): T | undefined; moveItemUp(key: string | number): void; moveItemDown(key: string | number): void; getChangedItems(originalItems: T[]): T[]; } export class AdvancedCollection<T> implements IAdvancedCollection<T> { private ov: ObservableValue<T[]>; private keyFn: (o: T) => string | number; constructor(keyFn: (o: T) => string | number, initialItems: T[] = []) { this.ov = makeObservableValue(initialItems); this.keyFn = keyFn; } getItems(): T[] { return [...this.ov.getValue()]; } asObservable(): Observable<T[]> { return this.ov.asObservable(); } asObservableValue(): ObservableValue<T[]> { return this.ov; } setItems(newItems: T[]): void { this.ov.setValue(_ => newItems); } updateItem(updatedItem: T): void { this.setItems(this.getItems().map(item => this.keyFn(item) === this.keyFn(updatedItem) ? updatedItem : item)); } addItem(newItem: T): void { this.setItems([...this.getItems(), newItem]); } addItems(newItems: T[]): void { this.setItems([...this.getItems(), ...newItems]); } removeItem(key: string | number): void { const newItems = this.getItems().filter(item => this.keyFn(item) !== key); this.setItems(newItems); } getItem(key: string | number): T | undefined { return this.getItems().find(item => this.keyFn(item) === key); } moveItemUp(key: string | number): void { const items = this.getItems(); const index = items.findIndex(item => this.keyFn(item) === key); // Als het item niet bestaat of al bovenaan staat, doe niets if (index <= 0) return; // Verwissel het item met het item ervoor const newItems = [...items]; [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]]; this.setItems(newItems); } moveItemDown(key: string | number): void { const items = this.getItems(); const index = items.findIndex(item => this.keyFn(item) === key); // Als het item niet bestaat of al onderaan staat, doe niets if (index === -1 || index >= items.length - 1) return; // Verwissel het item met het item erna const newItems = [...items]; [newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]]; this.setItems(newItems); } getChangedItems(originalItems: T[]): T[] { return this.getItems().filter(item => { const original = originalItems.find(o => this.keyFn(o) === this.keyFn(item)); return original && JSON.stringify(original) !== JSON.stringify(item); }); } } export class MutableObservableCollection<T extends { key: string | number }> extends AdvancedCollection<T> { constructor(initialItems: T[] = []) { super((o: T) => o.key, initialItems); } } // react hooks: export function useObservableValue<T>(subject: ObservableValue<T>): T { const [, forceUpdate] = useReducer(x => x + 1, 0); useEffect(() => { if (!subject) { return; } const subscription = subject.subscribe(forceUpdate); return () => subscription.unsubscribe(); }, [subject]); return subject.getValue(); } export function useObservableValueDebug<T>(subject: ObservableValue<T>, key: string): T { const [, forceUpdate] = useReducer(x => x + 1, 0); useEffect(() => { if (!subject) { return; } console.log('observable-value subscribe', key); const subscription = subject.subscribe((x: T) => { console.log('observable-value changed', key, x); forceUpdate(); }); return () => { console.log('observable-value unsubscribe', key); subscription.unsubscribe(); }; }, [subject, key]); console.log('observable-value render', key, subject.getValue()); return subject.getValue(); }
Example
function DashboardFieldsTest(props: { dashboard: ObservableValue<IDashboardDetailData> }) { const { dashboard } = props; const nameField = makeChildObservableValueFlexible(dashboard, d => d.name, (d, value) => d.setPartial({ name: value })); const titleField = makeChildObservableValueFlexible(dashboard, d => d.title, (d, value) => d.setPartial({ title: value })); const arrayField = makeChildAdvancedCollection(dashboard, d => d.filters, (d, value) => d.setPartial({ filters: value }), (x) => x.field); return <> <ObservableEditor editorName="TextEditor" label={translate("DashboardDetailView.name")} field={nameField} /> {/* <ObservableEditor editorName="TextEditor" label={translate("DashboardDetailView.name")} field={nameField} /> */} <ArrayEditor field={arrayField} /> <Button onClick={() => { arrayField.addItem({ field: nanoid(), operator: ListViewFilterOperator.Between, values: [""] }); console.log("dashboard.getValue().filters", dashboard.getValue().filters); }}> Add item</Button> <ObservableEditor editorName="TextEditor" label={translate("DashboardDetailView.title")} field={titleField} /> </>; } function ArrayEditor(props: { field: IAdvancedCollection<IWidgetFilterArg> }) { const value = useObservableValue(props.field.asObservableValue()); return <>{value.map(x => <div key={x.field}>{x.field} <Button onClick={() => props.field.removeItem(x.field)}>delete</Button></div>)}</> }
import { nanoid } from "nanoid"; import { Observable, Subject, Subscription } from "rxjs"; import { AdvancedCollection } from "./MutableObservable"; import { ensureTypedArray } from "./tshelper"; export interface ISubscribable<T> { subscribe( next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null ): Subscription; } export interface ObservableValue<T> extends ISubscribable<T> { getValue(): T; setValue(fn: (old: T) => T): void; setPartial(v: Partial<T>): void; asObservable(): Observable<T>; } /** * Maakt een gecontroleerde observable waarde met toegang tot de huidige waarde. * Emit geen waarde automatisch bij constructie (zoals BehaviorSubject zou doen), * maar houdt wel de laatste waarde bij. */ export function makeObservableValue<T>(initialValue: T): ObservableValue<T> { let currentValue = initialValue; const subject = new Subject<T>(); const o = subject.asObservable(); const subscribe = o.subscribe.bind(o); const setValue = (fn: (old: T) => T) => { const newValue = fn(currentValue); currentValue = newValue; subject.next(newValue); }; return { getValue: () => currentValue, setPartial: (v: Partial<T>) => setValue(c => ({ ...c, ...v })), setValue: setValue, asObservable: () => subject.asObservable(), subscribe: subscribe }; } export function makeChildObservableValue<T, K extends keyof T>(parent: ObservableValue<T>, key: K): ObservableValue<T[K]> { return makeChildObservableValueFlexible(parent, (p) => p[key], (p, v) => p.setPartial({ [key]: v } as any)); } export function makeChildObservableValueFlexible<T, C>(parent: ObservableValue<T>, getValueFromParent: (parent: T) => C, setInParent: (parent: ObservableValue<T>, childValue: C) => any): ObservableValue<C> { const child = makeObservableValue(getValueFromParent(parent.getValue())); const originalSetValue = child.setValue; child.setValue = (fn: (old: C) => C) => { originalSetValue(fn); const newVal = child.getValue(); setInParent(parent, newVal); }; child.setPartial = (v: Partial<C>) => child.setValue(x => ({ ...x, ...v })); return child; } export function makeChildAdvancedCollection<TParent, TItem>(parent: ObservableValue<TParent>, getArray: (p: TParent) => TItem[], setArray: (p: ObservableValue<TParent>, newArray: TItem[]) => void, keyFn: (item: TItem) => string | number): AdvancedCollection<TItem> { const initialItems = getArray(parent.getValue()); const ac = new AdvancedCollection<TItem>(keyFn, initialItems); // Override setItems zodat het ook de parent update const originalSetItems = ac.setItems.bind(ac); ac.setItems = (newItems: TItem[]) => { originalSetItems(newItems); setArray(parent, newItems); }; return ac; } export function addKeys<T>(arr: T[]): (T & { key: string })[] { return ensureTypedArray(arr).map(x => ({ ...x, key: addOrCreateKey(x) })); } function addOrCreateKey(x: any): string { if (!x?.key) return nanoid(); return x.key; }
941000cookie-checkTypescript MutableObservable and MutableObservableCollection