jattac.libs.web.responsive-table 0.2.13 → 0.2.15

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.
package/README.md CHANGED
@@ -1,637 +1,661 @@
1
- # ResponsiveTable: A Modern and Flexible React Table Component
2
-
3
- ResponsiveTable is a powerful, lightweight, and fully responsive React component for creating beautiful and functional tables. It’s designed to look great on any device, adapting from a traditional table layout on desktops to a clean, card-based view on mobile screens.
4
-
5
- ## Features
6
-
7
- - **Mobile-First Design**: Automatically switches to a card layout on smaller screens for optimal readability.
8
- - **Highly Customizable**: Tailor the look and feel of columns, headers, and footers.
9
- - **Dynamic Data Handling**: Define columns and footers based on your data or application state.
10
- - **Delightful Animations**: Includes an optional skeleton loader and staggered row entrance animations.
11
- - **Interactive Elements**: Easily add click handlers for rows, headers, and footer cells.
12
- - **Performant**: Built with performance in mind, including debounced resize handling.
13
- - **Easy to Use**: A simple and intuitive API for quick integration.
14
- - **Extensible Plugin System**: Easily add new functionalities like filtering, sorting, or infinite scrolling.
15
-
16
- ## Installation
17
-
18
- To get started, install the package from npm:
19
-
20
- ```bash
21
- npm install jattac.libs.web.responsive-table
22
- ```
23
-
24
- ## Getting Started
25
-
26
- Here’s a basic example to get you up and running in minutes.
27
-
28
- ```jsx
29
- import React from 'react';
30
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
31
-
32
- const GettingStarted = () => {
33
- const columns = [
34
- { displayLabel: 'Name', dataKey: 'name', cellRenderer: (row) => row.name },
35
- { displayLabel: 'Age', dataKey: 'age', cellRenderer: (row) => row.age },
36
- { displayLabel: 'City', dataKey: 'city', cellRenderer: (row) => row.city },
37
- ];
38
-
39
- const data = [
40
- { name: 'Alice', age: 32, city: 'New York' },
41
- { name: 'Bob', age: 28, city: 'Los Angeles' },
42
- { name: 'Charlie', age: 45, city: 'Chicago' },
43
- ];
44
-
45
- return <ResponsiveTable columnDefinitions={columns} data={data} />;
46
- };
47
-
48
- export default GettingStarted;
49
- ```
50
-
51
- This will render a table that automatically adapts to the screen size. On a desktop, it will look like a standard table, and on mobile, it will switch to a card-based layout.
52
-
53
- ---
54
-
55
- ## Comprehensive Examples
56
-
57
- ### Example 1: Loading State and Animations
58
-
59
- You can provide a seamless user experience by showing a skeleton loader while your data is being fetched, and then animating the rows in when the data is ready.
60
-
61
- ```jsx
62
- import React, { useState, useEffect } from 'react';
63
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
64
-
65
- const AnimatedTable = () => {
66
- const [data, setData] = useState([]);
67
- const [isLoading, setIsLoading] = useState(true);
68
-
69
- const columns = [
70
- { displayLabel: 'User', cellRenderer: (row) => row.name },
71
- { displayLabel: 'Email', cellRenderer: (row) => row.email },
72
- ];
73
-
74
- useEffect(() => {
75
- // Simulate a network request
76
- setTimeout(() => {
77
- setData([
78
- { name: 'Grace', email: 'grace@example.com' },
79
- { name: 'Henry', email: 'henry@example.com' },
80
- ]);
81
- setIsLoading(false);
82
- }, 2000);
83
- }, []);
84
-
85
- return (
86
- <ResponsiveTable columnDefinitions={columns} data={data} animationProps={{ isLoading, animateOnLoad: true }} />
87
- );
88
- };
89
- ```
90
-
91
- ### Example 2: Adding a Clickable Row Action
92
-
93
- You can make rows clickable to perform actions, such as navigating to a details page or opening a modal.
94
-
95
- ```jsx
96
- import React from 'react';
97
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
98
-
99
- const ClickableRows = () => {
100
- const columns = [
101
- { displayLabel: 'Product', cellRenderer: (row) => row.product },
102
- { displayLabel: 'Price', cellRenderer: (row) => `${row.price.toFixed(2)}` },
103
- ];
104
-
105
- const data = [
106
- { id: 1, product: 'Laptop', price: 1200 },
107
- { id: 2, product: 'Keyboard', price: 75 },
108
- ];
109
-
110
- const handleRowClick = (item) => {
111
- alert(`You clicked on product ID: ${item.id}`);
112
- };
113
-
114
- return <ResponsiveTable columnDefinitions={columns} data={data} onRowClick={handleRowClick} />;
115
- };
116
- ```
117
-
118
- ### Example 3: Custom Cell Rendering
119
-
120
- You can render any React component inside a cell, allowing for rich content like buttons, links, or status badges.
121
-
122
- ```jsx
123
- import React from 'react';
124
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
125
-
126
- const CustomCells = () => {
127
- const columns = [
128
- { displayLabel: <strong>User</strong>, cellRenderer: (row) => <strong>{row.user}</strong> },
129
- {
130
- displayLabel: 'Status',
131
- cellRenderer: (row) => (
132
- <span
133
- style={{
134
- color: row.status === 'Active' ? 'green' : 'red',
135
- fontWeight: 'bold',
136
- }}
137
- >
138
- {row.status}
139
- </span>
140
- ),
141
- },
142
- {
143
- displayLabel: 'Action',
144
- cellRenderer: (row) => <button onClick={() => alert(`Editing ${row.user}`)}>Edit</button>,
145
- },
146
- ];
147
-
148
- const data = [
149
- { user: 'Eve', status: 'Active' },
150
- { user: 'Frank', status: 'Inactive' },
151
- ];
152
-
153
- return <ResponsiveTable columnDefinitions={columns} data={data} />;
154
- };
155
- ```
156
-
157
- ### Example 4: Dynamic and Conditional Columns
158
-
159
- Columns can be generated dynamically based on your data or application state. This is useful for creating flexible tables that adapt to different datasets.
160
-
161
- ```jsx
162
- import React from 'react';
163
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
164
-
165
- const DynamicColumns = ({ isAdmin }) => {
166
- // Base columns for all users
167
- const columns = [
168
- { displayLabel: 'File Name', cellRenderer: (row) => row.fileName },
169
- { displayLabel: 'Size', cellRenderer: (row) => `${row.size} KB` },
170
- ];
171
-
172
- // Add an admin-only column conditionally
173
- if (isAdmin) {
174
- columns.push({
175
- displayLabel: 'Admin Actions',
176
- cellRenderer: (row) => <button onClick={() => alert(`Deleting ${row.fileName}`)}>Delete</button>,
177
- });
178
- }
179
-
180
- const data = [
181
- { fileName: 'document.pdf', size: 1024 },
182
- { fileName: 'image.jpg', size: 512 },
183
- ];
184
-
185
- return <ResponsiveTable columnDefinitions={columns} data={data} />;
186
- };
187
- ```
188
-
189
- ### Example 5: Advanced Footer with Labels and Interactivity
190
-
191
- You can add a footer to display summary information, such as totals or averages. The footer is also responsive and will appear correctly in both desktop and mobile views. With the enhanced footer functionality, you can provide explicit labels for mobile view and add click handlers to footer cells.
192
-
193
- ```jsx
194
- import React from 'react';
195
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
196
-
197
- const TableWithFooter = () => {
198
- const columns = [
199
- { displayLabel: 'Item', cellRenderer: (row) => row.item },
200
- { displayLabel: 'Quantity', cellRenderer: (row) => row.quantity },
201
- { displayLabel: 'Price', cellRenderer: (row) => `${row.price.toFixed(2)}` },
202
- ];
203
-
204
- const data = [
205
- { item: 'Apples', quantity: 10, price: 1.5 },
206
- { item: 'Oranges', quantity: 5, price: 2.0 },
207
- { item: 'Bananas', quantity: 15, price: 0.5 },
208
- ];
209
-
210
- const total = data.reduce((sum, row) => sum + row.quantity * row.price, 0);
211
-
212
- const footerRows = [
213
- {
214
- columns: [
215
- {
216
- colSpan: 2,
217
- cellRenderer: () => <strong>Total:</strong>,
218
- },
219
- {
220
- colSpan: 1,
221
- displayLabel: 'Total',
222
- cellRenderer: () => <strong>${total.toFixed(2)}</strong>,
223
- onCellClick: () => alert('Total clicked!'),
224
- },
225
- ],
226
- },
227
- ];
228
-
229
- return <ResponsiveTable columnDefinitions={columns} data={data} footerRows={footerRows} />;
230
- };
231
- ```
232
-
233
- ### Example 6: Disabling the Page-Level Sticky Header (ELI5)
234
-
235
- **Explain Like I'm 5:** Imagine you have a super long grocery list on a piece of paper (the webpage). The titles of the columns are "Item", "Quantity", and "Price" (the table header).
236
-
237
- Normally, as you slide the paper up to see items at the bottom, the titles disappear off the top.
238
-
239
- This table has a special power: by default, the header "sticks" to the top of your view so you never forget which column is which.
240
-
241
- But what if you don't want it to stick? The `enablePageLevelStickyHeader={false}` prop is like a magic switch. Flipping it to `false` tells the header to scroll away normally with the rest of the page.
242
-
243
- ```jsx
244
- import React from 'react';
245
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
246
-
247
- const NonStickyHeaderTable = () => {
248
- // We need enough data to make the page scroll
249
- const data = Array.from({ length: 50 }, (_, i) => ({
250
- id: i + 1,
251
- item: `Item #${i + 1}`,
252
- description: 'This is a sample item.',
253
- }));
254
-
255
- const columns = [
256
- { displayLabel: 'ID', cellRenderer: (row) => row.id },
257
- { displayLabel: 'Item', cellRenderer: (row) => row.item },
258
- { displayLabel: 'Description', cellRenderer: (row) => row.description },
259
- ];
260
-
261
- return (
262
- <div>
263
- <h1 style={{ height: '50vh', display: 'flex', alignItems: 'center' }}>Scroll down to see the table</h1>
264
- <ResponsiveTable
265
- columnDefinitions={columns}
266
- data={data}
267
- enablePageLevelStickyHeader={false} // <-- Here's the magic switch!
268
- />
269
- <div style={{ height: '50vh' }} />
270
- </div>
271
- );
272
- };
273
- ```
274
-
275
- ---
276
-
277
- ## Plugin System
278
-
279
- ResponsiveTable is designed with an extensible plugin system, allowing developers to easily add new functionalities or modify existing behaviors without altering the core component. Plugins can interact with the table's data, render custom UI elements (like headers or footers), and respond to table events.
280
-
281
- ### How to Use Plugins
282
-
283
- Plugins are passed to the `ResponsiveTable` component via the `plugins` prop, which accepts an array of `IResponsiveTablePlugin` instances. Some common functionalities, like filtering and infinite scrolling, are provided as built-in plugins that can be enabled via specific props (`filterProps` and `infiniteScrollProps`). When these props are used, the corresponding built-in plugins are automatically initialized if not already provided in the `plugins` array.
284
-
285
- ```jsx
286
- import React from 'react';
287
- // Note: All plugins are exported from the main package entry point.
288
- import ResponsiveTable, { FilterPlugin } from 'jattac.libs.web.responsive-table';
289
-
290
- const MyTableWithPlugins = () => {
291
- const columns = [
292
- { displayLabel: 'Name', dataKey: 'name', cellRenderer: (row) => row.name, getFilterableValue: (row) => row.name },
293
- {
294
- displayLabel: 'Age',
295
- dataKey: 'age',
296
- cellRenderer: (row) => row.age,
297
- getFilterableValue: (row) => row.age.toString(),
298
- },
299
- ];
300
-
301
- const data = [
302
- { name: 'Alice', age: 32 },
303
- { name: 'Bob', age: 28 },
304
- { name: 'Charlie', age: 45 },
305
- ];
306
-
307
- return (
308
- <ResponsiveTable
309
- columnDefinitions={columns}
310
- data={data}
311
- // Enable built-in filter plugin via props
312
- filterProps={{ showFilter: true, filterPlaceholder: 'Search by name or age...' }}
313
- // Or provide a custom instance of the plugin
314
- // plugins={[new FilterPlugin()]}
315
- />
316
- );
317
- };
318
- ```
319
-
320
- ### Built-in Plugins
321
-
322
- #### `SortPlugin`
323
-
324
- The `SortPlugin` provides powerful, type-safe, and highly customizable column sorting. It adds intuitive UI cues, allowing users to click column headers to sort the data in ascending, descending, or original order.
325
-
326
- **Enabling the `SortPlugin`:**
327
-
328
- To use the plugin, you must first import it and provide a generic instance of it to the `plugins` prop. The real power comes from making the plugin instance generic with your data type, which provides type-safety and IDE autocompletion for the sort comparer helpers.
329
-
330
- ```jsx
331
- import React from 'react';
332
- import ResponsiveTable, { IResponsiveTableColumnDefinition, SortPlugin } from 'jattac.libs.web.responsive-table';
333
-
334
- // Define the shape of your data
335
- interface User {
336
- id: number;
337
- name: string;
338
- signupDate: string;
339
- logins: number;
340
- }
341
-
342
- // 1. Create a single, generic instance of the plugin.
343
- // This is the ONLY setup step required.
344
- const sortPlugin = new SortPlugin<User>({
345
- initialSortColumn: 'logins',
346
- initialSortDirection: 'desc',
347
- });
348
-
349
- // 2. Define the columns, using the helpers directly from the plugin instance.
350
- const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
351
- // ... see examples below
352
- ];
353
-
354
- const UserTable = ({ users }) => (
355
- <ResponsiveTable
356
- columnDefinitions={columnDefinitions}
357
- data={users}
358
- // 3. Pass the already-configured plugin to the table.
359
- plugins={[sortPlugin]}
360
- />
361
- );
362
- ```
363
-
364
- **How to Make Columns Sortable (Opt-In):**
365
-
366
- A column is made sortable by adding either a `sortComparer` or a `getSortableValue` property to its definition.
367
-
368
- - `sortComparer`: A function that defines the exact comparison logic. This is the most powerful option and should be used for complex data types or custom logic.
369
- - `getSortableValue`: A simpler function that just returns the primitive value (string, number, etc.) to be used in a default comparison.
370
-
371
- **Example 1: Using Type-Safe Comparer Helpers**
372
-
373
- The `SortPlugin` instance provides a `comparers` object with pre-built, type-safe helper functions to eliminate boilerplate for common sorting scenarios. This is the recommended approach.
374
-
375
- ```jsx
376
- const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
377
- {
378
- displayLabel: 'Name',
379
- dataKey: 'name',
380
- cellRenderer: (user) => user.name,
381
- // The plugin instance itself provides the type-safe helpers.
382
- // The string 'name' is fully type-checked against the User interface.
383
- sortComparer: sortPlugin.comparers.caseInsensitiveString('name'),
384
- },
385
- {
386
- displayLabel: 'Signup Date',
387
- dataKey: 'signupDate',
388
- cellRenderer: (user) => new Date(user.signupDate).toLocaleDateString(),
389
- // IDE autocompletion for 'signupDate' works perfectly.
390
- sortComparer: sortPlugin.comparers.date('signupDate'),
391
- },
392
- {
393
- displayLabel: 'Logins',
394
- dataKey: 'logins',
395
- cellRenderer: (user) => user.logins,
396
- sortComparer: sortPlugin.comparers.numeric('logins'),
397
- },
398
- {
399
- displayLabel: 'Actions',
400
- // This column is NOT sortable because it has no sort-related properties.
401
- cellRenderer: (user) => <button>View</button>,
402
- },
403
- ];
404
- ```
405
-
406
- **Example 2: Writing a Custom `sortComparer`**
407
-
408
- For unique requirements, you can write your own comparison function from scratch.
409
-
410
- ```jsx
411
- const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
412
- {
413
- displayLabel: 'Name',
414
- dataKey: 'name',
415
- cellRenderer: (user) => user.name,
416
- // Writing custom logic for a case-sensitive sort
417
- sortComparer: (a, b, direction) => {
418
- const nameA = a.name; // No .toLowerCase()
419
- const nameB = b.name;
420
- if (nameA < nameB) return direction === 'asc' ? -1 : 1;
421
- if (nameA > nameB) return direction === 'asc' ? 1 : -1;
422
- return 0;
423
- },
424
- },
425
- ];
426
- ```
427
-
428
- **Example 3: Using `getSortableValue` for Simple Cases**
429
-
430
- If you don't need special logic, `getSortableValue` is a concise way to enable default sorting on a property.
431
-
432
- ```jsx
433
- const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
434
- {
435
- displayLabel: 'Logins',
436
- dataKey: 'logins',
437
- cellRenderer: (user) => user.logins,
438
- // This enables a simple, default numerical sort on the 'logins' property.
439
- getSortableValue: (user) => user.logins,
440
- },
441
- ];
442
- ```
443
-
444
- **Plugin Options (via `new SortPlugin(options)`):**
445
-
446
- | Prop | Type (`keyof TData`) | Description |
447
- | ---------------------- | -------------------- | ------------------------------------------------- |
448
- | `initialSortColumn` | `string` | The `dataKey` of the column to sort by initially. |
449
- | `initialSortDirection` | `'asc' \| 'desc'` | The direction for the initial sort. |
450
-
451
- **`SortPlugin.comparers` API:**
452
-
453
- The `comparers` object on your `SortPlugin` instance provides the following helper methods. Each method is a factory that takes a `dataKey` (which is type-checked against your data model) and returns a `sortComparer` function.
454
-
455
- | Method | Description |
456
- | -------------------------------- | ----------------------------------------------------------------------------- |
457
- | `numeric(dataKey)` | Performs a standard numerical sort. |
458
- | `caseInsensitiveString(dataKey)` | Performs a case-insensitive alphabetical sort. |
459
- | `date(dataKey)` | Correctly sorts dates, assuming the data is a valid date string or timestamp. |
460
-
461
- #### `FilterPlugin`
462
-
463
- Provides a search input to filter table data. It can be enabled by setting `filterProps.showFilter` to `true` on the `ResponsiveTable` component. For columns to be filterable, you must provide a `getFilterableValue` function in their `IResponsiveTableColumnDefinition`.
464
-
465
- **Props for `FilterPlugin` (via `filterProps` on `ResponsiveTable`):**
466
-
467
- | Prop | Type | Description |
468
- | ------------------- | --------- | --------------------------------------------------------------- |
469
- | `showFilter` | `boolean` | If `true`, displays a filter input field above the table. |
470
- | `filterPlaceholder` | `string` | Placeholder text for the filter input. Defaults to "Search...". |
471
-
472
- **Example with `FilterPlugin`:**
473
-
474
- ```jsx
475
- import React from 'react';
476
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
477
-
478
- const FilterableTable = () => {
479
- const initialData = [
480
- { id: 1, name: 'Alice', email: 'alice@example.com' },
481
- { id: 2, name: 'Bob', email: 'bob@example.com' },
482
- { id: 3, name: 'Charlie', email: 'charlie@example.com' },
483
- { id: 4, name: 'David', email: 'david@example.com' },
484
- ];
485
-
486
- const columns = [
487
- { displayLabel: 'ID', cellRenderer: (row) => row.id, getFilterableValue: (row) => row.id.toString() },
488
- { displayLabel: 'Name', cellRenderer: (row) => row.name, getFilterableValue: (row) => row.name },
489
- { displayLabel: 'Email', cellRenderer: (row) => row.email, getFilterableValue: (row) => row.email },
490
- ];
491
-
492
- return (
493
- <ResponsiveTable
494
- columnDefinitions={columns}
495
- data={initialData}
496
- filterProps={{ showFilter: true, filterPlaceholder: 'Filter users...' }}
497
- />
498
- );
499
- };
500
- ```
501
-
502
- #### `InfiniteScrollPlugin`
503
-
504
- Enables infinite scrolling for the table, loading more data as the user scrolls to the bottom. This plugin requires the `maxHeight` prop to be set on the `ResponsiveTable` to define a scrollable area.
505
-
506
- **Props for `InfiniteScrollPlugin` (via `infiniteScrollProps` on `ResponsiveTable`):**
507
-
508
- | Prop | Type | Description |
509
- | ---------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
510
- | `enableInfiniteScroll` | `boolean` | If `true`, enables infinite scrolling. |
511
- | `onLoadMore` | `(currentData: TData[]) => Promise<TData[] | null>` | Callback function to load more data. Should return a Promise resolving to new data or `null`. |
512
- | `hasMore` | `boolean` | Indicates if there is more data to load. |
513
- | `loadingMoreComponent` | `ReactNode` | Custom component to display while loading more data. Defaults to "Loading more...". |
514
- | `noMoreDataComponent` | `ReactNode` | Custom component to display when no more data is available. Defaults to "No more data.". |
515
-
516
- **Example with `InfiniteScrollPlugin`:**
517
-
518
- ```jsx
519
- import React, { useState, useEffect } from 'react';
520
- import ResponsiveTable from 'jattac.libs.web.responsive-table';
521
-
522
- const InfiniteScrollTable = () => {
523
- const [data, setData] = useState([]);
524
- const [hasMore, setHasMore] = useState(true);
525
- const [page, setPage] = useState(0);
526
-
527
- const fetchData = async (currentPage) => {
528
- // Simulate API call
529
- return new Promise((resolve) => {
530
- setTimeout(() => {
531
- const newData = [];
532
- for (let i = 0; i < 10; i++) {
533
- newData.push({ id: currentPage * 10 + i, value: `Item ${currentPage * 10 + i}` });
534
- }
535
- resolve(newData);
536
- }, 500);
537
- });
538
- };
539
-
540
- useEffect(() => {
541
- fetchData(0).then((initialData) => {
542
- setData(initialData);
543
- setPage(1);
544
- });
545
- }, []);
546
-
547
- const onLoadMore = async (currentData) => {
548
- if (!hasMore) return null;
549
-
550
- const newItems = await fetchData(page);
551
- if (newItems.length === 0) {
552
- setHasMore(false);
553
- return null;
554
- }
555
- setData((prevData) => [...prevData, ...newItems]);
556
- setPage((prevPage) => prevPage + 1);
557
- return newItems; // Return new items for the plugin to know data was loaded
558
- };
559
-
560
- const columns = [
561
- { displayLabel: 'ID', cellRenderer: (row) => row.id },
562
- { displayLabel: 'Value', cellRenderer: (row) => row.value },
563
- ];
564
-
565
- return (
566
- <div style={{ height: '300px' }}>
567
- {' '}
568
- {/* Container for scrollable table */}
569
- <ResponsiveTable
570
- columnDefinitions={columns}
571
- data={data}
572
- maxHeight="100%"
573
- infiniteScrollProps={{
574
- enableInfiniteScroll: true,
575
- onLoadMore: onLoadMore,
576
- hasMore: hasMore,
577
- loadingMoreComponent: <div>Loading more items...</div>,
578
- noMoreDataComponent: <div>All items loaded.</div>,
579
- }}
580
- />
581
- </div>
582
- );
583
- };
584
- ```
585
-
586
- ---
587
-
588
- ## API Reference
589
-
590
- ### `ResponsiveTable` Props
591
-
592
- | Prop | Type | Required | Description |
593
- | ----------------------------- | ------------------------------------ | -------- | --------------------------------------------------------------------------------------------------- |
594
- | `columnDefinitions` | `IResponsiveTableColumnDefinition[]` | Yes | An array of objects defining the table columns. |
595
- | `data` | `TData[]` | Yes | An array of data objects to populate the table rows. |
596
- | `footerRows` | `IFooterRowDefinition[]` | No | An array of objects defining the table footer. |
597
- | `onRowClick` | `(item: TData) => void` | No | A callback function that is triggered when a row is clicked. |
598
- | `noDataComponent` | `ReactNode` | No | A custom component to display when there is no data. |
599
- | `maxHeight` | `string` | No | Sets a maximum height for the table body, making it scrollable. |
600
- | `mobileBreakpoint` | `number` | No | The pixel width at which the table switches to the mobile view. Defaults to `600`. |
601
- | `enablePageLevelStickyHeader` | `boolean` | No | If `false`, disables the header from sticking to the top of the page on scroll. Defaults to `true`. |
602
- | `plugins` | `IResponsiveTablePlugin<TData>[]` | No | An array of plugin instances to extend table functionality. |
603
- | `infiniteScrollProps` | `object` | No | Configuration for the built-in infinite scroll plugin. |
604
- | `filterProps` | `object` | No | Configuration for the built-in filter plugin. |
605
- | `animationProps` | `object` | No | Configuration for animations, including `isLoading` and `animateOnLoad`. |
606
-
607
- ### `IResponsiveTableColumnDefinition<TData>`
608
-
609
- | Property | Type | Required | Description |
610
- | -------------------- | ------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------------- |
611
- | `displayLabel` | `ReactNode` | Yes | The label displayed in the table header (can be a string or any React component). |
612
- | `cellRenderer` | `(row: TData) => ReactNode` | Yes | A function that returns the content to be rendered in the cell. |
613
- | `dataKey` | `string` | No | A key to match the column to a property in the data object (required for sorting). |
614
- | `interactivity` | `object` | No | An object to define header interactivity (`onHeaderClick`, `id`, `className`). |
615
- | `getFilterableValue` | `(row: TData) => string \| number` | No | A function that returns the string or number value to be used for filtering this column. |
616
- | `getSortableValue` | `(row: TData) => any` | No | A function that returns a primitive value from a row to be used for default sorting. |
617
- | `sortComparer` | `(a: TData, b: TData, direction: 'asc' \| 'desc') => number` | No | A function that provides the precise comparison logic for sorting a column. |
618
-
619
- ### `IFooterRowDefinition`
620
-
621
- | Property | Type | Required | Description |
622
- | --------- | --------------------------- | -------- | -------------------------------------------------- |
623
- | `columns` | `IFooterColumnDefinition[]` | Yes | An array of column definitions for the footer row. |
624
-
625
- ### `IFooterColumnDefinition`
626
-
627
- | Property | Type | Required | Description |
628
- | -------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
629
- | `colSpan` | `number` | Yes | The number of columns the footer cell should span. |
630
- | `cellRenderer` | `() => ReactNode` | Yes | A function that returns the content for the footer cell. |
631
- | `displayLabel` | `ReactNode` | No | An optional, explicit label for the footer cell. In mobile view, if `colSpan` is 1 and this is not provided, the corresponding column header will be used as a fallback. This is required for `colSpan` > 1 if you want a label to be displayed. |
632
- | `onCellClick` | `() => void` | No | An optional click handler for the footer cell. |
633
- | `className` | `string` | No | Optional class name for custom styling of the footer cell. |
634
-
635
- ## License
636
-
637
- This project is licensed under the MIT License.
1
+ # ResponsiveTable: A Modern and Flexible React Table Component
2
+
3
+ ResponsiveTable is a powerful, lightweight, and fully responsive React component for creating beautiful and functional tables. It’s designed to look great on any device, adapting from a traditional table layout on desktops to a clean, card-based view on mobile screens.
4
+
5
+ ## Features
6
+
7
+ - **Mobile-First Design**: Automatically switches to a card layout on smaller screens for optimal readability.
8
+ - **Highly Customizable**: Tailor the look and feel of columns, headers, and footers.
9
+ - **Dynamic Data Handling**: Define columns and footers based on your data or application state.
10
+ - **Delightful Animations**: Includes an optional skeleton loader and staggered row entrance animations.
11
+ - **Interactive Elements**: Easily add click handlers for rows, headers, and footer cells.
12
+ - **Performant**: Built with performance in mind, including debounced resize handling.
13
+ - **Easy to Use**: A simple and intuitive API for quick integration.
14
+ - **Extensible Plugin System**: Easily add new functionalities like filtering, sorting, or infinite scrolling.
15
+
16
+ ## Installation
17
+
18
+ To get started, install the package from npm:
19
+
20
+ ```bash
21
+ npm install jattac.libs.web.responsive-table
22
+ ```
23
+
24
+ ## Getting Started
25
+
26
+ Here’s a basic example to get you up and running in minutes.
27
+
28
+ ```jsx
29
+ import React from 'react';
30
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
31
+
32
+ const GettingStarted = () => {
33
+ const columns = [
34
+ { displayLabel: 'Name', dataKey: 'name', cellRenderer: (row) => row.name },
35
+ { displayLabel: 'Age', dataKey: 'age', cellRenderer: (row) => row.age },
36
+ { displayLabel: 'City', dataKey: 'city', cellRenderer: (row) => row.city },
37
+ ];
38
+
39
+ const data = [
40
+ { name: 'Alice', age: 32, city: 'New York' },
41
+ { name: 'Bob', age: 28, city: 'Los Angeles' },
42
+ { name: 'Charlie', age: 45, city: 'Chicago' },
43
+ ];
44
+
45
+ return <ResponsiveTable columnDefinitions={columns} data={data} />;
46
+ };
47
+
48
+ export default GettingStarted;
49
+ ```
50
+
51
+ This will render a table that automatically adapts to the screen size. On a desktop, it will look like a standard table, and on mobile, it will switch to a card-based layout.
52
+
53
+ ---
54
+
55
+ ## Comprehensive Examples
56
+
57
+ ### Example 1: Loading State and Animations
58
+
59
+ You can provide a seamless user experience by showing a skeleton loader while your data is being fetched, and then animating the rows in when the data is ready.
60
+
61
+ ```jsx
62
+ import React, { useState, useEffect } from 'react';
63
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
64
+
65
+ const AnimatedTable = () => {
66
+ const [data, setData] = useState([]);
67
+ const [isLoading, setIsLoading] = useState(true);
68
+
69
+ const columns = [
70
+ { displayLabel: 'User', cellRenderer: (row) => row.name },
71
+ { displayLabel: 'Email', cellRenderer: (row) => row.email },
72
+ ];
73
+
74
+ useEffect(() => {
75
+ // Simulate a network request
76
+ setTimeout(() => {
77
+ setData([
78
+ { name: 'Grace', email: 'grace@example.com' },
79
+ { name: 'Henry', email: 'henry@example.com' },
80
+ ]);
81
+ setIsLoading(false);
82
+ }, 2000);
83
+ }, []);
84
+
85
+ return (
86
+ <ResponsiveTable columnDefinitions={columns} data={data} animationProps={{ isLoading, animateOnLoad: true }} />
87
+ );
88
+ };
89
+ ```
90
+
91
+ ### Example 2: Adding a Clickable Row Action
92
+
93
+ You can make rows clickable to perform actions, such as navigating to a details page or opening a modal.
94
+
95
+ ```jsx
96
+ import React from 'react';
97
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
98
+
99
+ const ClickableRows = () => {
100
+ const columns = [
101
+ { displayLabel: 'Product', cellRenderer: (row) => row.product },
102
+ { displayLabel: 'Price', cellRenderer: (row) => `${row.price.toFixed(2)}` },
103
+ ];
104
+
105
+ const data = [
106
+ { id: 1, product: 'Laptop', price: 1200 },
107
+ { id: 2, product: 'Keyboard', price: 75 },
108
+ ];
109
+
110
+ const handleRowClick = (item) => {
111
+ alert(`You clicked on product ID: ${item.id}`);
112
+ };
113
+
114
+ return <ResponsiveTable columnDefinitions={columns} data={data} onRowClick={handleRowClick} />;
115
+ };
116
+ ```
117
+
118
+ ### Example 3: Custom Cell Rendering
119
+
120
+ You can render any React component inside a cell, allowing for rich content like buttons, links, or status badges.
121
+
122
+ ```jsx
123
+ import React from 'react';
124
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
125
+
126
+ const CustomCells = () => {
127
+ const columns = [
128
+ { displayLabel: <strong>User</strong>, cellRenderer: (row) => <strong>{row.user}</strong> },
129
+ {
130
+ displayLabel: 'Status',
131
+ cellRenderer: (row) => (
132
+ <span
133
+ style={{
134
+ color: row.status === 'Active' ? 'green' : 'red',
135
+ fontWeight: 'bold',
136
+ }}
137
+ >
138
+ {row.status}
139
+ </span>
140
+ ),
141
+ },
142
+ {
143
+ displayLabel: 'Action',
144
+ cellRenderer: (row) => <button onClick={() => alert(`Editing ${row.user}`)}>Edit</button>,
145
+ },
146
+ ];
147
+
148
+ const data = [
149
+ { user: 'Eve', status: 'Active' },
150
+ { user: 'Frank', status: 'Inactive' },
151
+ ];
152
+
153
+ return <ResponsiveTable columnDefinitions={columns} data={data} />;
154
+ };
155
+ ```
156
+
157
+ ### Example 4: Dynamic and Conditional Columns
158
+
159
+ Columns can be generated dynamically based on your data or application state. This is useful for creating flexible tables that adapt to different datasets.
160
+
161
+ ```jsx
162
+ import React from 'react';
163
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
164
+
165
+ const DynamicColumns = ({ isAdmin }) => {
166
+ // Base columns for all users
167
+ const columns = [
168
+ { displayLabel: 'File Name', cellRenderer: (row) => row.fileName },
169
+ { displayLabel: 'Size', cellRenderer: (row) => `${row.size} KB` },
170
+ ];
171
+
172
+ // Add an admin-only column conditionally
173
+ if (isAdmin) {
174
+ columns.push({
175
+ displayLabel: 'Admin Actions',
176
+ cellRenderer: (row) => <button onClick={() => alert(`Deleting ${row.fileName}`)}>Delete</button>,
177
+ });
178
+ }
179
+
180
+ const data = [
181
+ { fileName: 'document.pdf', size: 1024 },
182
+ { fileName: 'image.jpg', size: 512 },
183
+ ];
184
+
185
+ return <ResponsiveTable columnDefinitions={columns} data={data} />;
186
+ };
187
+ ```
188
+
189
+ ### Example 5: Advanced Footer with Labels and Interactivity
190
+
191
+ You can add a footer to display summary information, such as totals or averages. The footer is also responsive and will appear correctly in both desktop and mobile views. With the enhanced footer functionality, you can provide explicit labels for mobile view and add click handlers to footer cells.
192
+
193
+ ```jsx
194
+ import React from 'react';
195
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
196
+
197
+ const TableWithFooter = () => {
198
+ const columns = [
199
+ { displayLabel: 'Item', cellRenderer: (row) => row.item },
200
+ { displayLabel: 'Quantity', cellRenderer: (row) => row.quantity },
201
+ { displayLabel: 'Price', cellRenderer: (row) => `${row.price.toFixed(2)}` },
202
+ ];
203
+
204
+ const data = [
205
+ { item: 'Apples', quantity: 10, price: 1.5 },
206
+ { item: 'Oranges', quantity: 5, price: 2.0 },
207
+ { item: 'Bananas', quantity: 15, price: 0.5 },
208
+ ];
209
+
210
+ const total = data.reduce((sum, row) => sum + row.quantity * row.price, 0);
211
+
212
+ const footerRows = [
213
+ {
214
+ columns: [
215
+ {
216
+ colSpan: 2,
217
+ cellRenderer: () => <strong>Total:</strong>,
218
+ },
219
+ {
220
+ colSpan: 1,
221
+ displayLabel: 'Total',
222
+ cellRenderer: () => <strong>${total.toFixed(2)}</strong>,
223
+ onCellClick: () => alert('Total clicked!'),
224
+ },
225
+ ],
226
+ },
227
+ ];
228
+
229
+ return <ResponsiveTable columnDefinitions={columns} data={data} footerRows={footerRows} />;
230
+ };
231
+ ```
232
+
233
+ ### Example 6: Disabling the Page-Level Sticky Header (ELI5)
234
+
235
+ **Explain Like I'm 5:** Imagine you have a super long grocery list on a piece of paper (the webpage). The titles of the columns are "Item", "Quantity", and "Price" (the table header).
236
+
237
+ Normally, as you slide the paper up to see items at the bottom, the titles disappear off the top.
238
+
239
+ This table has a special power: by default, the header "sticks" to the top of your view so you never forget which column is which.
240
+
241
+ But what if you don't want it to stick? The `enablePageLevelStickyHeader={false}` prop is like a magic switch. Flipping it to `false` tells the header to scroll away normally with the rest of the page.
242
+
243
+ ```jsx
244
+ import React from 'react';
245
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
246
+
247
+ const NonStickyHeaderTable = () => {
248
+ // We need enough data to make the page scroll
249
+ const data = Array.from({ length: 50 }, (_, i) => ({
250
+ id: i + 1,
251
+ item: `Item #${i + 1}`,
252
+ description: 'This is a sample item.',
253
+ }));
254
+
255
+ const columns = [
256
+ { displayLabel: 'ID', cellRenderer: (row) => row.id },
257
+ { displayLabel: 'Item', cellRenderer: (row) => row.item },
258
+ { displayLabel: 'Description', cellRenderer: (row) => row.description },
259
+ ];
260
+
261
+ return (
262
+ <div>
263
+ <h1 style={{ height: '50vh', display: 'flex', alignItems: 'center' }}>Scroll down to see the table</h1>
264
+ <ResponsiveTable
265
+ columnDefinitions={columns}
266
+ data={data}
267
+ enablePageLevelStickyHeader={false} // <-- Here's the magic switch!
268
+ />
269
+ <div style={{ height: '50vh' }} />
270
+ </div>
271
+ );
272
+ };
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Plugin System
278
+
279
+ ResponsiveTable is designed with an extensible plugin system, allowing developers to easily add new functionalities or modify existing behaviors without altering the core component. Plugins can interact with the table's data, render custom UI elements (like headers or footers), and respond to table events.
280
+
281
+ ### How to Use Plugins
282
+
283
+ Plugins are passed to the `ResponsiveTable` component via the `plugins` prop, which accepts an array of `IResponsiveTablePlugin` instances. Some common functionalities, like filtering and infinite scrolling, are provided as built-in plugins that can be enabled via specific props (`filterProps` and `infiniteScrollProps`). When these props are used, the corresponding built-in plugins are automatically initialized if not already provided in the `plugins` array.
284
+
285
+ ```jsx
286
+ import React from 'react';
287
+ // Note: All plugins are exported from the main package entry point.
288
+ import ResponsiveTable, { FilterPlugin } from 'jattac.libs.web.responsive-table';
289
+
290
+ const MyTableWithPlugins = () => {
291
+ const columns = [
292
+ { displayLabel: 'Name', dataKey: 'name', cellRenderer: (row) => row.name, getFilterableValue: (row) => row.name },
293
+ {
294
+ displayLabel: 'Age',
295
+ dataKey: 'age',
296
+ cellRenderer: (row) => row.age,
297
+ getFilterableValue: (row) => row.age.toString(),
298
+ },
299
+ ];
300
+
301
+ const data = [
302
+ { name: 'Alice', age: 32 },
303
+ { name: 'Bob', age: 28 },
304
+ { name: 'Charlie', age: 45 },
305
+ ];
306
+
307
+ return (
308
+ <ResponsiveTable
309
+ columnDefinitions={columns}
310
+ data={data}
311
+ // Enable built-in filter plugin via props
312
+ filterProps={{ showFilter: true, filterPlaceholder: 'Search by name or age...' }}
313
+ // Or provide a custom instance of the plugin
314
+ // plugins={[new FilterPlugin()]}
315
+ />
316
+ );
317
+ };
318
+ ```
319
+
320
+ ### Built-in Plugins
321
+
322
+ #### `SortPlugin`
323
+
324
+ The `SortPlugin` provides powerful, type-safe, and highly customizable column sorting. It adds intuitive UI cues, allowing users to click column headers to sort the data in ascending, descending, or original order.
325
+
326
+ **Enabling the `SortPlugin`:**
327
+
328
+ To use the plugin, you must first import it and provide a generic instance of it to the `plugins` prop. The real power comes from making the plugin instance generic with your data type, which provides type-safety and IDE autocompletion for the sort comparer helpers.
329
+
330
+ ```jsx
331
+ import React from 'react';
332
+ import ResponsiveTable, { IResponsiveTableColumnDefinition, SortPlugin } from 'jattac.libs.web.responsive-table';
333
+
334
+ // Define the shape of your data
335
+ interface User {
336
+ id: number;
337
+ name: string;
338
+ signupDate: string;
339
+ logins: number;
340
+ }
341
+
342
+ // 1. Create a single, generic instance of the plugin.
343
+ // This is the ONLY setup step required.
344
+ const sortPlugin = new SortPlugin<User>({
345
+ initialSortColumn: 'logins',
346
+ initialSortDirection: 'desc',
347
+ });
348
+
349
+ // 2. Define the columns, using the helpers directly from the plugin instance.
350
+ const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
351
+ // ... see examples below
352
+ ];
353
+
354
+ const UserTable = ({ users }) => (
355
+ <ResponsiveTable
356
+ columnDefinitions={columnDefinitions}
357
+ data={users}
358
+ // 3. Pass the already-configured plugin to the table.
359
+ plugins={[sortPlugin]}
360
+ />
361
+ );
362
+ ```
363
+
364
+ **How to Make Columns Sortable (Opt-In):**
365
+
366
+ A column is made sortable by adding either a `sortComparer` or a `getSortableValue` property to its definition.
367
+
368
+ - `sortComparer`: A function that defines the exact comparison logic. This is the most powerful option and should be used for complex data types or custom logic.
369
+ - `getSortableValue`: A simpler function that just returns the primitive value (string, number, etc.) to be used in a default comparison.
370
+
371
+ **Example 1: Using Type-Safe Comparer Helpers**
372
+
373
+ The `SortPlugin` instance provides a `comparers` object with pre-built, type-safe helper functions to eliminate boilerplate for common sorting scenarios. This is the recommended approach.
374
+
375
+ ```jsx
376
+ const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
377
+ {
378
+ displayLabel: 'Name',
379
+ dataKey: 'name',
380
+ cellRenderer: (user) => user.name,
381
+ // The plugin instance itself provides the type-safe helpers.
382
+ // The string 'name' is fully type-checked against the User interface.
383
+ sortComparer: sortPlugin.comparers.caseInsensitiveString('name'),
384
+ },
385
+ {
386
+ displayLabel: 'Signup Date',
387
+ dataKey: 'signupDate',
388
+ cellRenderer: (user) => new Date(user.signupDate).toLocaleDateString(),
389
+ // IDE autocompletion for 'signupDate' works perfectly.
390
+ sortComparer: sortPlugin.comparers.date('signupDate'),
391
+ },
392
+ {
393
+ displayLabel: 'Logins',
394
+ dataKey: 'logins',
395
+ cellRenderer: (user) => user.logins,
396
+ sortComparer: sortPlugin.comparers.numeric('logins'),
397
+ },
398
+ {
399
+ displayLabel: 'Actions',
400
+ // This column is NOT sortable because it has no sort-related properties.
401
+ cellRenderer: (user) => <button>View</button>,
402
+ },
403
+ ];
404
+ ```
405
+
406
+ **Example 2: Writing a Custom `sortComparer`**
407
+
408
+ For unique requirements, you can write your own comparison function from scratch.
409
+
410
+ ```jsx
411
+ const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
412
+ {
413
+ displayLabel: 'Name',
414
+ dataKey: 'name',
415
+ cellRenderer: (user) => user.name,
416
+ // Writing custom logic for a case-sensitive sort
417
+ sortComparer: (a, b, direction) => {
418
+ const nameA = a.name; // No .toLowerCase()
419
+ const nameB = b.name;
420
+ if (nameA < nameB) return direction === 'asc' ? -1 : 1;
421
+ if (nameA > nameB) return direction === 'asc' ? 1 : -1;
422
+ return 0;
423
+ },
424
+ },
425
+ ];
426
+ ```
427
+
428
+ **Example 3: Using `getSortableValue` for Simple Cases**
429
+
430
+ If you don't need special logic, `getSortableValue` is a concise way to enable default sorting on a property.
431
+
432
+ ```jsx
433
+ const columnDefinitions: IResponsiveTableColumnDefinition<User>[] = [
434
+ {
435
+ displayLabel: 'Logins',
436
+ dataKey: 'logins',
437
+ cellRenderer: (user) => user.logins,
438
+ // This enables a simple, default numerical sort on the 'logins' property.
439
+ getSortableValue: (user) => user.logins,
440
+ },
441
+ ];
442
+ ```
443
+
444
+ **Plugin Options (via `new SortPlugin(options)`):**
445
+
446
+ | Prop | Type (`keyof TData`) | Description |
447
+ | ---------------------- | -------------------- | ------------------------------------------------- |
448
+ | `initialSortColumn` | `string` | The `dataKey` of the column to sort by initially. |
449
+ | `initialSortDirection` | `'asc' \| 'desc'` | The direction for the initial sort. |
450
+
451
+ **`SortPlugin.comparers` API:**
452
+
453
+ The `comparers` object on your `SortPlugin` instance provides the following helper methods. Each method is a factory that takes a `dataKey` (which is type-checked against your data model) and returns a `sortComparer` function.
454
+
455
+ | Method | Description |
456
+ | -------------------------------- | ----------------------------------------------------------------------------- |
457
+ | `numeric(dataKey)` | Performs a standard numerical sort. |
458
+ | `caseInsensitiveString(dataKey)` | Performs a case-insensitive alphabetical sort. |
459
+ | `date(dataKey)` | Correctly sorts dates, assuming the data is a valid date string or timestamp. |
460
+
461
+ #### `FilterPlugin`
462
+
463
+ Provides a search input to filter table data. It can be enabled by setting `filterProps.showFilter` to `true` on the `ResponsiveTable` component. For columns to be filterable, you must provide a `getFilterableValue` function in their `IResponsiveTableColumnDefinition`.
464
+
465
+ **Props for `FilterPlugin` (via `filterProps` on `ResponsiveTable`):**
466
+
467
+ | Prop | Type | Description |
468
+ | ------------------- | --------- | --------------------------------------------------------------- |
469
+ | `showFilter` | `boolean` | If `true`, displays a filter input field above the table. |
470
+ | `filterPlaceholder` | `string` | Placeholder text for the filter input. Defaults to "Search...". |
471
+
472
+ **Example with `FilterPlugin`:**
473
+
474
+ ```jsx
475
+ import React from 'react';
476
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
477
+
478
+ const FilterableTable = () => {
479
+ const initialData = [
480
+ { id: 1, name: 'Alice', email: 'alice@example.com' },
481
+ { id: 2, name: 'Bob', email: 'bob@example.com' },
482
+ { id: 3, name: 'Charlie', email: 'charlie@example.com' },
483
+ { id: 4, name: 'David', email: 'david@example.com' },
484
+ ];
485
+
486
+ const columns = [
487
+ { displayLabel: 'ID', cellRenderer: (row) => row.id, getFilterableValue: (row) => row.id.toString() },
488
+ { displayLabel: 'Name', cellRenderer: (row) => row.name, getFilterableValue: (row) => row.name },
489
+ { displayLabel: 'Email', cellRenderer: (row) => row.email, getFilterableValue: (row) => row.email },
490
+ ];
491
+
492
+ return (
493
+ <ResponsiveTable
494
+ columnDefinitions={columns}
495
+ data={initialData}
496
+ filterProps={{ showFilter: true, filterPlaceholder: 'Filter users...' }}
497
+ />
498
+ );
499
+ };
500
+ ```
501
+
502
+ #### `InfiniteScrollPlugin`
503
+
504
+ Enables high-performance virtualization for displaying large datasets. When enabled, the table will only render the rows currently visible on the screen, loading more data as the user scrolls. This is essential for maintaining a smooth user experience with thousands or millions of rows.
505
+
506
+ > **Important:** To enable infinite scrolling, you **must** give the table a bounded height. This is done by passing the `maxHeight` prop to the `<ResponsiveTable>`. This prop creates a scrollable container for the table body, which is required for virtualization to work.
507
+
508
+ **Configuration (via `infiniteScrollProps` on `ResponsiveTable`):**
509
+
510
+ | Prop | Type | Description |
511
+ | ---------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
512
+ | `enableInfiniteScroll` | `boolean` | If `true`, enables infinite scrolling. |
513
+ | `hasMore` | `boolean` | Set to `true` if there is more data to load, `false` otherwise. This tells the table whether to show the loading indicator. |
514
+ | `onLoadMore` | `(currentData: TData[]) => Promise<TData[]>` | A callback function that fires when the user scrolls near the end. It should fetch the next page of data and return it in a Promise. |
515
+ | `loadingMoreComponent` | `ReactNode` | A custom component to display at the bottom while new data is being loaded. Defaults to "Loading more...". |
516
+ | `noMoreDataComponent` | `ReactNode` | A custom component to display at the bottom when `hasMore` is `false`. Defaults to "No more data.". |
517
+
518
+ **Comprehensive Example:**
519
+
520
+ Here is a complete example of a component that fetches data from a simulated API and uses the infinite scroll feature.
521
+
522
+ ```jsx
523
+ import React, { useState, useEffect, useCallback } from 'react';
524
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
525
+
526
+ // Define the shape of our data items
527
+ interface DataItem {
528
+ id: number;
529
+ value: string;
530
+ }
531
+
532
+ // This is a mock API function to simulate fetching data.
533
+ // In a real app, this would be an actual network request.
534
+ const fetchData = async (page: number): Promise<DataItem[]> => {
535
+ console.log(`Fetching page: ${page}`);
536
+ return new Promise((resolve) => {
537
+ setTimeout(() => {
538
+ // Return an empty array for pages > 5 to simulate the end of the data
539
+ if (page > 5) {
540
+ resolve([]);
541
+ return;
542
+ }
543
+ // Generate 20 new items for the current page
544
+ const newData = Array.from({ length: 20 }, (_, i) => ({
545
+ id: page * 20 + i,
546
+ value: `Item #${page * 20 + i}`,
547
+ }));
548
+ resolve(newData);
549
+ }, 500); // Simulate network latency
550
+ });
551
+ };
552
+
553
+ const InfiniteScrollExample = () => {
554
+ // State for the data items displayed in the table
555
+ const [data, setData] = useState<DataItem[]>([]);
556
+ // State to track if there is more data to be loaded
557
+ const [hasMore, setHasMore] = useState(true);
558
+ // State to track the current page number for the API call
559
+ const [nextPage, setNextPage] = useState(0);
560
+
561
+ // The function that the table will call to load more items.
562
+ // It's wrapped in useCallback to prevent unnecessary re-renders.
563
+ const loadMoreItems = useCallback(async () => {
564
+ // Fetch the next page of data
565
+ const newItems = await fetchData(nextPage);
566
+
567
+ if (newItems.length === 0) {
568
+ // If the API returns no more items, update hasMore to false
569
+ setHasMore(false);
570
+ } else {
571
+ // Add the new items to the existing data
572
+ setData((prevData) => [...prevData, ...newItems]);
573
+ // Increment the page number for the next fetch
574
+ setNextPage((prevPage) => prevPage + 1);
575
+ }
576
+ }, [nextPage]);
577
+
578
+ // Load the initial data when the component mounts
579
+ useEffect(() => {
580
+ loadMoreItems();
581
+ }, []); // The empty dependency array ensures this runs only once on mount
582
+
583
+ const columns = [
584
+ { displayLabel: 'ID', dataKey: 'id', cellRenderer: (row) => row.id },
585
+ { displayLabel: 'Value', dataKey: 'value', cellRenderer: (row) => row.value },
586
+ ];
587
+
588
+ return (
589
+ // The table MUST be inside a container with a defined height.
590
+ <div style={{ height: '400px' }}>
591
+ <ResponsiveTable
592
+ columnDefinitions={columns}
593
+ data={data}
594
+ // The maxHeight prop makes the table body scrollable within the container.
595
+ maxHeight="100%"
596
+ // Pass the infinite scroll configuration
597
+ infiniteScrollProps={{
598
+ enableInfiniteScroll: true,
599
+ onLoadMore: loadMoreItems,
600
+ hasMore: hasMore,
601
+ loadingMoreComponent: <h4>Loading more items...</h4>,
602
+ noMoreDataComponent: <p>You've reached the end!</p>,
603
+ }}
604
+ />
605
+ </div>
606
+ );
607
+ };
608
+ ```
609
+
610
+ ---
611
+
612
+ ## API Reference
613
+
614
+ ### `ResponsiveTable` Props
615
+
616
+ | Prop | Type | Required | Description |
617
+ | ----------------------------- | ------------------------------------ | -------- | --------------------------------------------------------------------------------------------------- |
618
+ | `columnDefinitions` | `IResponsiveTableColumnDefinition[]` | Yes | An array of objects defining the table columns. |
619
+ | `data` | `TData[]` | Yes | An array of data objects to populate the table rows. |
620
+ | `footerRows` | `IFooterRowDefinition[]` | No | An array of objects defining the table footer. |
621
+ | `onRowClick` | `(item: TData) => void` | No | A callback function that is triggered when a row is clicked. |
622
+ | `noDataComponent` | `ReactNode` | No | A custom component to display when there is no data. |
623
+ | `maxHeight` | `string` | No | Sets a maximum height for the table body, making it scrollable. |
624
+ | `mobileBreakpoint` | `number` | No | The pixel width at which the table switches to the mobile view. Defaults to `600`. |
625
+ | `enablePageLevelStickyHeader` | `boolean` | No | If `false`, disables the header from sticking to the top of the page on scroll. Defaults to `true`. |
626
+ | `plugins` | `IResponsiveTablePlugin<TData>[]` | No | An array of plugin instances to extend table functionality. |
627
+ | `infiniteScrollProps` | `object` | No | Configuration for the built-in infinite scroll plugin. |
628
+ | `filterProps` | `object` | No | Configuration for the built-in filter plugin. |
629
+ | `animationProps` | `object` | No | Configuration for animations, including `isLoading` and `animateOnLoad`. |
630
+
631
+ ### `IResponsiveTableColumnDefinition<TData>`
632
+
633
+ | Property | Type | Required | Description |
634
+ | -------------------- | ------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------------- |
635
+ | `displayLabel` | `ReactNode` | Yes | The label displayed in the table header (can be a string or any React component). |
636
+ | `cellRenderer` | `(row: TData) => ReactNode` | Yes | A function that returns the content to be rendered in the cell. |
637
+ | `dataKey` | `string` | No | A key to match the column to a property in the data object (required for sorting). |
638
+ | `interactivity` | `object` | No | An object to define header interactivity (`onHeaderClick`, `id`, `className`). |
639
+ | `getFilterableValue` | `(row: TData) => string \| number` | No | A function that returns the string or number value to be used for filtering this column. |
640
+ | `getSortableValue` | `(row: TData) => any` | No | A function that returns a primitive value from a row to be used for default sorting. |
641
+ | `sortComparer` | `(a: TData, b: TData, direction: 'asc' \| 'desc') => number` | No | A function that provides the precise comparison logic for sorting a column. |
642
+
643
+ ### `IFooterRowDefinition`
644
+
645
+ | Property | Type | Required | Description |
646
+ | --------- | --------------------------- | -------- | -------------------------------------------------- |
647
+ | `columns` | `IFooterColumnDefinition[]` | Yes | An array of column definitions for the footer row. |
648
+
649
+ ### `IFooterColumnDefinition`
650
+
651
+ | Property | Type | Required | Description |
652
+ | -------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
653
+ | `colSpan` | `number` | Yes | The number of columns the footer cell should span. |
654
+ | `cellRenderer` | `() => ReactNode` | Yes | A function that returns the content for the footer cell. |
655
+ | `displayLabel` | `ReactNode` | No | An optional, explicit label for the footer cell. In mobile view, if `colSpan` is 1 and this is not provided, the corresponding column header will be used as a fallback. This is required for `colSpan` > 1 if you want a label to be displayed. |
656
+ | `onCellClick` | `() => void` | No | An optional click handler for the footer cell. |
657
+ | `className` | `string` | No | Optional class name for custom styling of the footer cell. |
658
+
659
+ ## License
660
+
661
+ This project is licensed under the MIT License.