Compare commits
1 commit
main
...
trash/asyn
Author | SHA1 | Date | |
---|---|---|---|
76c56145ca |
7 changed files with 444 additions and 1 deletions
6
TODO.md
Normal file
6
TODO.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- [ ] Add shown columns.
|
||||||
|
- [ ] Test async content.
|
||||||
|
- [ ] Multi-columns sort.
|
||||||
|
- [ ] Pagination.
|
||||||
|
- [ ] Filters.
|
||||||
|
- [ ] Async filters.
|
|
@ -1,10 +1,14 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Application} from "@kernelui/core";
|
import {Application} from "@kernelui/core";
|
||||||
|
import {DemoTable} from "./DemoTable";
|
||||||
|
|
||||||
export function DemoApp()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Application>
|
<Application>
|
||||||
|
<h1>Simple table</h1>
|
||||||
|
|
||||||
|
<DemoTable />
|
||||||
</Application>
|
</Application>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
71
demo/DemoTable.tsx
Normal file
71
demo/DemoTable.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React from "react";
|
||||||
|
import {createSmartable} from "../src/Smartable/Smartable";
|
||||||
|
import {createColumn, createColumns} from "../src/Smartable/Column";
|
||||||
|
|
||||||
|
// Create main table.
|
||||||
|
const Smartable = createSmartable({
|
||||||
|
columns: createColumns(
|
||||||
|
createColumn("123", {
|
||||||
|
title: "test",
|
||||||
|
}),
|
||||||
|
createColumn("456", {
|
||||||
|
title: "ttt",
|
||||||
|
}),
|
||||||
|
createColumn("789", {
|
||||||
|
title: "another",
|
||||||
|
}),
|
||||||
|
createColumn("test", {
|
||||||
|
title: "last one",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DemoTable()
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Smartable.Table shownColumns={["123", "456", "test"]} data={[
|
||||||
|
{
|
||||||
|
"123": {
|
||||||
|
data: "test abc",
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
data: 123,
|
||||||
|
},
|
||||||
|
"789": {
|
||||||
|
data: "test etset",
|
||||||
|
},
|
||||||
|
"456": {
|
||||||
|
data: "test vccvcvc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"123": {
|
||||||
|
data: "any data",
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
data: 5552,
|
||||||
|
},
|
||||||
|
"789": {
|
||||||
|
data: "foo bar",
|
||||||
|
},
|
||||||
|
"456": {
|
||||||
|
data: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"123": {
|
||||||
|
data: "test test",
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
data: 5552,
|
||||||
|
},
|
||||||
|
"789": {
|
||||||
|
data: "other test",
|
||||||
|
},
|
||||||
|
"456": {
|
||||||
|
data: "infinite testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]} />
|
||||||
|
);
|
||||||
|
}
|
317
src/Smartable/AsyncManager.tsx
Normal file
317
src/Smartable/AsyncManager.tsx
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
|
import {ColumnKey} from "./Column";
|
||||||
|
import {RowCells, RowData, RowDefinition} from "./Row";
|
||||||
|
import {CellDefinition} from "./Cell";
|
||||||
|
import {SmartableData} from "./Smartable";
|
||||||
|
import {Modify, Promisable} from "@kernelui/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current Smartable data state.
|
||||||
|
*/
|
||||||
|
export interface CurrentTableData<CK extends ColumnKey>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Current rows state, undefined if they are still loading.
|
||||||
|
*/
|
||||||
|
rows?: CurrentRowData<CK>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smartable current row cells data state.
|
||||||
|
*/
|
||||||
|
export type CurrentRowData<CK extends ColumnKey, T = any> = Modify<RowData<CK>, {
|
||||||
|
cells: Record<CK, CellDefinition<T>>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current async table data manager.
|
||||||
|
*/
|
||||||
|
export function useAsyncManager<CK extends ColumnKey>(data: SmartableData<CK>): AsyncManager<CK>
|
||||||
|
{
|
||||||
|
// Get the main instance of async manager.
|
||||||
|
const asyncManager = useRef<AsyncManager<CK>>();
|
||||||
|
|
||||||
|
// Get the main async manager state.
|
||||||
|
const [currentDataState, setCurrentDataState] = useState<CurrentTableData<CK>>({
|
||||||
|
rows: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!asyncManager.current)
|
||||||
|
{
|
||||||
|
// Initialize a new async manager if there is none.
|
||||||
|
asyncManager.current = new AsyncManager<CK>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current data state and its dispatcher.
|
||||||
|
asyncManager.current.currentDataState = currentDataState;
|
||||||
|
asyncManager.current.setCurrentDataState = setCurrentDataState;
|
||||||
|
|
||||||
|
// When defined table data change, process async state again.
|
||||||
|
useEffect(() => {
|
||||||
|
// Process new data.
|
||||||
|
asyncManager.current.handle(data);
|
||||||
|
}, [asyncManager, data]);
|
||||||
|
|
||||||
|
//TODO is this required? Update the current async state.
|
||||||
|
//asyncManager.current.update();
|
||||||
|
|
||||||
|
// Return the main instance of async manager.
|
||||||
|
return asyncManager.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smartable async data manager.
|
||||||
|
*/
|
||||||
|
class AsyncManager<CK extends ColumnKey>
|
||||||
|
{
|
||||||
|
currentDataState: CurrentTableData<CK>;
|
||||||
|
setCurrentDataState: React.Dispatch<React.SetStateAction<CurrentTableData<CK>>>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data state update object.
|
||||||
|
*/
|
||||||
|
protected dataStateUpdate: {
|
||||||
|
rows: Record<number, Modify<RowData<CK>, {
|
||||||
|
cells: Partial<Record<CK, CellDefinition>>;
|
||||||
|
}>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main promised data.
|
||||||
|
*/
|
||||||
|
protected promisedData: Promised<SmartableData<CK>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promised row definitions.
|
||||||
|
*/
|
||||||
|
protected promisedRowsDefinitions: Promised<RowDefinition<CK>>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promised full rows.
|
||||||
|
*/
|
||||||
|
protected promisedRows: Modify<RowData<CK>, {
|
||||||
|
cells: Record<CK, Promised<CellDefinition>>;
|
||||||
|
}>[];
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
// Initialize promised data object.
|
||||||
|
this.promisedData = new Promised(this.handleNewData.bind(this));
|
||||||
|
this.promisedRows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new Smartable data.
|
||||||
|
* @param data Smartable data to handle.
|
||||||
|
*/
|
||||||
|
handle(data: SmartableData<CK>)
|
||||||
|
{
|
||||||
|
// Refresh global promised data.
|
||||||
|
this.promisedData.refresh(data);
|
||||||
|
|
||||||
|
console.log(this.dataStateUpdate);
|
||||||
|
// Update state.
|
||||||
|
this.dataStateUpdate = {
|
||||||
|
rows: this.dataStateUpdate?.rows,
|
||||||
|
};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when new Smartable data is loaded.
|
||||||
|
* @param newData New loaded data.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewData(newData: (Promisable<RowDefinition<CK>>)[]): void
|
||||||
|
{
|
||||||
|
// Initialize a new array of updated promised rows.
|
||||||
|
const updatedPromiseRows: Promised<RowDefinition<CK>>[] = [];
|
||||||
|
|
||||||
|
for (const [rowId, row] of newData.entries())
|
||||||
|
{ // For each promisable row, save the promised row in the updated array.
|
||||||
|
updatedPromiseRows[rowId] = (this.promisedRowsDefinitions?.[rowId] ?? new Promised(this.handleNewRow.bind(this, rowId))).refresh(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save new promised rows.
|
||||||
|
this.promisedRowsDefinitions = updatedPromiseRows;
|
||||||
|
|
||||||
|
// Update state.
|
||||||
|
this.dataStateUpdate = {
|
||||||
|
rows: this.dataStateUpdate.rows ?? undefined,
|
||||||
|
};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new row definition is loaded.
|
||||||
|
* @param rowId Row ID.
|
||||||
|
* @param newRow New row definition.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewRow(rowId: number, newRow: RowDefinition<CK>): void
|
||||||
|
{
|
||||||
|
if (!("cells" in newRow) || !Object.values(newRow?.cells).some((cellData: Promisable<CellDefinition>) => (
|
||||||
|
cellData instanceof Promise || ("data" in cellData)
|
||||||
|
))) { // If the row definition doesn't form a RowData object (= it is a RowCell object), converting it.
|
||||||
|
newRow = {
|
||||||
|
cells: newRow as RowCells<CK>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the new row cells object, with the promised cells.
|
||||||
|
this.promisedRows[rowId] = Object.assign({}, newRow, {
|
||||||
|
cells: Object.fromEntries(
|
||||||
|
// For each cell, create its promised object from the given promisable.
|
||||||
|
(Object.entries(newRow.cells) as [CK, Promisable<CellDefinition>][]).map(([columnKey, cellData]) => (
|
||||||
|
// Return the same entry, with a promised instead of a promisable.
|
||||||
|
[columnKey, (this.promisedRows?.[rowId]?.cells?.[columnKey] ?? new Promised<CellDefinition>(this.handleNewCell.bind(this, rowId, columnKey))).refresh(cellData)]
|
||||||
|
))
|
||||||
|
) as Record<CK, Promised<CellDefinition>>,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update state.
|
||||||
|
this.dataStateUpdate = {
|
||||||
|
rows: {
|
||||||
|
[rowId]: {
|
||||||
|
cells: Object.fromEntries((Object.keys(newRow.cells) as CK[]).map((columnKey) => (
|
||||||
|
[columnKey, this.dataStateUpdate.rows[rowId].cells[columnKey]] ?? [columnKey, undefined]
|
||||||
|
))) as Record<CK, CellDefinition>,
|
||||||
|
cellElement: newRow.cellElement,
|
||||||
|
element: newRow.element,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new row cell definition is loaded.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewCell(rowId: number, columnKey: CK, cellData: CellDefinition): void
|
||||||
|
{
|
||||||
|
// Update state.
|
||||||
|
if (!this.dataStateUpdate?.rows)
|
||||||
|
this.dataStateUpdate = { rows: {} };
|
||||||
|
if (!this.dataStateUpdate.rows?.[rowId])
|
||||||
|
this.dataStateUpdate.rows[rowId] = { cells: {}, };
|
||||||
|
this.dataStateUpdate.rows[rowId].cells[columnKey] = cellData;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current async state.
|
||||||
|
*/
|
||||||
|
protected update(): void
|
||||||
|
{
|
||||||
|
// Set the new current state.
|
||||||
|
this.setCurrentDataState(this.currentDataState = this.buildNewState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a new state from the current async state.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected buildNewState(): CurrentTableData<CK>
|
||||||
|
{
|
||||||
|
if (this.promisedData.isInitialized())
|
||||||
|
{ // Waiting for initialization.
|
||||||
|
return this.currentDataState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
rows: this.currentDataState.rows,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.dataStateUpdate.rows)
|
||||||
|
{ // Something changed in the rows.
|
||||||
|
// Copy the existing rows, if there are some.
|
||||||
|
newState.rows = [...this.currentDataState.rows];
|
||||||
|
for (const [rowId, rowData] of Object.entries(this.dataStateUpdate.rows))
|
||||||
|
{ // For each changed row, creating its new state.
|
||||||
|
// Get current row state.
|
||||||
|
const currentRow = this.currentDataState.rows?.[parseInt(rowId)];
|
||||||
|
|
||||||
|
if (currentRow)
|
||||||
|
{ // If there was an existing row, copy the current row state and only change the specified cells.
|
||||||
|
const newRow = {...currentRow};
|
||||||
|
|
||||||
|
if (rowData.cells)
|
||||||
|
{ // If some cells have been changed, updating them.
|
||||||
|
newRow.cells = Object.assign({}, currentRow.cells, rowData.cells);
|
||||||
|
}
|
||||||
|
if (rowData.cellElement)
|
||||||
|
{ // If cell element have been changed, updating it.
|
||||||
|
newRow.cellElement = rowData.cellElement;
|
||||||
|
}
|
||||||
|
if (rowData.element)
|
||||||
|
{ // If element have been changed, updating it.
|
||||||
|
newRow.element = rowData.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new row state.
|
||||||
|
newState.rows[parseInt(rowId)] = newRow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// Create a new row state with the given cells' data.
|
||||||
|
newState.rows[parseInt(rowId)] = {...rowData} as CurrentRowData<CK>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Promised<T>
|
||||||
|
{
|
||||||
|
promise?: Promise<T>;
|
||||||
|
|
||||||
|
data?: T;
|
||||||
|
|
||||||
|
onData?: (data: T) => void;
|
||||||
|
|
||||||
|
constructor(onChanged?: (data: T) => void, data?: Promisable<T>)
|
||||||
|
{
|
||||||
|
this.onData = onChanged;
|
||||||
|
if (data)
|
||||||
|
this.refresh(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(data: Promisable<T>): this
|
||||||
|
{
|
||||||
|
if (data instanceof Promise)
|
||||||
|
{
|
||||||
|
if (data != this.promise)
|
||||||
|
{
|
||||||
|
this.data = undefined;
|
||||||
|
this.promise = data;
|
||||||
|
this.onData?.(undefined);
|
||||||
|
|
||||||
|
this.promise.then((data) => {
|
||||||
|
this.data = data;
|
||||||
|
this.onData?.(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
this.onData?.(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized(): boolean
|
||||||
|
{
|
||||||
|
return !!this.data || !!this.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading(): boolean
|
||||||
|
{
|
||||||
|
return this.data === undefined && !!this.promise;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import React, {useCallback, useContext} from "react";
|
import React, {useCallback, useContext} from "react";
|
||||||
import {Smartable, useTable} from "./Smartable";
|
import {Smartable, useTable} from "./Smartable";
|
||||||
|
import {Instance} from "./Instance";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic column key type.
|
* Basic column key type.
|
||||||
|
@ -36,6 +37,13 @@ export interface Column
|
||||||
* Column cell default element.
|
* Column cell default element.
|
||||||
*/
|
*/
|
||||||
cellElement?: React.ReactElement;
|
cellElement?: React.ReactElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorting function for data of the column.
|
||||||
|
* @param a First data to compare.
|
||||||
|
* @param b Second data to compare.
|
||||||
|
*/
|
||||||
|
sort?: (a: unknown, b: unknown) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,3 +171,21 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
|
||||||
</ColumnContext.Provider>
|
</ColumnContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global generic string comparator.
|
||||||
|
*/
|
||||||
|
const comparator = Intl.Collator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic sorting function for data of a column.
|
||||||
|
* @param a First data to compare.
|
||||||
|
* @param b Second data to compare.
|
||||||
|
*/
|
||||||
|
export function genericColumnSort(a: any, b: any): number
|
||||||
|
{
|
||||||
|
if (typeof a == "number" && typeof b == "number")
|
||||||
|
return b - a;
|
||||||
|
else
|
||||||
|
return comparator.compare(String(a), String(b));
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnK
|
||||||
import {SmartableProperties, useTable} from "./Smartable";
|
import {SmartableProperties, useTable} from "./Smartable";
|
||||||
import {Async, Promisable} from "@kernelui/core";
|
import {Async, Promisable} from "@kernelui/core";
|
||||||
import {RowDefinition, RowInstance} from "./Row";
|
import {RowDefinition, RowInstance} from "./Row";
|
||||||
|
import {useAsyncManager} from "./AsyncManager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smartable instance component properties.
|
* Smartable instance component properties.
|
||||||
|
@ -55,7 +56,21 @@ export function TableBody<CK extends ColumnKey>()
|
||||||
// Get data from table.
|
// Get data from table.
|
||||||
const {data} = useTable();
|
const {data} = useTable();
|
||||||
|
|
||||||
|
// Get async data manager for the current table.
|
||||||
|
const asyncManager = useAsyncManager(data);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
asyncManager.currentDataState
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
asyncManager.currentDataState?.rows?.map((rowData, index) => (
|
||||||
|
// Rendering each row.
|
||||||
|
<RowInstance key={index} row={rowData} />
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
/*return (
|
||||||
<Async<(Promisable<RowDefinition<CK>>)[]> promise={data}>
|
<Async<(Promisable<RowDefinition<CK>>)[]> promise={data}>
|
||||||
{(rowsData) => (
|
{(rowsData) => (
|
||||||
// Rendering defined rows.
|
// Rendering defined rows.
|
||||||
|
@ -73,5 +88,9 @@ export function TableBody<CK extends ColumnKey>()
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Async>
|
</Async>
|
||||||
);
|
);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableRows<CK extends ColumnKey>()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
0
src/Utils.tsx
Normal file
0
src/Utils.tsx
Normal file
Loading…
Reference in a new issue