Smartable tables basic structure.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Madeorsk 2024-07-19 22:53:14 +02:00
parent 036861bf4d
commit dd8b0bde8a
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
8 changed files with 456 additions and 6 deletions

View file

@ -1,4 +1,5 @@
import "@kernelui/core/lib/style.css"; import "@kernelui/core/lib/style.css";
import "../src/styles/smartable.less";
import React from "react"; import React from "react";
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";

View file

@ -17,7 +17,7 @@
"@kernelui:registry": "https://code.zeptotech.net/api/packages/UIKernel/npm/" "@kernelui:registry": "https://code.zeptotech.net/api/packages/UIKernel/npm/"
}, },
"dependencies": { "dependencies": {
"@kernelui/core": "^1.0.1" "@kernelui/core": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "^18.3.3",

69
src/Smartable/Cell.tsx Normal file
View file

@ -0,0 +1,69 @@
import React, {useContext} from "react";
import {ColumnKey, useColumn} from "./Column";
import {Smartable} from "./Smartable";
import {useRow} from "./Row";
/**
* Smartable cell definition.
*/
export interface CellDefinition<T = any>
{
/**
* Cell data.
*/
data: T;
/**
* Rendered cell element.
*/
element?: React.ReactElement;
}
/**
* Default cell component.
*/
export function Cell({children}: React.PropsWithChildren<{}>)
{
// Get cell data.
const {data} = useCell();
// Try to render cell data to string when no children given.
return (
<td>{children ?? String(data)}</td>
);
}
/**
* Table cell context data.
*/
export interface CellContextData<CK extends ColumnKey, T = any> extends CellDefinition<T>
{
}
const CellContext = React.createContext<CellContextData<ColumnKey>>(undefined);
/**
* Hook to get current cell data.
*/
export function useCell<CK extends ColumnKey, T = any>(smartable?: Smartable<CK>): CellContextData<CK, T>
{
return useContext(CellContext);
}
/**
* Cell instance component.
*/
export function CellInstance<T>({cell}: {cell: CellDefinition<T>})
{
// Get column data.
const {column} = useColumn();
const row = useRow();
return (
<CellContext.Provider value={cell}>
{ // Trying to render cell-specific element, then column-specific element, then default element.
cell.element ?? column.cellElement ?? row.cellElement ?? <Cell />
}
</CellContext.Provider>
)
}

104
src/Smartable/Column.tsx Normal file
View file

@ -0,0 +1,104 @@
import React, {useContext} from "react";
import {Smartable, useTable} from "./Smartable";
/**
* Basic column key type.
*/
export type ColumnKey = string|symbol;
/**
* Smartable columns definition.
*/
export type Columns<K extends ColumnKey> = Record<K, Column>;
/**
* Define columns for a Smartable.
* @param columns
*/
export function createColumns<K extends ColumnKey>(...columns: [K, Column][]): Columns<K>
{
return Object.fromEntries(
columns,
) as Columns<K>;
}
/**
* Smartable column definition.
*/
export interface Column
{
/**
* Column title element.
*/
title: React.ReactNode;
/**
* Column cell default element.
*/
cellElement?: React.ReactElement;
}
/**
* Create a column for a Smartable.
* @param key The column key, which should be unique per Smartable instance.
* @param column The column definition.
*/
export function createColumn<K extends ColumnKey>(key: K, column: Column): [K, Column]
{
return [key, column];
}
/**
* Table column context data.
*/
export interface ColumnContextData<CK extends ColumnKey>
{
/**
* Column key.
*/
key: ColumnKey;
/**
* Column definition.
*/
column: Column;
}
export const ColumnContext = React.createContext<ColumnContextData<ColumnKey>>(undefined);
/**
* Hook to get current column data.
*/
export function useColumn<CK extends ColumnKey>(smartable?: Smartable<CK>): ColumnContextData<CK>
{
return useContext(ColumnContext);
}
/**
* Default column heading component.
*/
export function ColumnHeading()
{
// Get current column data.
const {column} = useColumn();
return <th>{column.title}</th>;
}
/**
* Auto column context provider from table context.
*/
export function AutoColumnContextProvider({columnKey, children}: React.PropsWithChildren<{columnKey: ColumnKey;}>)
{
// Get table data.
const table = useTable();
return (
<ColumnContext.Provider value={{
key: columnKey,
// Get current column data from table data.
column: table.columns[columnKey],
}}>
{children}
</ColumnContext.Provider>
)
}

View file

@ -0,0 +1,80 @@
import React from "react";
import {Column, ColumnContext, ColumnHeading, ColumnKey, Columns} from "./Column";
import {SmartableProperties, useTable} from "./Smartable";
import {Async, Promisable} from "@kernelui/core";
import {RowDefinition, RowInstance} from "./Row";
/**
* Smartable instance component properties.
*/
export interface InstanceProperties<CK extends ColumnKey> extends SmartableProperties<CK>
{
/**
* Table columns.
*/
columns: Columns<CK>;
}
/**
* Main component for a Smartable table.
*/
export function Instance<CK extends ColumnKey>({columns}: InstanceProperties<CK>)
{
return (
<table>
<thead>
<ColumnsHeadings columns={columns} />
</thead>
<tbody>
<TableBody />
</tbody>
</table>
);
}
/**
* Columns headings of a Smartable table.
*/
export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Columns<CK>})
{
return (
<tr className={"headings"}>
{ // Showing title of each column.
Object.entries(columns).map(([key, column]) => (
<ColumnContext.Provider key={key} value={{
key: key,
column: column as Column,
}}>
<ColumnHeading/>
</ColumnContext.Provider>
))
}
</tr>
);
}
export function TableBody<CK extends ColumnKey>()
{
// Get data from table.
const {data} = useTable();
return (
<Async<(Promisable<RowDefinition<CK>>)[]> promise={data}>
{(rowsData) => (
// Rendering defined rows.
<>
{ // Rendering each row.
rowsData.map((rowData, index) => (
// Rendering current row from its definition.
<Async<RowDefinition<CK>> key={index} promise={rowData}>
{(rowDefinition) => (
<RowInstance row={rowDefinition} />
)}
</Async>
))
}
</>
)}
</Async>
);
}

105
src/Smartable/Row.tsx Normal file
View file

@ -0,0 +1,105 @@
import React, {useContext, useMemo} from "react";
import {AutoColumnContextProvider, ColumnKey} from "./Column";
import {CellDefinition, CellInstance} from "./Cell";
import {Smartable, useTable} from "./Smartable";
import {Async, Promisable} from "@kernelui/core";
/**
* Smartable row cells.
*/
export type RowCells<CK extends ColumnKey, T = any> = Record<CK, Promisable<CellDefinition<T>>>;
/**
* Smartable row data.
*/
export interface RowData<CK extends ColumnKey, T = any>
{
/**
* Cells definition.
*/
cells: RowCells<CK, T>;
/**
* Rendered row element.
*/
element?: React.ReactElement;
/**
* Rendered row cell element.
*/
cellElement?: React.ReactElement;
}
/**
* Smartable row definition.
*/
export type RowDefinition<CK extends ColumnKey> = RowCells<CK>|RowData<CK>;
/**
* Table row context data.
*/
export interface RowContextData<CK extends ColumnKey, T = any> extends RowData<CK, T>
{
}
const RowContext = React.createContext<RowContextData<ColumnKey>>(undefined);
/**
* Hook to get current row data.
*/
export function useRow<CK extends ColumnKey, T = any>(smartable?: Smartable<CK>): RowContextData<CK, T>
{
return useContext(RowContext);
}
/**
* Default row component.
*/
export function Row()
{
return (
<tr>
<RowCells />
</tr>
);
}
export function RowCells()
{
// Get row data.
const row = useRow();
return (
Object.entries(row.cells).map(([columnKey, cellData]) => (
<AutoColumnContextProvider key={columnKey} columnKey={columnKey}>
<Async<CellDefinition> promise={cellData}>
{(cellDefinition) => (
<CellInstance cell={cellDefinition} />
)}
</Async>
</AutoColumnContextProvider>
))
);
}
/**
* Row instance component.
*/
export function RowInstance<CK extends ColumnKey>({row}: { row: RowDefinition<CK> })
{
// Get row context value from given row definition.
const rowContextValue = useMemo(() => (
// If a simple RowCells<CK> object is given, converting it to a RowData<CK>
!("cells" in row) ? { cells: row } : row
), [row]);
const {rowElement} = useTable();
return (
<RowContext.Provider value={rowContextValue}>
{ // Trying to render row-specific element, then table-specific element, then default element.
rowContextValue.element ?? rowElement ?? <Row />
}
</RowContext.Provider>
);
}

View file

@ -0,0 +1,91 @@
import React, {useContext} from "react";
import {Instance} from "./Instance";
import {ColumnKey, Columns} from "./Column";
import {RowDefinition} from "./Row";
import {Promisable} from "@kernelui/core";
/**
* Smartable data type.
*/
export type SmartableData<CK extends ColumnKey> = Promisable<(Promisable<RowDefinition<CK>>)[]>;
/**
* Smartable properties.
*/
export interface SmartableProperties<CK extends ColumnKey>
{
/**
* Table data.
*/
data: SmartableData<CK>;
/**
* Default row element.
*/
rowElement?: React.ReactElement;
/**
* Default column heading element.
*/
columnHeadingElement?: React.ReactElement;
}
/**
* Smartable main object.
*/
export type Smartable<CK extends ColumnKey> = {
/**
* Table component.
*/
Table: React.FunctionComponent<SmartableProperties<CK>>;
};
/**
* Define a new Smartable.
*/
export function createSmartable<CK extends ColumnKey>({columns}: {
/**
* Smartable table columns.
*/
columns: Columns<CK>;
}): Smartable<CK>
{
return {
Table: (props: SmartableProperties<CK>) => {
return (
<TableContext.Provider value={{
columns: columns,
...props,
}}>
<Instance columns={columns} {...props} />
</TableContext.Provider>
);
},
};
}
/**
* Smartable table context data.
*/
export interface TableContextData<CK extends ColumnKey> extends SmartableProperties<CK>
{
/**
* Current table columns.
*/
columns: Columns<CK>;
}
/**
* Smartable table context.
*/
const TableContext = React.createContext<TableContextData<ColumnKey>>(undefined);
/**
* Hook to get current Smartable table context.
* @param smartable The Smartable main object.
*/
export function useTable<CK extends ColumnKey>(smartable?: Smartable<CK>): TableContextData<CK>
{
return useContext(TableContext);
}

View file

@ -563,9 +563,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@kernelui/core@npm:^1.0.1": "@kernelui/core@npm:^1.1.0":
version: 1.0.1 version: 1.1.0
resolution: "@kernelui/core@npm:1.0.1::__archiveUrl=https%3A%2F%2Fcode.zeptotech.net%2Fapi%2Fpackages%2FUIKernel%2Fnpm%2F%2540kernelui%252Fcore%2F-%2F1.0.1%2Fcore-1.0.1.tgz" resolution: "@kernelui/core@npm:1.1.0::__archiveUrl=https%3A%2F%2Fcode.zeptotech.net%2Fapi%2Fpackages%2FUIKernel%2Fnpm%2F%2540kernelui%252Fcore%2F-%2F1.1.0%2Fcore-1.1.0.tgz"
dependencies: dependencies:
"@floating-ui/react": "npm:^0.26.17" "@floating-ui/react": "npm:^0.26.17"
"@fontsource-variable/jetbrains-mono": "npm:^5.0.21" "@fontsource-variable/jetbrains-mono": "npm:^5.0.21"
@ -577,7 +577,7 @@ __metadata:
react-merge-refs: "npm:^2.1.1" react-merge-refs: "npm:^2.1.1"
react-router-dom: "npm:^6.24.1" react-router-dom: "npm:^6.24.1"
uuid: "npm:^10.0.0" uuid: "npm:^10.0.0"
checksum: 10c0/39b65996ca12eb3384db24a97c554c6a84e88088862dcdbd31331227f8d9a4fc91544367a9c5cabc6c6dc9154c8b904028b719eca2f6811297f8578e11b07638 checksum: 10c0/24ade4c41c2d0f55301324550ee548127532b14217f4104e2de8ee3a3243adad11b61abe1c4275f1978d3175201392ac99188c89f02b7191ea97f6cdac698e4f
languageName: node languageName: node
linkType: hard linkType: hard
@ -585,7 +585,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@kernelui/smartable@workspace:." resolution: "@kernelui/smartable@workspace:."
dependencies: dependencies:
"@kernelui/core": "npm:^1.0.1" "@kernelui/core": "npm:^1.1.0"
"@types/react": "npm:^18.3.3" "@types/react": "npm:^18.3.3"
"@types/react-dom": "npm:^18.3.0" "@types/react-dom": "npm:^18.3.0"
"@vitejs/plugin-react": "npm:^4.3.0" "@vitejs/plugin-react": "npm:^4.3.0"