[READ-ONLY] Mirror of https://github.com/andrioid/generic-table-ts. Prototype. Playing around with TypeScript generics and a stackable Table.
0

Configure Feed

Select the types of activity you want to include in your feed.

generic-table-ts / src / stacked-table.tsx
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}