diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..1875848
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,6 @@
+- [ ] Add shown columns.
+- [ ] Test async content.
+- [ ] Multi-columns sort.
+- [ ] Pagination.
+- [ ] Filters.
+- [ ] Async filters.
diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx
index 115918b..3447b55 100644
--- a/demo/DemoApp.tsx
+++ b/demo/DemoApp.tsx
@@ -1,10 +1,14 @@
import React from "react";
import {Application} from "@kernelui/core";
+import {DemoTable} from "./DemoTable";
export function DemoApp()
{
return (
+ Simple table
+
+
)
}
diff --git a/demo/DemoTable.tsx b/demo/DemoTable.tsx
new file mode 100644
index 0000000..e8f4ea2
--- /dev/null
+++ b/demo/DemoTable.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/Smartable/AsyncManager.tsx b/src/Smartable/AsyncManager.tsx
new file mode 100644
index 0000000..dd3729a
--- /dev/null
+++ b/src/Smartable/AsyncManager.tsx
@@ -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
+{
+ /**
+ * Current rows state, undefined if they are still loading.
+ */
+ rows?: CurrentRowData[];
+}
+
+/**
+ * Smartable current row cells data state.
+ */
+export type CurrentRowData = Modify, {
+ cells: Record>;
+}>;
+
+
+/**
+ * Get current async table data manager.
+ */
+export function useAsyncManager(data: SmartableData): AsyncManager
+{
+ // Get the main instance of async manager.
+ const asyncManager = useRef>();
+
+ // Get the main async manager state.
+ const [currentDataState, setCurrentDataState] = useState>({
+ rows: undefined,
+ });
+
+ if (!asyncManager.current)
+ {
+ // Initialize a new async manager if there is none.
+ asyncManager.current = new AsyncManager();
+ }
+
+ // 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
+{
+ currentDataState: CurrentTableData;
+ setCurrentDataState: React.Dispatch>>;
+
+
+ /**
+ * Data state update object.
+ */
+ protected dataStateUpdate: {
+ rows: Record, {
+ cells: Partial>;
+ }>>;
+ };
+
+ /**
+ * Main promised data.
+ */
+ protected promisedData: Promised>;
+
+ /**
+ * Promised row definitions.
+ */
+ protected promisedRowsDefinitions: Promised>[];
+
+ /**
+ * Promised full rows.
+ */
+ protected promisedRows: Modify, {
+ cells: Record>;
+ }>[];
+
+ 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)
+ {
+ // 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>)[]): void
+ {
+ // Initialize a new array of updated promised rows.
+ const updatedPromiseRows: Promised>[] = [];
+
+ 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): void
+ {
+ if (!("cells" in newRow) || !Object.values(newRow?.cells).some((cellData: Promisable) => (
+ 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,
+ };
+ }
+
+ // 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][]).map(([columnKey, cellData]) => (
+ // Return the same entry, with a promised instead of a promisable.
+ [columnKey, (this.promisedRows?.[rowId]?.cells?.[columnKey] ?? new Promised(this.handleNewCell.bind(this, rowId, columnKey))).refresh(cellData)]
+ ))
+ ) as Record>,
+ });
+
+ // 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,
+ 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
+ {
+ 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;
+ }
+ }
+
+ return newState;
+ }
+}
+
+export class Promised
+{
+ promise?: Promise;
+
+ data?: T;
+
+ onData?: (data: T) => void;
+
+ constructor(onChanged?: (data: T) => void, data?: Promisable)
+ {
+ this.onData = onChanged;
+ if (data)
+ this.refresh(data);
+ }
+
+ refresh(data: Promisable): 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;
+ }
+}
diff --git a/src/Smartable/Column.tsx b/src/Smartable/Column.tsx
index 3634b50..83aab33 100644
--- a/src/Smartable/Column.tsx
+++ b/src/Smartable/Column.tsx
@@ -1,5 +1,6 @@
import React, {useCallback, useContext} from "react";
import {Smartable, useTable} from "./Smartable";
+import {Instance} from "./Instance";
/**
* Basic column key type.
@@ -36,6 +37,13 @@ export interface Column
* Column cell default element.
*/
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
)
}
+
+/**
+ * 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));
+}
diff --git a/src/Smartable/Instance.tsx b/src/Smartable/Instance.tsx
index 0a6dbb3..c00f383 100644
--- a/src/Smartable/Instance.tsx
+++ b/src/Smartable/Instance.tsx
@@ -3,6 +3,7 @@ import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnK
import {SmartableProperties, useTable} from "./Smartable";
import {Async, Promisable} from "@kernelui/core";
import {RowDefinition, RowInstance} from "./Row";
+import {useAsyncManager} from "./AsyncManager";
/**
* Smartable instance component properties.
@@ -55,7 +56,21 @@ export function TableBody()
// Get data from table.
const {data} = useTable();
+ // Get async data manager for the current table.
+ const asyncManager = useAsyncManager(data);
+
+ console.log(
+ asyncManager.currentDataState
+ );
+
return (
+ asyncManager.currentDataState?.rows?.map((rowData, index) => (
+ // Rendering each row.
+
+ ))
+ );
+
+ /*return (
>)[]> promise={data}>
{(rowsData) => (
// Rendering defined rows.
@@ -73,5 +88,9 @@ export function TableBody()
>
)}
- );
+ );*/
+}
+
+export function TableRows()
+{
}
diff --git a/src/Utils.tsx b/src/Utils.tsx
new file mode 100644
index 0000000..e69de29