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