Typescript MutableObservable and MutableObservableCollection

Date: 2025-04-03
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;
}
94100cookie-checkTypescript MutableObservable and MutableObservableCollection