jattac.libs.web.responsive-table 0.10.0 → 0.11.1
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 +66 -11
- package/dist/Plugins/FilterPlugin.d.ts +1 -0
- package/dist/UI/ResponsiveTable.d.ts +1 -1
- package/dist/index.es.js +57 -14
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +57 -14
- package/dist/index.js.map +1 -1
- package/docs/api.md +15 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -13,45 +13,81 @@ ResponsiveTable is a high-performance, type-safe React component designed for co
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install jattac.libs.web.responsive-table
|
|
16
|
+
npm install jattac.libs.web.responsive-table jattac.libs.web.zest-textbox react-icons
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
## Built-in Filter
|
|
22
|
+
|
|
23
|
+
Enable the search box with one prop. A clear (×) button appears automatically when the field has text.
|
|
24
|
+
|
|
25
|
+
**Client-side** — filters the in-memory `data` array and highlights matches:
|
|
26
|
+
```tsx
|
|
27
|
+
<ResponsiveTable
|
|
28
|
+
data={rows}
|
|
29
|
+
columnDefinitions={columns}
|
|
30
|
+
filterProps={{ showFilter: true }}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Server-side** — when `dataSource` is present, server mode is automatic. The table resets to page 1 and calls your fetch function with the current `filter` string on every change:
|
|
35
|
+
```tsx
|
|
36
|
+
<ResponsiveTable
|
|
37
|
+
dataSource={async ({ page, pageSize, filter }) =>
|
|
38
|
+
api.getUsers({ page, pageSize, search: filter })
|
|
39
|
+
}
|
|
40
|
+
columnDefinitions={columns}
|
|
41
|
+
filterProps={{ showFilter: true }}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> To force client-side filtering even with a `dataSource`, pass `mode: 'client'`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
21
49
|
## Delightful Data Fetching: Smart Data Source
|
|
22
50
|
|
|
23
|
-
The
|
|
51
|
+
The `dataSource` pattern makes handling large datasets, server-side sorting, filtering, and infinite scroll completely painless. You provide the fetch logic; the table handles bookkeeping.
|
|
24
52
|
|
|
25
|
-
###
|
|
53
|
+
### Pagination only
|
|
26
54
|
```tsx
|
|
27
55
|
<ResponsiveTable
|
|
28
56
|
dataSource={async ({ page, pageSize }) => {
|
|
29
57
|
const users = await api.getUsers({ page, pageSize });
|
|
30
|
-
return users; //
|
|
58
|
+
return users; // hasMore is auto-detected from page size
|
|
31
59
|
}}
|
|
32
60
|
columnDefinitions={columns}
|
|
33
61
|
/>
|
|
34
62
|
```
|
|
35
63
|
|
|
36
|
-
###
|
|
37
|
-
The table tells you exactly what it needs based on user interaction:
|
|
64
|
+
### Pagination + sorting + filtering
|
|
38
65
|
```tsx
|
|
39
66
|
<ResponsiveTable
|
|
40
|
-
dataSource={async ({ page, pageSize, sort, filter }) =>
|
|
41
|
-
|
|
67
|
+
dataSource={async ({ page, pageSize, sort, filter }) =>
|
|
68
|
+
api.getUsers({
|
|
42
69
|
page,
|
|
43
70
|
limit: pageSize,
|
|
44
71
|
sortBy: sort?.columnId,
|
|
45
72
|
order: sort?.direction,
|
|
46
|
-
search: filter
|
|
47
|
-
})
|
|
48
|
-
}
|
|
73
|
+
search: filter,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
49
76
|
columnDefinitions={columns}
|
|
50
77
|
sortProps={{ initialSortColumn: 'name' }}
|
|
51
78
|
filterProps={{ showFilter: true }}
|
|
52
79
|
/>
|
|
53
80
|
```
|
|
54
81
|
|
|
82
|
+
### With total count (accurate hasMore)
|
|
83
|
+
Return `{ items, totalCount }` instead of a plain array and the table derives `hasMore` precisely:
|
|
84
|
+
```tsx
|
|
85
|
+
dataSource={async ({ page, pageSize }) => {
|
|
86
|
+
const { data, total } = await api.getUsers({ page, pageSize });
|
|
87
|
+
return { items: data, totalCount: total };
|
|
88
|
+
}}
|
|
89
|
+
```
|
|
90
|
+
|
|
55
91
|
---
|
|
56
92
|
|
|
57
93
|
## Basic Implementation
|
|
@@ -106,6 +142,25 @@ For a deep dive into more complex scenarios, see the **[Handling Interactive Ele
|
|
|
106
142
|
|
|
107
143
|
---
|
|
108
144
|
|
|
145
|
+
## Loading States & Animations
|
|
146
|
+
|
|
147
|
+
Control skeleton loaders and entrance animations with `animationProps`:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<ResponsiveTable
|
|
151
|
+
data={rows}
|
|
152
|
+
columnDefinitions={columns}
|
|
153
|
+
animationProps={{ isLoading: isFetching, animateOnLoad: true }}
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Prop | Type | Description |
|
|
158
|
+
| :--- | :--- | :--- |
|
|
159
|
+
| `isLoading` | `boolean` | Shows a skeleton loader while `true`. Merges with internal `dataSource` loading state. |
|
|
160
|
+
| `animateOnLoad` | `boolean` | Animates rows in on initial mount with a staggered entrance effect. |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
109
164
|
## Documentation Directory
|
|
110
165
|
|
|
111
166
|
The following technical documentation provides comprehensive implementation guidance:
|
|
@@ -12,4 +12,5 @@ export declare class FilterPlugin<TData> implements IResponsiveTablePlugin<TData
|
|
|
12
12
|
processData: (data: TData[]) => TData[];
|
|
13
13
|
renderCell: (content: React.ReactNode, _row: TData, _column: IResponsiveTableColumnDefinition<TData>) => React.ReactNode;
|
|
14
14
|
private handleFilterChange;
|
|
15
|
+
private handleClear;
|
|
15
16
|
}
|
|
@@ -72,7 +72,7 @@ interface IProps<TData> {
|
|
|
72
72
|
showFilter?: boolean;
|
|
73
73
|
filterPlaceholder?: string;
|
|
74
74
|
className?: string;
|
|
75
|
-
/**
|
|
75
|
+
/** Default: 'server' when dataSource is present, 'client' otherwise. Pass 'client' to force in-memory filtering even with a dataSource. */
|
|
76
76
|
mode?: 'client' | 'server';
|
|
77
77
|
};
|
|
78
78
|
/** Configuration for row selection. */
|
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
|
2
|
+
import ZestTextbox from 'jattac.libs.web.zest-textbox';
|
|
3
|
+
import { MdClose } from 'react-icons/md';
|
|
2
4
|
|
|
3
5
|
function styleInject(css, ref) {
|
|
4
6
|
if ( ref === void 0 ) ref = {};
|
|
@@ -484,16 +486,30 @@ class FilterPlugin {
|
|
|
484
486
|
this.api = api;
|
|
485
487
|
};
|
|
486
488
|
this.renderHeader = () => {
|
|
487
|
-
var _a;
|
|
489
|
+
var _a, _b;
|
|
488
490
|
if (!((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.showFilter)) {
|
|
489
491
|
return null;
|
|
490
492
|
}
|
|
491
|
-
return (React.createElement("div", { style: { marginBottom: '1rem' } },
|
|
492
|
-
React.createElement(
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
493
|
+
return (React.createElement("div", { style: { marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' } },
|
|
494
|
+
React.createElement(ZestTextbox, { value: this.filterText, placeholder: (_b = this.api.filterProps.filterPlaceholder) !== null && _b !== void 0 ? _b : 'Search...', onChange: this.handleFilterChange, className: this.api.filterProps.className, zest: { stretch: true } }),
|
|
495
|
+
React.createElement("button", { onClick: this.handleClear, "aria-label": "Clear filter", style: {
|
|
496
|
+
display: 'flex',
|
|
497
|
+
alignItems: 'center',
|
|
498
|
+
justifyContent: 'center',
|
|
499
|
+
minWidth: '2.75rem',
|
|
500
|
+
minHeight: '2.75rem',
|
|
501
|
+
padding: 0,
|
|
502
|
+
border: 'none',
|
|
503
|
+
borderRadius: '50%',
|
|
504
|
+
background: 'transparent',
|
|
505
|
+
cursor: 'pointer',
|
|
506
|
+
color: '#666',
|
|
507
|
+
opacity: this.filterText ? 1 : 0,
|
|
508
|
+
pointerEvents: this.filterText ? 'auto' : 'none',
|
|
509
|
+
transition: 'opacity 0.15s ease',
|
|
510
|
+
flexShrink: 0,
|
|
511
|
+
} },
|
|
512
|
+
React.createElement(MdClose, { size: 20 }))));
|
|
497
513
|
};
|
|
498
514
|
this.processData = (data) => {
|
|
499
515
|
var _a;
|
|
@@ -548,6 +564,15 @@ class FilterPlugin {
|
|
|
548
564
|
(_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, currentFilterText);
|
|
549
565
|
}, 300);
|
|
550
566
|
};
|
|
567
|
+
this.handleClear = () => {
|
|
568
|
+
var _a, _b;
|
|
569
|
+
if (this.debounceTimeout) {
|
|
570
|
+
clearTimeout(this.debounceTimeout);
|
|
571
|
+
}
|
|
572
|
+
this.filterText = '';
|
|
573
|
+
this.api.forceUpdate();
|
|
574
|
+
(_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, '');
|
|
575
|
+
};
|
|
551
576
|
}
|
|
552
577
|
}
|
|
553
578
|
|
|
@@ -1040,6 +1065,8 @@ const useTableDataSource = (props) => {
|
|
|
1040
1065
|
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
|
1041
1066
|
const [error, setError] = useState(undefined);
|
|
1042
1067
|
const isInitialMount = useRef(true);
|
|
1068
|
+
const dataLengthRef = useRef(0);
|
|
1069
|
+
dataLengthRef.current = data.length;
|
|
1043
1070
|
const fetchData = useCallback((page, isAppend) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1044
1071
|
if (!dataSource)
|
|
1045
1072
|
return;
|
|
@@ -1072,7 +1099,7 @@ const useTableDataSource = (props) => {
|
|
|
1072
1099
|
setCurrentPage(page);
|
|
1073
1100
|
// Intelligent hasMore detection
|
|
1074
1101
|
if (newTotalCount !== undefined) {
|
|
1075
|
-
const currentTotalLoaded = (isAppend ?
|
|
1102
|
+
const currentTotalLoaded = (isAppend ? dataLengthRef.current : 0) + newItems.length;
|
|
1076
1103
|
setHasMore(currentTotalLoaded < newTotalCount);
|
|
1077
1104
|
}
|
|
1078
1105
|
else {
|
|
@@ -1089,7 +1116,7 @@ const useTableDataSource = (props) => {
|
|
|
1089
1116
|
setIsLoading(false);
|
|
1090
1117
|
setIsFetchingMore(false);
|
|
1091
1118
|
}
|
|
1092
|
-
}), [dataSource, pageSize, sort, filter
|
|
1119
|
+
}), [dataSource, pageSize, sort, filter]);
|
|
1093
1120
|
const loadNextPage = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1094
1121
|
if (isLoading || isFetchingMore || !hasMore || !dataSource)
|
|
1095
1122
|
return;
|
|
@@ -1147,12 +1174,24 @@ function ResponsiveTableInner(props, ref) {
|
|
|
1147
1174
|
const handleFilterChange = useCallback((text) => {
|
|
1148
1175
|
setActiveFilter(text);
|
|
1149
1176
|
}, []);
|
|
1177
|
+
const isServerFilter = !!dataSource && !!(filterProps === null || filterProps === void 0 ? void 0 : filterProps.showFilter) && (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) !== 'client';
|
|
1178
|
+
const resolvedFilterProps = useMemo(() => {
|
|
1179
|
+
var _a;
|
|
1180
|
+
if (!filterProps)
|
|
1181
|
+
return undefined;
|
|
1182
|
+
return {
|
|
1183
|
+
showFilter: filterProps.showFilter,
|
|
1184
|
+
filterPlaceholder: filterProps.filterPlaceholder,
|
|
1185
|
+
className: filterProps.className,
|
|
1186
|
+
mode: isServerFilter ? 'server' : ((_a = filterProps.mode) !== null && _a !== void 0 ? _a : 'client'),
|
|
1187
|
+
};
|
|
1188
|
+
}, [filterProps === null || filterProps === void 0 ? void 0 : filterProps.showFilter, filterProps === null || filterProps === void 0 ? void 0 : filterProps.filterPlaceholder, filterProps === null || filterProps === void 0 ? void 0 : filterProps.className, filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode, isServerFilter]);
|
|
1150
1189
|
const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, error, resetAndFetch, } = useTableDataSource({
|
|
1151
1190
|
dataSource,
|
|
1152
1191
|
pageSize,
|
|
1153
1192
|
initialData,
|
|
1154
1193
|
sort: activeSort,
|
|
1155
|
-
filter:
|
|
1194
|
+
filter: isServerFilter ? activeFilter : undefined,
|
|
1156
1195
|
});
|
|
1157
1196
|
useImperativeHandle(ref, () => ({
|
|
1158
1197
|
loadNextPage: () => loadNextPage(),
|
|
@@ -1171,8 +1210,8 @@ function ResponsiveTableInner(props, ref) {
|
|
|
1171
1210
|
const { processedData, activePlugins, visibleColumns } = useTablePlugins({
|
|
1172
1211
|
data: currentDataToProcess,
|
|
1173
1212
|
plugins,
|
|
1174
|
-
onFilterChange:
|
|
1175
|
-
filterProps,
|
|
1213
|
+
onFilterChange: isServerFilter ? handleFilterChange : undefined,
|
|
1214
|
+
filterProps: resolvedFilterProps,
|
|
1176
1215
|
selectionProps,
|
|
1177
1216
|
sortProps,
|
|
1178
1217
|
columnDefinitions,
|
|
@@ -1265,10 +1304,14 @@ function ResponsiveTableInner(props, ref) {
|
|
|
1265
1304
|
return null;
|
|
1266
1305
|
});
|
|
1267
1306
|
}, [plugins]);
|
|
1307
|
+
const isLoading = (animationProps === null || animationProps === void 0 ? void 0 : animationProps.isLoading) || isSourceLoading;
|
|
1308
|
+
const resolvedAnimationProps = useMemo(() => ({
|
|
1309
|
+
animateOnLoad: animationProps === null || animationProps === void 0 ? void 0 : animationProps.animateOnLoad,
|
|
1310
|
+
isLoading,
|
|
1311
|
+
}), [animationProps === null || animationProps === void 0 ? void 0 : animationProps.animateOnLoad, animationProps === null || animationProps === void 0 ? void 0 : animationProps.isLoading, isLoading]);
|
|
1268
1312
|
if (infiniteScrollProps) {
|
|
1269
1313
|
return React.createElement(InfiniteTable, Object.assign({}, props));
|
|
1270
1314
|
}
|
|
1271
|
-
const isLoading = (animationProps === null || animationProps === void 0 ? void 0 : animationProps.isLoading) || isSourceLoading;
|
|
1272
1315
|
if (isLoading && !hasData) {
|
|
1273
1316
|
return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
|
|
1274
1317
|
}
|
|
@@ -1305,7 +1348,7 @@ function ResponsiveTableInner(props, ref) {
|
|
|
1305
1348
|
activePlugins,
|
|
1306
1349
|
onRowClick,
|
|
1307
1350
|
selectionProps,
|
|
1308
|
-
animationProps:
|
|
1351
|
+
animationProps: resolvedAnimationProps,
|
|
1309
1352
|
dataSource,
|
|
1310
1353
|
pagination: dataSource ? {
|
|
1311
1354
|
currentPage,
|