[READ-ONLY] Mirror of https://github.com/andrioid/generic-table-ts. Prototype. Playing around with TypeScript generics and a stackable Table.
6.7 kB
177 lines
1/** Purpose: Make it easy to make collapsable tables with Tailwind
2 *
3 * TODO: Tailwind's purge functionality is cleaning up our classes and the only workaround so far is to add the classes to safelist in the <tailwind className="config js"></tailwind>
4 * TODO. Maybe if we use the classes more explicitely, this will work.
5 *
6 **/
7
8import React, { ReactElement } from "react";
9import { clsx } from "clsx";
10
11type TableFieldMap<Entity> = {
12 [K in keyof Entity]: TableField<Entity, K>;
13};
14
15type Breakpoint = "sm" | "md" | "lg" | "xl";
16
17type TableField<Entity, K extends keyof Entity> = {
18 label: string;
19 collapse?: {
20 collapseOn?: keyof Entity; // If not defined then it just collapses completely
21 breakpoint: Breakpoint;
22 };
23 renderField?: (options: {
24 value: Entity[K];
25 config: TableFieldMap<Entity>[keyof Entity];
26 row: Entity;
27 }) => ReactElement | null;
28 /**
29 * weight overrides the default order and applies sorting to the field names
30 */
31 weight?: number;
32};
33
34export function StackedTable<Entity extends { id: number | string }>({
35 data,
36 dataFields,
37 extraFields,
38}: {
39 data: Entity[];
40 /** Field configuration control how the data is displayed */
41 dataFields: Partial<TableFieldMap<Entity>>;
42 /**
43 * Extra Fields configure any extra columns you might want, that do not map to data directly
44 * Rendered last, unless weight is applied
45 * */
46 extraFields?: Partial<Record<string, TableField<Entity, any>>>;
47}) {
48 const combinedFields = { ...dataFields, ...extraFields };
49 const fieldList = Object.entries(combinedFields).map(
50 ([key, fieldConfig]: [string, TableField<Entity, any>]) => ({
51 key,
52 config: fieldConfig,
53 })
54 );
55 // TODO: Add weight sorting to fieldList
56 return (
57 <div className="-mx-4 mt-8 overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:-mx-6 md:mx-0 md:rounded-lg">
58 <table className="min-w-full divide-y divide-gray-300">
59 <thead className="bg-gray-50">
60 <tr>
61 {fieldList.map((fc) => {
62 const bp = fc.config.collapse?.breakpoint;
63 return (
64 <th
65 key={fc.key}
66 scope="col"
67 className={clsx(
68 "py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6py-3.5",
69 bp === "sm" && "hidden sm:table-cell",
70 bp === "md" && "hidden md:table-cell",
71 bp === "lg" && "hidden lg:table-cell",
72 bp === "xl" && "hidden xl:table-cell"
73 )}
74 >
75 {fc.config.label}
76 </th>
77 );
78 })}
79 </tr>
80 </thead>
81 <tbody className="divide-y divide-gray-200 bg-white">
82 {data.map((row) => {
83 return (
84 <tr key={`${row.id}`}>
85 {fieldList.map((fc) => {
86 const fieldValue = row[fc.key as keyof typeof row];
87 const bp = fc.config.collapse?.breakpoint;
88 return (
89 <td
90 key={fc.key}
91 className={clsx(
92 "px-6 py-4 text-left text-sm text-gray-500",
93 bp === "sm" && "hidden sm:table-cell",
94 bp === "md" && "hidden md:table-cell",
95 bp === "lg" && "hidden lg:table-cell",
96 bp === "xl" && "hidden xl:table-cell"
97 )}
98 >
99 <>
100 {fc.config.renderField
101 ? fc.config.renderField({
102 value: fieldValue,
103 config: fc.config,
104 row,
105 })
106 : defaultFieldRenderer(fieldValue)}
107 <dl className="font-normal">
108 {fieldList // Collapsed fields here
109 .filter((cfc) => {
110 if (!row[cfc.key as keyof typeof row]) {
111 return false;
112 }
113 if (cfc.config.collapse?.collapseOn !== fc.key) {
114 return false;
115 }
116 return true;
117 })
118 .map((cfc) => {
119 const bp = cfc.config.collapse?.breakpoint;
120 return (
121 <React.Fragment
122 key={`${row.id}-${cfc.config.label}`}
123 >
124 <dt className="sr-only">
125 {cfc.config.label}
126 </dt>
127 <dd
128 className={clsx(
129 "mt-1 truncate text-gray-700",
130 bp === "sm" && "sm:hidden",
131 bp === "md" && "md:hidden",
132 bp === "lg" && "lg:hidden",
133 bp === "xl" && "xl:hidden"
134 )}
135 >
136 {cfc.config.renderField
137 ? cfc.config.renderField({
138 value: fieldValue,
139 config: cfc.config,
140 row,
141 })
142 : defaultFieldRenderer(
143 row[cfc.key as keyof typeof row]
144 )}
145 </dd>
146 </React.Fragment>
147 );
148 })}
149 </dl>
150 </>
151 </td>
152 );
153 })}
154 </tr>
155 );
156 })}
157 </tbody>
158 </table>
159 </div>
160 );
161}
162
163function defaultFieldRenderer(value: unknown) {
164 const type = typeof value;
165 switch (type) {
166 case "string":
167 case "number":
168 case "bigint":
169 return <>{value}</>;
170 case "undefined":
171 console.error(`[StackedTable] Received extra field, but no renderer`);
172 return "[missing renderer]";
173 default:
174 console.error(`[StackedTable] Missing renderer for type '${type}'`);
175 return "[missing renderer]";
176 }
177}