jattac.libs.web.responsive-table 0.9.2 → 0.10.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.
@@ -70,6 +70,7 @@ interface TableContextValue<TData> {
70
70
  isLoading: boolean;
71
71
  isFetchingMore: boolean;
72
72
  loadNextPage: () => void;
73
+ error?: Error;
73
74
  };
74
75
  /** Custom CSS class to apply to each card in mobile view. */
75
76
  mobileCardClassName?: string;
@@ -9,6 +9,15 @@ interface UseTableDataSourceProps<TData> {
9
9
  };
10
10
  filter?: string;
11
11
  }
12
+ export interface DataSourceState<TData> {
13
+ data: TData[];
14
+ currentPage: number;
15
+ hasMore: boolean;
16
+ totalCount?: number;
17
+ isLoading: boolean;
18
+ isFetchingMore: boolean;
19
+ error?: Error;
20
+ }
12
21
  interface UseTableDataSourceReturn<TData> {
13
22
  data: TData[];
14
23
  currentPage: number;
@@ -18,6 +27,7 @@ interface UseTableDataSourceReturn<TData> {
18
27
  isFetchingMore: boolean;
19
28
  loadNextPage: () => Promise<void>;
20
29
  resetAndFetch: () => Promise<void>;
30
+ error?: Error;
21
31
  }
22
32
  export declare const useTableDataSource: <TData>(props: UseTableDataSourceProps<TData>) => UseTableDataSourceReturn<TData>;
23
33
  export {};
@@ -14,6 +14,7 @@ interface UseTablePluginsProps<TData> {
14
14
  showFilter?: boolean;
15
15
  filterPlaceholder?: string;
16
16
  className?: string;
17
+ mode?: 'client' | 'server';
17
18
  };
18
19
  selectionProps?: {
19
20
  onSelectionChange: (selectedItems: TData[]) => void;
@@ -28,6 +29,7 @@ interface UseTablePluginsProps<TData> {
28
29
  columnDefinitions: (IResponsiveTableColumnDefinition<TData> | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>))[];
29
30
  getScrollableElement: () => HTMLElement | null;
30
31
  infiniteScrollProps?: IInfiniteScrollProps<TData>;
32
+ onFilterChange?: (filterText: string) => void;
31
33
  }
32
34
  interface UseTablePluginsReturn<TData> {
33
35
  processedData: TData[];
@@ -28,6 +28,7 @@ export interface IPluginAPI<TData> {
28
28
  showFilter?: boolean;
29
29
  filterPlaceholder?: string;
30
30
  className?: string;
31
+ mode?: 'client' | 'server';
31
32
  };
32
33
  selectionProps?: {
33
34
  onSelectionChange: (selectedItems: TData[]) => void;
@@ -36,4 +37,5 @@ export interface IPluginAPI<TData> {
36
37
  selectedItems?: TData[];
37
38
  selectedRowClassName?: string;
38
39
  };
40
+ onFilterChange?: (filterText: string) => void;
39
41
  }
@@ -3,7 +3,13 @@ import { SortDirection } from '../Data/IResponsiveTableColumnDefinition';
3
3
  import IFooterRowDefinition from '../Data/IFooterRowDefinition';
4
4
  import { IResponsiveTablePlugin } from '../Plugins/IResponsiveTablePlugin';
5
5
  import { ColumnDefinition, DataSource } from '../Context/TableContext';
6
+ import { DataSourceState } from '../Hooks/useTableDataSource';
6
7
  export { ColumnDefinition };
8
+ export interface ResponsiveTableHandle<TData> {
9
+ loadNextPage: () => Promise<void>;
10
+ resetAndFetch: () => Promise<void>;
11
+ getState: () => DataSourceState<TData>;
12
+ }
7
13
  interface IInfiniteScrollProps<TData> {
8
14
  onLoadMore: (currentData: TData[]) => Promise<TData[] | null>;
9
15
  hasMore?: boolean;
@@ -66,6 +72,8 @@ interface IProps<TData> {
66
72
  showFilter?: boolean;
67
73
  filterPlaceholder?: string;
68
74
  className?: string;
75
+ /** When 'server', filter changes trigger a dataSource re-fetch with the filter param instead of client-side filtering. Default: 'client'. */
76
+ mode?: 'client' | 'server';
69
77
  };
70
78
  /** Configuration for row selection. */
71
79
  selectionProps?: {
@@ -84,10 +92,14 @@ interface IProps<TData> {
84
92
  sortProps?: ISortProps;
85
93
  /** Custom CSS class to apply to each card in mobile view. */
86
94
  mobileCardClassName?: string;
95
+ /** Callback fired whenever the dataSource state changes (data, page, loading, error). */
96
+ onDataSourceStateChange?: (state: DataSourceState<TData>) => void;
97
+ /** Callback fired when the current page changes. */
98
+ onPageChange?: (page: number) => void;
99
+ /** Callback fired when a dataSource fetch fails. */
100
+ onDataSourceError?: (error: Error) => void;
87
101
  }
88
- /**
89
- * A highly customizable, mobile-first responsive React table.
90
- * Supports static data or async data sources with built-in infinite scroll.
91
- */
92
- declare function ResponsiveTable<TData>(props: IProps<TData>): React.JSX.Element;
102
+ declare const ResponsiveTable: <TData>(props: IProps<TData> & {
103
+ ref?: React.Ref<ResponsiveTableHandle<TData>>;
104
+ }) => React.ReactElement;
93
105
  export default ResponsiveTable;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import IFooterColumnDefinition from './Data/IFooterColumnDefinition';
2
2
  import IFooterRowDefinition from './Data/IFooterRowDefinition';
3
3
  import { IResponsiveTableColumnDefinition, SortDirection } from './Data/IResponsiveTableColumnDefinition';
4
- import ResponsiveTable from './UI/ResponsiveTable';
5
- import { ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult } from './Context/TableContext';
4
+ import ResponsiveTable, { ResponsiveTableHandle } from './UI/ResponsiveTable';
5
+ import { ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, useTableContext } from './Context/TableContext';
6
+ import { DataSourceState } from './Hooks/useTableDataSource';
6
7
  import { FilterPlugin } from './Plugins/FilterPlugin';
7
8
  import { InfiniteScrollPlugin } from './Plugins/InfiniteScrollPlugin';
8
9
  import { IResponsiveTablePlugin } from './Plugins/IResponsiveTablePlugin';
9
10
  import { SortPlugin } from './Plugins/SortPlugin';
10
11
  import { SelectionPlugin } from './Plugins/SelectionPlugin';
11
- export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, };
12
+ export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, ResponsiveTableHandle, DataSourceState, useTableContext, };
12
13
  export default ResponsiveTable;
package/dist/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState } from 'react';
1
+ import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState, forwardRef, useImperativeHandle } from 'react';
2
2
 
3
3
  function styleInject(css, ref) {
4
4
  if ( ref === void 0 ) ref = {};
@@ -496,6 +496,10 @@ class FilterPlugin {
496
496
  } })));
497
497
  };
498
498
  this.processData = (data) => {
499
+ var _a;
500
+ if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
501
+ return data;
502
+ }
499
503
  if (!this.filterText || !this.api.columnDefinitions) {
500
504
  return data;
501
505
  }
@@ -522,6 +526,10 @@ class FilterPlugin {
522
526
  _row,
523
527
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
524
528
  _column) => {
529
+ var _a;
530
+ if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
531
+ return content;
532
+ }
525
533
  if (!this.filterText || typeof content !== 'string') {
526
534
  return content;
527
535
  }
@@ -534,8 +542,10 @@ class FilterPlugin {
534
542
  clearTimeout(this.debounceTimeout);
535
543
  }
536
544
  this.debounceTimeout = setTimeout(() => {
545
+ var _a, _b;
537
546
  this.filterText = currentFilterText;
538
547
  this.api.forceUpdate();
548
+ (_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, currentFilterText);
539
549
  }, 300);
540
550
  };
541
551
  }
@@ -757,7 +767,7 @@ class SortPlugin {
757
767
  }
758
768
 
759
769
  const useTablePlugins = (props) => {
760
- const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, } = props;
770
+ const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, onFilterChange, } = props;
761
771
  const [processedData, setProcessedData] = useState(data);
762
772
  const [activePlugins, setActivePlugins] = useState([]);
763
773
  // Persist internal plugins using refs to prevent state loss
@@ -831,6 +841,7 @@ const useTablePlugins = (props) => {
831
841
  filterProps: filterProps,
832
842
  selectionProps: selectionProps,
833
843
  columnDefinitions: columnDefinitions,
844
+ onFilterChange: onFilterChange,
834
845
  };
835
846
  // Initialize/Refresh all active plugins with the current API
836
847
  newActivePlugins.forEach((plugin) => {
@@ -856,6 +867,7 @@ const useTablePlugins = (props) => {
856
867
  getScrollableElement,
857
868
  infiniteScrollProps,
858
869
  getRawColumnDefinition,
870
+ onFilterChange,
859
871
  ]);
860
872
  const forceUpdatePlugins = useCallback(() => {
861
873
  setProcessedData(initializePlugins());
@@ -1026,6 +1038,7 @@ const useTableDataSource = (props) => {
1026
1038
  const [totalCount, setTotalCount] = useState(undefined);
1027
1039
  const [isLoading, setIsLoading] = useState(false);
1028
1040
  const [isFetchingMore, setIsFetchingMore] = useState(false);
1041
+ const [error, setError] = useState(undefined);
1029
1042
  const isInitialMount = useRef(true);
1030
1043
  const fetchData = useCallback((page, isAppend) => __awaiter(void 0, void 0, void 0, function* () {
1031
1044
  if (!dataSource)
@@ -1037,6 +1050,7 @@ const useTableDataSource = (props) => {
1037
1050
  setIsLoading(true);
1038
1051
  }
1039
1052
  try {
1053
+ setError(undefined);
1040
1054
  const params = {
1041
1055
  page,
1042
1056
  pageSize,
@@ -1066,9 +1080,10 @@ const useTableDataSource = (props) => {
1066
1080
  setHasMore(newItems.length === pageSize);
1067
1081
  }
1068
1082
  }
1069
- catch (error) {
1070
- console.error('Error fetching data from dataSource:', error);
1083
+ catch (err) {
1084
+ setError(err);
1071
1085
  setHasMore(false);
1086
+ console.error('Error fetching data from dataSource:', err);
1072
1087
  }
1073
1088
  finally {
1074
1089
  setIsLoading(false);
@@ -1105,6 +1120,7 @@ const useTableDataSource = (props) => {
1105
1120
  isFetchingMore,
1106
1121
  loadNextPage,
1107
1122
  resetAndFetch,
1123
+ error,
1108
1124
  };
1109
1125
  };
1110
1126
 
@@ -1112,8 +1128,8 @@ const useTableDataSource = (props) => {
1112
1128
  * A highly customizable, mobile-first responsive React table.
1113
1129
  * Supports static data or async data sources with built-in infinite scroll.
1114
1130
  */
1115
- function ResponsiveTable(props) {
1116
- const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, mobileCardClassName, } = props;
1131
+ function ResponsiveTableInner(props, ref) {
1132
+ const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, mobileCardClassName, onDataSourceStateChange, onPageChange, onDataSourceError, } = props;
1117
1133
  const tableContainerRef = useRef(null);
1118
1134
  const headerRef = useRef(null);
1119
1135
  const { isMobile, isHeaderSticky } = useResponsiveTable({
@@ -1126,17 +1142,36 @@ function ResponsiveTable(props) {
1126
1142
  const getScrollableElement = useCallback(() => tableContainerRef.current, []);
1127
1143
  // Track active sort state for dataSource
1128
1144
  const [activeSort /*, setActiveSort*/] = useState((sortProps === null || sortProps === void 0 ? void 0 : sortProps.initialSortColumn) ? { columnId: sortProps.initialSortColumn, direction: sortProps.initialSortDirection || 'asc' } : undefined);
1129
- const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, } = useTableDataSource({
1145
+ // Track active filter state for dataSource
1146
+ const [activeFilter, setActiveFilter] = useState('');
1147
+ const handleFilterChange = useCallback((text) => {
1148
+ setActiveFilter(text);
1149
+ }, []);
1150
+ const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, error, resetAndFetch, } = useTableDataSource({
1130
1151
  dataSource,
1131
1152
  pageSize,
1132
1153
  initialData,
1133
1154
  sort: activeSort,
1134
- // We'll need to extract filter state if we want to support dataSource filtering
1155
+ filter: (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) === 'server' ? activeFilter : undefined,
1135
1156
  });
1157
+ useImperativeHandle(ref, () => ({
1158
+ loadNextPage: () => loadNextPage(),
1159
+ resetAndFetch: () => resetAndFetch(),
1160
+ getState: () => ({
1161
+ data: sourceData,
1162
+ currentPage,
1163
+ hasMore,
1164
+ totalCount,
1165
+ isLoading: isSourceLoading,
1166
+ isFetchingMore,
1167
+ error,
1168
+ }),
1169
+ }), [loadNextPage, resetAndFetch, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error]);
1136
1170
  const currentDataToProcess = dataSource ? sourceData : initialData;
1137
1171
  const { processedData, activePlugins, visibleColumns } = useTablePlugins({
1138
1172
  data: currentDataToProcess,
1139
1173
  plugins,
1174
+ onFilterChange: (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) === 'server' ? handleFilterChange : undefined,
1140
1175
  filterProps,
1141
1176
  selectionProps,
1142
1177
  sortProps,
@@ -1144,11 +1179,32 @@ function ResponsiveTable(props) {
1144
1179
  getScrollableElement,
1145
1180
  infiniteScrollProps,
1146
1181
  });
1147
- // Sync sort state from SortPlugin back to our local state to trigger dataSource re-fetch
1182
+ // Fire onDataSourceStateChange when dataSource state changes
1183
+ useEffect(() => {
1184
+ if (dataSource && onDataSourceStateChange) {
1185
+ onDataSourceStateChange({
1186
+ data: sourceData,
1187
+ currentPage,
1188
+ hasMore,
1189
+ totalCount,
1190
+ isLoading: isSourceLoading,
1191
+ isFetchingMore,
1192
+ error,
1193
+ });
1194
+ }
1195
+ }, [dataSource, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error, onDataSourceStateChange]);
1196
+ // Fire onPageChange when page changes
1197
+ useEffect(() => {
1198
+ if (dataSource && onPageChange) {
1199
+ onPageChange(currentPage);
1200
+ }
1201
+ }, [dataSource, currentPage, onPageChange]);
1202
+ // Fire onDataSourceError when error occurs
1148
1203
  useEffect(() => {
1149
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1150
- activePlugins.find(p => p.id === 'sort');
1151
- }, [activePlugins, dataSource]);
1204
+ if (dataSource && error && onDataSourceError) {
1205
+ onDataSourceError(error);
1206
+ }
1207
+ }, [dataSource, error, onDataSourceError]);
1152
1208
  const hasData = useMemo(() => processedData.length > 0, [processedData]);
1153
1209
  const noDataSvg = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "#ccc", height: "40", width: "40", viewBox: "0 0 24 24" },
1154
1210
  React.createElement("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-14h2v6h-2zm0 8h2v2h-2z" })));
@@ -1216,6 +1272,31 @@ function ResponsiveTable(props) {
1216
1272
  if (isLoading && !hasData) {
1217
1273
  return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
1218
1274
  }
1275
+ if (error && !isLoading && !hasData) {
1276
+ return (React.createElement("div", { style: {
1277
+ display: 'flex',
1278
+ flexDirection: 'column',
1279
+ alignItems: 'center',
1280
+ justifyContent: 'center',
1281
+ padding: '4rem 2rem',
1282
+ gap: '1rem',
1283
+ color: '#6c757d',
1284
+ border: '2px dashed #e0e0e0',
1285
+ borderRadius: '12px',
1286
+ backgroundColor: '#f8f9fa',
1287
+ } },
1288
+ React.createElement("div", { style: { fontWeight: 500, fontSize: '1.1rem' } }, "Failed to load data"),
1289
+ React.createElement("div", { style: { fontSize: '0.85rem', textAlign: 'center' } }, error.message),
1290
+ React.createElement("button", { onClick: resetAndFetch, style: {
1291
+ padding: '0.5rem 1.5rem',
1292
+ backgroundColor: '#007bff',
1293
+ color: '#fff',
1294
+ border: 'none',
1295
+ borderRadius: '6px',
1296
+ cursor: 'pointer',
1297
+ fontWeight: 500,
1298
+ } }, "Retry")));
1299
+ }
1219
1300
  return (React.createElement(TableProvider, { value: {
1220
1301
  data: currentDataToProcess,
1221
1302
  processedData,
@@ -1234,6 +1315,7 @@ function ResponsiveTable(props) {
1234
1315
  isLoading: isSourceLoading,
1235
1316
  isFetchingMore,
1236
1317
  loadNextPage,
1318
+ error,
1237
1319
  } : undefined,
1238
1320
  mobileCardClassName,
1239
1321
  } },
@@ -1243,6 +1325,7 @@ function ResponsiveTable(props) {
1243
1325
  (hasData || isLoading) && isMobile && (React.createElement(MobileView, { mobileFooter: mobileFooter })),
1244
1326
  (hasData || isLoading) && !isMobile && (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters })))));
1245
1327
  }
1328
+ const ResponsiveTable = forwardRef(ResponsiveTableInner);
1246
1329
 
1247
1330
  class InfiniteScrollPlugin {
1248
1331
  constructor() {
@@ -1297,5 +1380,5 @@ class InfiniteScrollPlugin {
1297
1380
  }
1298
1381
  }
1299
1382
 
1300
- export { FilterPlugin, InfiniteScrollPlugin, SelectionPlugin, SortPlugin, ResponsiveTable as default };
1383
+ export { FilterPlugin, InfiniteScrollPlugin, SelectionPlugin, SortPlugin, ResponsiveTable as default, useTableContext };
1301
1384
  //# sourceMappingURL=index.es.js.map