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;
}

WIP

import { Observable, Observer, Subject, Subscription } from "rxjs";

export interface ObservableValue<T> {
    getValue(): T;
    setValue(value: T): void;
    asObservable(): Observable<T>;
    subscribe(
        next?: ((value: T) => void) | null,
        error?: ((error: any) => void) | null,
        complete?: (() => void) | null
    ): Subscription;
}

/**
 * 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);
    return {
        getValue: () => currentValue,
        setValue: (value: T) => {
            console.log("setValue", value);
            currentValue = value;
            subject.next(value);
        },
        asObservable: () => subject.asObservable(),
        subscribe: subscribe //(next, error, complete) => subject.subscribe({ next, error, complete } as Partial<Observer<T>>),
    };
}

export class MutableObservableCollection<T extends { key: string | number }> {
    private ov: ObservableValue<T[]>;

    constructor(initialItems: T[] = []) {
        this.ov = makeObservableValue(initialItems);
    }

    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 => item.key === updatedItem.key ? updatedItem : item));
    }

    addItem(newItem: T): void {
        this.setItems([...this.getItems(), newItem]);
    }

    removeItem(key: string | number): void {
        const newItems = this.getItems().filter(item => item.key !== key);
        this.setItems(newItems);
    }

    getItem(key: string | number): T | undefined {
        return this.getItems().find(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.setItems(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.setItems(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 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();
}
94100cookie-checkTypescript MutableObservable and MutableObservableCollection