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 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 new `dataSource` pattern makes handling large datasets, server-side sorting, and infinite scroll completely painless. You provide the fetch logic; we handle the bookkeeping.
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
- ### Basic Usage
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; // Table automatically handles appending and hasMore detection!
58
+ return users; // hasMore is auto-detected from page size
31
59
  }}
32
60
  columnDefinitions={columns}
33
61
  />
34
62
  ```
35
63
 
36
- ### With Sorting & Filtering
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
- return await api.getUsers({
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
- /** When 'server', filter changes trigger a dataSource re-fetch with the filter param instead of client-side filtering. Default: 'client'. */
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("input", { type: "text", placeholder: this.api.filterProps.filterPlaceholder || "Search...", onChange: this.handleFilterChange, className: this.api.filterProps.className, style: {
493
- padding: '0.5rem',
494
- border: '1px solid #ccc',
495
- borderRadius: '4px',
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 ? data.length : 0) + newItems.length;
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, data.length]);
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: (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) === 'server' ? activeFilter : undefined,
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: (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) === 'server' ? handleFilterChange : undefined,
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: Object.assign(Object.assign({}, animationProps), { isLoading }),
1351
+ animationProps: resolvedAnimationProps,
1309
1352
  dataSource,
1310
1353
  pagination: dataSource ? {
1311
1354
  currentPage,