jattac.libs.web.responsive-table 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ # Recommendations and Pitfalls
2
+ ## Best Practices, Anti-Patterns, and Module Compatibility
3
+
4
+ This document covers three topics: the known ESM/CJS module hazard with a peer dependency, general best practices for optimal usage, and anti-patterns to avoid.
5
+
6
+ ---
7
+
8
+ [← Previous: Handling Interactive Elements](./handling-interactive-elements.md) | [Return to README →](../README.md)
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ - [ESM/CJS Module Compatibility](#esmcjs-module-compatibility)
15
+ - [Root Cause](#root-cause)
16
+ - [Symptoms](#symptoms)
17
+ - [Consumer Workarounds](#consumer-workarounds)
18
+ - [Planned Fix](#planned-fix)
19
+ - [Recommendations](#recommendations)
20
+ - [Row Identity and State Stability](#1-row-identity-and-state-stability)
21
+ - [Interactive Elements and Event Isolation](#2-interactive-elements-and-event-isolation)
22
+ - [Expandable Rows](#3-expandable-rows)
23
+ - [Theming and Visual Customization](#4-theming-and-visual-customization)
24
+ - [Performance and Rendering](#5-performance-and-rendering)
25
+ - [Data Fetching and Pagination](#6-data-fetching-and-pagination)
26
+ - [Column Definitions](#7-column-definitions)
27
+ - [Pitfalls](#pitfalls)
28
+ - [Chevron Transform Override](#1-chevron-transform-override)
29
+ - [rowIndex as a Stable Identifier](#2-rowindex-as-a-stable-identifier)
30
+ - [Missing rowIdKey on Sortable Tables](#3-missing-rowidkey-on-sortable-tables)
31
+ - [Missing data-rt-ignore-row-click](#4-missing-data-rt-ignore-row-click)
32
+ - [Non-null expandRowRenderer for Empty Detail](#5-non-null-expandrowrenderer-for-empty-detail)
33
+ - [Heavy Computation in expandRowRenderer](#6-heavy-computation-in-expandrowrenderer)
34
+ - [Inline Props Without useMemo](#7-inline-props-without-usememo)
35
+ - [No Error Handling with dataSource](#8-no-error-handling-with-datasource)
36
+
37
+ ---
38
+
39
+ ## ESM/CJS Module Compatibility
40
+
41
+ ### Root Cause
42
+
43
+ The peer dependency `jattac.libs.web.zest-textbox` is published with `"type": "module"` in its `package.json`. This tells Node.js to treat all `.js` files in the package as ES modules. However, the file referenced by the `"require"` export condition — `dist/index.js` — is CommonJS (uses `require()` calls, `module.exports`).
44
+
45
+ The conflict:
46
+ 1. Consumer calls `require('jattac.libs.web.zest-textbox')`
47
+ 2. Node.js resolves via `"exports": { "require": "./dist/index.js" }`
48
+ 3. Root `package.json` has `"type": "module"` → Node.js parses `dist/index.js` as ESM
49
+ 4. ESM does not support `require()` → error
50
+
51
+ ### Symptoms
52
+
53
+ - **Next.js Pages Router:** `ERR_REQUIRE_ESM` at build or server start
54
+ - **Jest tests:** `Jest failed to parse a file` — the test runner cannot `require()` the ESM-marked file
55
+ - **Webpack 4 / Create React App:** Module parse failure — bundler cannot handle the unexpected ESM/CJS mismatch
56
+ - **Next.js App Router:** Usually unaffected because `import` resolves to the separate `.esm.js` bundle which is genuine ESM
57
+
58
+ ### Consumer Workarounds
59
+
60
+ #### Next.js
61
+
62
+ **App Router (13+):** If you still see the error, add the package to `transpilePackages`:
63
+
64
+ ```js
65
+ // next.config.js
66
+ module.exports = {
67
+ transpilePackages: ['jattac.libs.web.zest-textbox'],
68
+ };
69
+ ```
70
+
71
+ **Pages Router:** Force webpack to use the ESM entry:
72
+
73
+ ```js
74
+ // next.config.js
75
+ const path = require('path');
76
+
77
+ module.exports = {
78
+ webpack: (config) => {
79
+ config.resolve.alias['jattac.libs.web.zest-textbox'] =
80
+ path.resolve(__dirname, 'node_modules/jattac.libs.web.zest-textbox/dist/index.esm.js');
81
+ return config;
82
+ },
83
+ };
84
+ ```
85
+
86
+ #### Jest
87
+
88
+ Add the package to `transformIgnorePatterns`:
89
+
90
+ ```js
91
+ // jest.config.js
92
+ module.exports = {
93
+ transformIgnorePatterns: [
94
+ '/node_modules/(?!(jattac.libs.web.zest-textbox)/)',
95
+ ],
96
+ };
97
+ ```
98
+
99
+ #### Vite
100
+
101
+ ```js
102
+ // vite.config.js
103
+ export default {
104
+ optimizeDeps: {
105
+ include: ['jattac.libs.web.zest-textbox'],
106
+ },
107
+ };
108
+ ```
109
+
110
+ ### Planned Fix
111
+
112
+ The next release of `jattac.libs.web.zest-textbox` will remove the `"type": "module"` field. The `"exports"` map already correctly routes `require` → CJS (`dist/index.js`) and `import` → ESM (`dist/index.esm.js`), so the field is redundant and harmful.
113
+
114
+ ---
115
+
116
+ ## Recommendations
117
+
118
+ ### 1. Row Identity and State Stability
119
+
120
+ **Always provide `selectionProps.rowIdKey`** when using expandable rows or selection on a table that supports sorting, filtering, or pagination. The key links a row's visual identity to a stable data field so that expand and selection states survive re-order operations.
121
+
122
+ ```tsx
123
+ // Without rowIdKey: re-sort resets all expanded/selected rows
124
+ // With rowIdKey: "Order #1042" stays expanded wherever it moves
125
+ <ResponsiveTable
126
+ data={orders}
127
+ columnDefinitions={columns}
128
+ selectionProps={{
129
+ rowIdKey: 'id',
130
+ onSelectionChange: () => {},
131
+ }}
132
+ expandRowRenderer={(order) => <OrderLineItems orderId={order.id} />}
133
+ />
134
+ ```
135
+
136
+ Even if you do not need row selection, provide `selectionProps` solely to anchor expand state. The no-op `onSelectionChange` is harmless.
137
+
138
+ ### 2. Interactive Elements and Event Isolation
139
+
140
+ When using `onRowClick`, always add `data-rt-ignore-row-click` to buttons, links, inputs, and any other interactive element inside a cell. The table uses an explicit opt-in contract rather than automatic detection because many UI libraries render with custom elements that are not standard `<button>` or `<a>` tags.
141
+
142
+ ```tsx
143
+ const columns = [
144
+ {
145
+ displayLabel: 'Actions',
146
+ cellRenderer: (row) => (
147
+ <button data-rt-ignore-row-click onClick={() => deleteRow(row.id)}>
148
+ Delete
149
+ </button>
150
+ ),
151
+ },
152
+ ];
153
+ ```
154
+
155
+ The same attribute works on containers — any child click is ignored:
156
+
157
+ ```tsx
158
+ <div data-rt-ignore-row-click>
159
+ <MyCustomToggle value={row.flag} onChange={(v) => setFlag(row.id, v)} />
160
+ </div>
161
+ ```
162
+
163
+ See the [Handling Interactive Elements](./handling-interactive-elements.md) guide for detailed scenarios.
164
+
165
+ ### 3. Expandable Rows
166
+
167
+ - **Return `null` for rows with no detail content.** The toggle is completely absent — not hidden — for those rows. A visible chevron that opens an empty panel confuses users.
168
+ - **Keep `expandRowRenderer` lightweight.** It is called for every visible row on every render pass. Expensive work (data fetching, heavy computation) should live inside the detail component itself (which is lazy-mounted).
169
+ - **Use `expandChevronClassName` to customize the chevron.** Override `color`, `font-size`, etc. Never override `transform` or `transition` — these drive the collapse animation.
170
+ - **The chevron is keyboard-accessible.** It has `role="button"`, `tabIndex={0}`, and `aria-expanded`. Enter/Space toggle the panel — no additional configuration needed.
171
+
172
+ ```tsx
173
+ <ResponsiveTable
174
+ data={orders}
175
+ columnDefinitions={columns}
176
+ expandRowRenderer={(order) =>
177
+ order.lineItems.length > 0
178
+ ? <OrderLineItems order={order} />
179
+ : null
180
+ }
181
+ expandChevronClassName="my-chevron"
182
+ />
183
+ ```
184
+
185
+ ### 4. Theming and Visual Customization
186
+
187
+ Override `--primary-color` at the `:root` level rather than targeting individual elements. The chevron, the expanded left-border indicator, selection highlights, focus rings, and the spinner all derive from this single variable.
188
+
189
+ ```css
190
+ :root {
191
+ --primary-color: #7c3aed; /* violet accent */
192
+ }
193
+ ```
194
+
195
+ For per-component overrides, scope the variable to the table's container:
196
+
197
+ ```css
198
+ .my-page .responsiveTable {
199
+ --primary-color: #059669; /* emerald green */
200
+ }
201
+ ```
202
+
203
+ ### 5. Performance and Rendering
204
+
205
+ - **Memoize props passed to `ResponsiveTable`.** Arrays and objects created inline (`columnDefinitions={[...]}`, `animationProps={{...}}`) create new references on every render, triggering unnecessary downstream work.
206
+ - **Use `useTableContext` sparingly.** Custom components rendered inside the table should `useMemo` their outputs when deriving data from context.
207
+ - **Avoid large `data` arrays without pagination.** Client-side rendering of thousands of rows will degrade performance. Use `dataSource` for server-side pagination or implement windowing.
208
+ - **Keep per-row callbacks stable.** Functions passed inside `cellRenderer` that capture state should use `useCallback` to avoid re-creating React elements for every row.
209
+
210
+ ### 6. Data Fetching and Pagination
211
+
212
+ - **Prefer `dataSource` over manual pagination.** The table handles page tracking, `hasMore` detection, loading states, and error recovery automatically.
213
+ - **Return `{ items, totalCount }`** from your `dataSource` function for accurate `hasMore` detection. A plain array is also supported but `hasMore` is inferred from `pageSize`.
214
+ - **Always provide `selectionProps.rowIdKey` with `dataSource`.** Without it, expand and selection states reset every time a new page loads because indices no longer match.
215
+ - **Handle errors.** Provide `onDataSourceError` to show user-facing error messages. The table has a built-in retry button when the initial load fails and no data is available.
216
+
217
+ ### 7. Column Definitions
218
+
219
+ - **Use the function form** `(row) => IResponsiveTableColumnDefinition<TData>` when columns need row-dependent options (visibility, class names, or rendering logic that differs per row).
220
+ - **Use `getSortableValue` for sorting.** This tells the built-in sort plugin how to compare cells. Without it, sorting falls back to `displayLabel` comparison which is rarely correct.
221
+ - **Use `getFilterableValue` for client-side filtering.** Without it, the filter plugin skips the column entirely.
222
+
223
+ ---
224
+
225
+ ## Pitfalls
226
+
227
+ ### 1. Chevron Transform Override
228
+
229
+ **Problem:** Applying `transform` or `transition` in `expandChevronClassName`. The collapse animation rotates the chevron from `-90deg` to `0deg` using these exact properties. Overriding them breaks the rotation.
230
+
231
+ **Fix:** Override only cosmetic properties — `color`, `font-size`, `opacity`, `margin`, etc.:
232
+
233
+ ```css
234
+ /* Correct */
235
+ .myChevron {
236
+ color: var(--brand-color);
237
+ font-size: 1.5rem;
238
+ }
239
+
240
+ /* Wrong — breaks rotation */
241
+ .myChevron {
242
+ transform: scale(1.5);
243
+ transition: all 0.5s;
244
+ }
245
+ ```
246
+
247
+ ### 2. rowIndex as a Stable Identifier
248
+
249
+ **Problem:** Using `rowIndex` from `expandRowRenderer` or `cellRenderer` as a database key, cache key, or state identifier. `rowIndex` is the display-order position after sort and filter — it changes when the user re-orders or searches.
250
+
251
+ **Fix:** Use a stable field from the data object (`row.id`, `row.uuid`, etc.) for any cross-render correlation:
252
+
253
+ ```tsx
254
+ // Wrong — breaks after re-sort
255
+ expandRowRenderer={(row, rowIndex) => (
256
+ <DetailPanel row={row} cacheKey={rowIndex} />
257
+ )}
258
+
259
+ // Correct
260
+ expandRowRenderer={(row) => (
261
+ <DetailPanel row={row} cacheKey={row.id} />
262
+ )}
263
+ ```
264
+
265
+ ### 3. Missing rowIdKey on Sortable Tables
266
+
267
+ **Problem:** Omitting `selectionProps.rowIdKey` on a table with sorting, filtering, or `dataSource`. Expand state and selection state are tracked by array index. When the user re-sorts, indices shift and all open panels close.
268
+
269
+ **Fix:** Always provide `selectionProps` with a `rowIdKey` pointing to a stable unique field. If selection is not needed, use a no-op handler:
270
+
271
+ ```tsx
272
+ <ResponsiveTable
273
+ data={rows}
274
+ columnDefinitions={columns}
275
+ selectionProps={{
276
+ rowIdKey: 'id',
277
+ onSelectionChange: () => {}, // expand stability only
278
+ }}
279
+ expandRowRenderer={(row) => <Detail row={row} />}
280
+ />
281
+ ```
282
+
283
+ ### 4. Missing data-rt-ignore-row-click
284
+
285
+ **Problem:** Clicking a button or link inside a data row also fires `onRowClick`. This causes navigation, modals, or other side effects to fire unintentionally when the user tries to interact with a cell-level control.
286
+
287
+ **Fix:** Add `data-rt-ignore-row-click` to every interactive element inside a cell row when `onRowClick` is active:
288
+
289
+ ```tsx
290
+ <button data-rt-ignore-row-click onClick={...}>Edit</button>
291
+ <a data-rt-ignore-row-click href={`/detail/${row.id}`}>View</a>
292
+ ```
293
+
294
+ ### 5. Non-null expandRowRenderer for Empty Detail
295
+
296
+ **Problem:** The renderer always returns a `ReactNode` even when there is no meaningful detail content. Every row gets a toggle chevron, including rows where the panel would be empty or useless.
297
+
298
+ **Fix:** Return `null` for rows that have no detail content:
299
+
300
+ ```tsx
301
+ expandRowRenderer={(order) =>
302
+ order.lineItems.length > 0
303
+ ? <LineItems items={order.lineItems} />
304
+ : null // ← no toggle for orders without line items
305
+ }
306
+ ```
307
+
308
+ ### 6. Heavy Computation in expandRowRenderer
309
+
310
+ **Problem:** Performing expensive operations inside `expandRowRenderer` — API calls, heavy transforms, large array traversals. The renderer runs for every visible row on every render pass (sort, filter, page change, state update).
311
+
312
+ **Fix:** Move expensive work into the detail component, which only mounts on first expand:
313
+
314
+ ```tsx
315
+ // Wrong — fetch runs on every render for every row
316
+ expandRowRenderer={(row) => {
317
+ const data = useExpensiveQuery(row.id); // hooks in renderer is illegal anyway
318
+ return <Detail data={data} />;
319
+ }}
320
+
321
+ // Correct — fetch runs once, on first expand
322
+ expandRowRenderer={(row) => <DetailPage rowId={row.id} />}
323
+
324
+ function DetailPage({ rowId }: { rowId: string }) {
325
+ const { data } = useExpensiveQuery(rowId); // lazy-mounted
326
+ return <Detail data={data} />;
327
+ }
328
+ ```
329
+
330
+ ### 7. Inline Props Without useMemo
331
+
332
+ **Problem:** Passing arrays, objects, or callbacks as inline literals in JSX:
333
+
334
+ ```tsx
335
+ <ResponsiveTable
336
+ data={rows}
337
+ columnDefinitions={columns} // if this is a new array each render...
338
+ animationProps={{ animateOnLoad: true }} // ...or this is a new object
339
+ />
340
+ ```
341
+
342
+ Each render creates new references. The table's comparison logic detects changes and triggers unnecessary re-computation and re-renders.
343
+
344
+ **Fix:** Define stable references:
345
+
346
+ ```tsx
347
+ const columns = useMemo(() => [...], []);
348
+ const animProps = useMemo(() => ({ animateOnLoad: true }), []);
349
+
350
+ <ResponsiveTable
351
+ data={rows}
352
+ columnDefinitions={columns}
353
+ animationProps={animProps}
354
+ />
355
+ ```
356
+
357
+ ### 8. No Error Handling with dataSource
358
+
359
+ **Problem:** Using `dataSource` without listening for errors. If the fetch fails and there is already cached data, the table continues showing stale data silently. If the initial fetch fails with no cached data, the user sees an empty skeleton indefinitely.
360
+
361
+ **Fix:** Provide `onDataSourceError` for logging or toast notifications, and use the built-in retry button for initial load failures:
362
+
363
+ ```tsx
364
+ <ResponsiveTable
365
+ dataSource={fetchPage}
366
+ columnDefinitions={columns}
367
+ onDataSourceError={(error) => {
368
+ console.error('Failed to fetch page:', error);
369
+ toast.error('Could not load data. Please try again.');
370
+ }}
371
+ />
372
+ ```
373
+
374
+ ---
375
+
376
+ **Previous:** [Handling Interactive Elements](./handling-interactive-elements.md) | **Next:** [Return to README](../README.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jattac.libs.web.responsive-table",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "A fully responsive, customizable, and lightweight React table component with a modern, mobile-first design and a powerful plugin system.",
5
5
  "author": {
6
6
  "name": "Nyingi Maina",
@@ -1,13 +0,0 @@
1
- import React from 'react';
2
- import { IResponsiveTablePlugin, IPluginAPI } from './IResponsiveTablePlugin';
3
- export declare class InfiniteScrollPlugin<TData> implements IResponsiveTablePlugin<TData> {
4
- id: string;
5
- private api;
6
- private isLoadingMore;
7
- constructor();
8
- onPluginInit: (api: IPluginAPI<TData>) => void;
9
- private attachScrollListener;
10
- private handleScroll;
11
- processData: (data: TData[]) => TData[];
12
- renderFooter: () => string | number | true | Iterable<React.ReactNode> | React.JSX.Element | null;
13
- }
@@ -1,46 +0,0 @@
1
- import React, { ReactNode } from 'react';
2
- import { SortDirection } from '../Data/IResponsiveTableColumnDefinition';
3
- import IFooterRowDefinition from '../Data/IFooterRowDefinition';
4
- import { IResponsiveTablePlugin } from '../Plugins/IResponsiveTablePlugin';
5
- import { ColumnDefinition } from '../Context/TableContext';
6
- interface IInfiniteScrollProps<TData> {
7
- onLoadMore: (currentData: TData[]) => Promise<TData[] | null>;
8
- hasMore?: boolean;
9
- loadingMoreComponent?: ReactNode;
10
- noMoreDataComponent?: ReactNode;
11
- }
12
- interface ISortProps {
13
- initialSortColumn?: string;
14
- initialSortDirection?: SortDirection;
15
- }
16
- interface IProps<TData> {
17
- columnDefinitions: ColumnDefinition<TData>[];
18
- data: TData[];
19
- noDataComponent?: ReactNode;
20
- maxHeight?: string;
21
- onRowClick?: (item: TData) => void;
22
- footerRows?: IFooterRowDefinition[];
23
- mobileBreakpoint?: number;
24
- plugins?: IResponsiveTablePlugin<TData>[];
25
- enablePageLevelStickyHeader?: boolean;
26
- infiniteScrollProps?: IInfiniteScrollProps<TData>;
27
- filterProps?: {
28
- showFilter?: boolean;
29
- filterPlaceholder?: string;
30
- className?: string;
31
- };
32
- selectionProps?: {
33
- onSelectionChange: (selectedItems: TData[]) => void;
34
- rowIdKey: keyof TData;
35
- mode?: 'single' | 'multiple';
36
- selectedItems?: TData[];
37
- selectedRowClassName?: string;
38
- };
39
- animationProps?: {
40
- isLoading?: boolean;
41
- animateOnLoad?: boolean;
42
- };
43
- sortProps?: ISortProps;
44
- }
45
- declare function InfiniteTable<TData>(props: IProps<TData>): React.JSX.Element;
46
- export default InfiniteTable;