Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comprehensive example #34

Open
ChrisBuergi opened this issue Sep 26, 2024 · 0 comments
Open

Comprehensive example #34

ChrisBuergi opened this issue Sep 26, 2024 · 0 comments

Comments

@ChrisBuergi
Copy link

I have been trying to create a more comprehensive example that includes one-to-one (easy), one-to-many, and many-to-one relationships between signals.

For this example I came up with the following two data tables, where new rows can be added and existing rows can be removed at any time. The empty fields are all computed, the rest of the fields are a signal.

Building rows:
+-----+--------+------------+------------------+
| id  | name   | room_count | total_floor_area |
+-----+--------+------------+------------------+
| 1   | Bldg 1 |            |                  |
+-----+--------+------------+------------------+

Room rows:
+-----+---------+-------------+--------+-------+------+-----------------------+
| id  | name    | building_id | length | width | area | building_area_percent |
+-----+---------+-------------+--------+-------+------+-----------------------+
| 101 | Room 1  | 1           | 5      | 4     |      |                       |
| 102 | Room 2  | 1           | 2      | 2     |      |                       |
| 103 | Room 10 | 2           | 10     | 3     |      |                       |
| 104 | Room 3  | 1           | 3      | 3     |      |                       |
+-----+---------+-------------+--------+-------+------+-----------------------+

And this is the code I came up with. It just feels complicated or too verbose and not in the style of the simplicity of this library. My biggest issue is with how to best track a list of dicts for added, removed and changed items? I'm certain there is a better way than what I did.

Maybe someone has a better way to solve these dependencies, so the fields are computed when a signal changes or a row is added or removed. Thank you!

import {
    root,
    signal,
    computed,
    effect,
    tick,
    WriteSignal,
    ReadSignal,
    Effect,
    peek
} from './maverick-js';



type CustomDict<KT extends string|number|symbol, VT> = {
    [key in KT]: VT;
};


interface IRowBase {
    id: WriteSignal<number>;
    parentList: WriteSignal<any>;
}

class BaseRows<RT extends IRowBase> {
    protected _rows: Array<RT> = [];
    protected _rowIdxById: CustomDict<number, number> = {};
    protected _rowIdEffectsById: CustomDict<number, Effect> = {};

    public rowCount: WriteSignal<number> = signal(this._rows.length);

    constructor() {
    }

    addRow(row: RT) {
        let rowId = row.id();

        // Only allow to add the same row once.
        if (this._rowIdxById[rowId] >= 0) {
            return;
        }

        this._rows.push(row);
        this._rowIdxById[rowId] = this._rows.length-1;
        this._rowIdEffectsById[rowId] = effect(() => {
            // TODO: Is there a way to access the previous value without resorting to keeping it in the outer scope?
            // Handle an "id" change.
            const newRowId = row.id();
            if (rowId !== newRowId) {
                this._rowIdxById[newRowId] = this._rowIdxById[rowId];
                this._rowIdEffectsById[newRowId] = this._rowIdEffectsById[rowId];
                delete this._rowIdxById[rowId];
                delete this._rowIdEffectsById[rowId];
                rowId = newRowId;
            }
        }, {id: '_rowIdEffect'});
        this.rowCount.set(this._rows.length);
        row.parentList.set(this);
    }

    removeRowByRow(row: RT): RT {
        const idx = this._rows.findIndex((value) => peek(value.id) == peek(row.id));
        return this.removeRowByIdx(idx);
    }
    removeRowByIdx(idx: number): RT {
        let row = undefined,
            rowId;
        if (idx >= 0 && idx < this._rows.length) {
            row = this._rows[idx];
            rowId = row.id();
            this._rows.splice(idx, 1);
            delete this._rowIdxById[rowId];
            // Stop the effect and remove the entry.
            this._rowIdEffectsById[rowId]();
            delete this._rowIdEffectsById[rowId];
            this.rowCount.set(this._rows.length);
            row.parentList.set(undefined);
        }
        return row;
    }

    findRowById(id: number): RT {
        const idx = this._rowIdxById[id];
        if (idx >= 0 && idx < this._rows.length) {
            return this._rows[idx];
        }
        return undefined;
    }

    iterateRows(callbackFn: (value: RT, index: number) => void, thisArg?: any) {
        this._rows.forEach(callbackFn, thisArg);
    }
}


class BuildingRow implements IRowBase {
    private __roomRows: Array<RoomRow> = [];

    id: WriteSignal<number>;
    room_count: WriteSignal<number>;
    total_floor_area: ReadSignal<number>;

    parentList: WriteSignal<BuildingRows> = signal(undefined);
    constructor(dataRow: CustomDict<string, any>, public dataRoot: DataRoot) {
        this.id = signal(dataRow['id']);
        this.room_count = signal(0);
        this.total_floor_area = computed<number>(() => {
            this.room_count();
            return this.__roomRows.reduce((previousValue, currentValue, currentIndex, array) => previousValue + currentValue.area(), 0);
        });

        // Check if there are any rooms that belong to the current building.
        const roomRowsSource = this.dataRoot.roomRows;
        roomRowsSource.iterateRows((roomRow) => {
            if (peek(roomRow.building_id) == peek(this.id)) {
                this.__roomRows.push(roomRow);
            }
            this.room_count.set(this.__roomRows.length);
        });
    }

    attachRoomRow(roomRow: RoomRow) {
        // Do not allow to add the same row twice.
        const roomRowId = peek(roomRow.id);
        const idx = this.__roomRows.findIndex(v => peek(v.id) == roomRowId);
        if (idx == -1) {
            this.__roomRows.push(roomRow);
            this.room_count.set(this.__roomRows.length);
        }
    }
    detachRoomRow(roomRow: RoomRow) {
        const roomRowId = peek(roomRow.id);
        const idx = this.__roomRows.findIndex(v => peek(v.id) == roomRowId);
        if (idx > -1) {
            this.__roomRows.splice(idx, 1);
            this.room_count.set(this.__roomRows.length);
        }
    }
}

class BuildingRows extends BaseRows<BuildingRow> {

}

class RoomRow implements IRowBase {
    id: WriteSignal<number>;
    building_id: WriteSignal<number>;
    length: WriteSignal<number>;
    width: WriteSignal<number>;
    area: ReadSignal<number>;
    building_area_percent: ReadSignal<number>;

    parentList: WriteSignal<BuildingRows> = signal(undefined);

    // Helper computes...
    buildingRow: ReadSignal<BuildingRow>;
    private _prevBuildingRow: BuildingRow;

    constructor(dataRow: CustomDict<string, any>, public dataRoot: DataRoot) {
        const this_ = this;
        this.id = signal(dataRow['id']);
        this.building_id = signal(dataRow['building_id']);
        this.length = signal(dataRow['length']);
        this.width = signal(dataRow['width']);
        this.area = computed<number>(() => {
            return this_.length() * this_.width();
        });
        this.building_area_percent = computed<number>(() => {
            const bldgRow = this_.buildingRow();
            if (bldgRow && bldgRow.total_floor_area() > 0) {
                return this.area() * 100 / bldgRow.total_floor_area();
            }
            return 0;
        });

        // Helper computes...
        this.buildingRow = computed<BuildingRow>(() => {
            const buildingId = this.building_id();
            let resultingBuildingRow = null;

            if (!isNaN(buildingId)) {
                const buildingRow = this.dataRoot.buildingRows.findRowById(buildingId);
                // buildingRow.parentList() returns "undefined" as soon as the building row has been removed from the rows.
                // Calling parentList() allows us to keep track of that.
                // Also track the id of the building row, since that id might change as well.
                if (buildingRow && buildingRow.parentList() != undefined && buildingRow.id() == buildingId) {
                    buildingRow.attachRoomRow(this);
                    resultingBuildingRow = buildingRow;
                } else {
                    // Introduce this dependency so the computed function is re-run when the buildingRow might have been
                    // added to the list.
                    // TODO: This is not very nice.. is it possible to solve this another way?
                    this.dataRoot.buildingRows.rowCount();
                }
            }

            if (this._prevBuildingRow) {
                this._prevBuildingRow.detachRoomRow(this);
            }
            this._prevBuildingRow = resultingBuildingRow;
            return resultingBuildingRow;
        });

        effect(() => {
            const parentList = this.parentList();
            if (parentList) {
                this.buildingRow();
            } else if (parentList == undefined && this.buildingRow()) {
                this.buildingRow().detachRoomRow(this);
            }
        });
    }
}

class RoomRows extends BaseRows<RoomRow> {

}

class DataRoot {
    buildingRows: BuildingRows = new BuildingRows();
    roomRows: RoomRows = new RoomRows();

    constructor() {

    }
}


root(() => {
    const dataRoot = new DataRoot();
    const room1 = new RoomRow({id: 101, name: 'R1', building_id: 1, length: 5, width: 4}, dataRoot);
    const room2 = new RoomRow({id: 102, name: 'R2', building_id: 1, length: 2, width: 2}, dataRoot);
    const room3 = new RoomRow({id: 104, name: 'R3', building_id: 1, length: 3, width: 3}, dataRoot);
    dataRoot.roomRows.addRow(room1);
    dataRoot.roomRows.addRow(room2);
    console.log(room2.building_area_percent());

    const bldg1 = new BuildingRow({id: 1, name: 'Building 1'}, dataRoot);
    dataRoot.buildingRows.addRow(bldg1);

    console.log(room1.area());
    console.log(bldg1.room_count());
    console.log(room2.building_area_percent());

    dataRoot.roomRows.addRow(room3);
    tick();
    console.log(room2.building_area_percent());

    dataRoot.roomRows.removeRowByRow(room3);
    tick();
    console.log(room2.building_area_percent());
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant