@7span/react-list 0.0.6 → 1.0.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.
@@ -0,0 +1,375 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+ import { hasActiveFilters } from "./utils";
4
+ import { isEqual } from "../utils";
5
+
6
+ /**
7
+ * ReactList component for handling data fetching, pagination, and state management
8
+ */
9
+ const ReactList = ({
10
+ initialItems = [],
11
+ children,
12
+ endpoint,
13
+ page = 1,
14
+ perPage = 25,
15
+ sortBy = "",
16
+ sortOrder = "desc",
17
+ count = 0,
18
+ search = "",
19
+ filters = {},
20
+ attrs,
21
+ version = 1,
22
+ paginationMode = "pagination",
23
+ meta = {},
24
+ onResponse,
25
+ afterPageChange,
26
+ afterLoadMore,
27
+ }) => {
28
+ const { requestHandler, setListState, stateManager } = useListContext();
29
+
30
+ const initRef = useRef(false);
31
+
32
+ const isLoadMore = paginationMode === "loadMore";
33
+
34
+ const getContext = useCallback(
35
+ (currentState) => {
36
+ return {
37
+ endpoint,
38
+ version,
39
+ meta,
40
+ search: currentState?.search || search,
41
+ page: currentState?.page || page,
42
+ perPage: currentState?.perPage || perPage,
43
+ sortBy: currentState?.sortBy || sortBy,
44
+ sortOrder: currentState?.sortOrder || sortOrder,
45
+ filters: currentState?.filters || filters,
46
+ attrSettings: currentState?.attrSettings || {},
47
+ isRefresh: false,
48
+ };
49
+ },
50
+ [endpoint, version, meta, search, page, perPage, sortBy, sortOrder, filters]
51
+ );
52
+
53
+ const getSavedState = useCallback(() => {
54
+ try {
55
+ const context = getContext();
56
+ const oldState = stateManager?.get?.(context);
57
+
58
+ return {
59
+ page: oldState?.page,
60
+ perPage: oldState?.perPage,
61
+ sortBy: oldState?.sortBy,
62
+ sortOrder: oldState?.sortOrder,
63
+ search: oldState?.search,
64
+ attrSettings: oldState?.attrSettings,
65
+ filters: oldState?.filters,
66
+ };
67
+ } catch (err) {
68
+ console.error(err);
69
+ return {};
70
+ }
71
+ }, [getContext, stateManager]);
72
+
73
+ const initializeState = useCallback(() => {
74
+ const savedState = getSavedState();
75
+
76
+ let initialPage = page;
77
+ if (isLoadMore) {
78
+ initialPage = 1;
79
+ } else if (savedState.page != null) {
80
+ initialPage = savedState.page;
81
+ }
82
+
83
+ return {
84
+ page: initialPage,
85
+ perPage: savedState.perPage != null ? savedState.perPage : perPage,
86
+ sortBy: savedState.sortBy != null ? savedState.sortBy : sortBy,
87
+ sortOrder:
88
+ savedState.sortOrder != null ? savedState.sortOrder : sortOrder,
89
+ search: savedState.search != null ? savedState.search : search,
90
+ filters: savedState.filters != null ? savedState.filters : filters,
91
+ attrSettings: savedState.attrSettings || {},
92
+ items: initialItems,
93
+ selection: [],
94
+ error: null,
95
+ response: null,
96
+ count: 0,
97
+ isLoading: false,
98
+ initializingState: !initialItems.length,
99
+ confirmedPage: null,
100
+ };
101
+ }, [
102
+ getSavedState,
103
+ search,
104
+ page,
105
+ perPage,
106
+ sortBy,
107
+ sortOrder,
108
+ search,
109
+ filters,
110
+ isLoadMore,
111
+ ]);
112
+
113
+ // Initialize state with default values
114
+ const [state, setState] = useState(initializeState);
115
+
116
+ const updateStateManager = useCallback(
117
+ (stateToSave) => {
118
+ if (stateManager) {
119
+ const context = getContext(stateToSave);
120
+ stateManager?.set?.(context);
121
+ }
122
+ },
123
+ [stateManager, getContext]
124
+ );
125
+
126
+ /**
127
+ * Fetch data from the API
128
+ * @param {Object} addContext - Additional context to pass to the request handler
129
+ * @param {Object} newState - New state to use for the request
130
+ */
131
+ const fetchData = useCallback(
132
+ async (addContext = {}, newState = null) => {
133
+ // Only set loading state if not initializing
134
+ if (!state.initializingState) {
135
+ setState((prev) => ({ ...prev, error: null, isLoading: true }));
136
+ }
137
+
138
+ try {
139
+ const currentState = newState || state;
140
+ const previousItems = newState?.items ?? state.items;
141
+ const res = await requestHandler({
142
+ endpoint,
143
+ version,
144
+ meta,
145
+ page: currentState.page,
146
+ perPage: currentState.perPage,
147
+ search: currentState.search,
148
+ sortBy: currentState.sortBy,
149
+ sortOrder: currentState.sortOrder,
150
+ filters: currentState.filters,
151
+ ...addContext,
152
+ });
153
+
154
+ if (onResponse) onResponse(res);
155
+
156
+ let newItems;
157
+
158
+ if (isLoadMore) {
159
+ newItems =
160
+ currentState.page === 1
161
+ ? res.items
162
+ : [...previousItems, ...res.items];
163
+ if (afterLoadMore) afterLoadMore(res);
164
+ } else {
165
+ newItems = res.items;
166
+ if (afterPageChange) afterPageChange(res);
167
+ }
168
+
169
+ const updatedState = {
170
+ ...currentState,
171
+ response: res,
172
+ selection: [],
173
+ // Append items for loadMore, replace for pagination
174
+ items:
175
+ isLoadMore && currentState.page > 1
176
+ ? [...previousItems, ...res.items]
177
+ : res.items,
178
+ count: res.count,
179
+ initializingState: false,
180
+ isLoading: false,
181
+ };
182
+
183
+ updateStateManager(updatedState);
184
+
185
+ setState(updatedState);
186
+ } catch (err) {
187
+ setState((prev) => ({
188
+ ...prev,
189
+ error: err,
190
+ items: [],
191
+ count: 0,
192
+ initializingState: false,
193
+ isLoading: false,
194
+ }));
195
+ throw err;
196
+ }
197
+ },
198
+ [endpoint, version, isLoadMore, meta, requestHandler, state]
199
+ );
200
+
201
+ /**
202
+ * Handlers for various actions (pagination, sorting, filtering, etc.)
203
+ */
204
+ const handlers = useMemo(
205
+ () => ({
206
+ setPage: (value, addContext) => {
207
+ let newPage = value;
208
+ if (value === 0) {
209
+ newPage = "";
210
+ }
211
+ const newState = { ...state, page: newPage };
212
+ setState(newState);
213
+ if (newPage) fetchData(addContext, newState);
214
+ },
215
+
216
+ setPerPage: (value) => {
217
+ const newState = { ...state, perPage: value, page: 1 };
218
+ setState(newState);
219
+ fetchData({}, newState);
220
+ },
221
+
222
+ setSearch: (value) => {
223
+ // Only update if value changed to prevent unnecessary requests
224
+ if (value !== state.search) {
225
+ const newState = { ...state, search: value, page: 1 };
226
+ setState(newState);
227
+ fetchData({}, newState);
228
+ }
229
+ },
230
+
231
+ setSort: ({ by, order }) => {
232
+ const newState = { ...state, sortBy: by, sortOrder: order, page: 1 };
233
+ setState(newState);
234
+ fetchData({}, newState);
235
+ },
236
+
237
+ loadMore: () => {
238
+ const newState = { ...state, page: state.page + 1 };
239
+ setState(newState);
240
+ fetchData({}, newState);
241
+ },
242
+
243
+ clearFilters: () => {
244
+ const newState = { ...state, filters: filters, page: 1 };
245
+ setState(newState);
246
+ fetchData({}, newState);
247
+ },
248
+
249
+ refresh: (addContext = { isRefresh: true }) => {
250
+ if (isLoadMore) {
251
+ // For loadMore, reset to page 1
252
+ const newState = { ...state, page: 1, items: [] };
253
+ setState(newState);
254
+ fetchData(addContext, newState);
255
+ } else {
256
+ // For pagination, keep current page
257
+ fetchData(addContext);
258
+ }
259
+ },
260
+
261
+ setFilters: (filters) => {
262
+ const newState = { ...state, filters, page: 1 };
263
+ setState(newState);
264
+ fetchData({}, newState);
265
+ },
266
+ updateItemById: (item, id) => {
267
+ const newItems = state.items.map((i) => {
268
+ if (i.id === id) {
269
+ return { ...i, ...item };
270
+ }
271
+ return i;
272
+ });
273
+ setState((prev) => ({ ...prev, items: newItems }));
274
+ },
275
+ setSelection: (selection) => setState((prev) => ({ ...prev, selection })),
276
+ }),
277
+ [fetchData, isLoadMore, state]
278
+ );
279
+
280
+ /**
281
+ * Memoized state for context to prevent unnecessary re-renders
282
+ */
283
+ const memoizedState = useMemo(
284
+ () => ({
285
+ data: state.items,
286
+ response: state.response,
287
+ error: state.error,
288
+ count: state.count,
289
+ selection: state.selection,
290
+ pagination: {
291
+ page: state.page,
292
+ perPage: state.perPage,
293
+ hasMore: state.items.length < state.count,
294
+ },
295
+ loader: {
296
+ isLoading: state.isLoading,
297
+ initialLoading: state.initializingState,
298
+ },
299
+ sort: { sortBy: state.sortBy, sortOrder: state.sortOrder },
300
+ hasActiveFilters: hasActiveFilters(state.filters, filters),
301
+ search: state.search,
302
+ filters: state.filters,
303
+ attrs: attrs || Object.keys(state.items[0] || {}),
304
+ isEmpty: state.items.length === 0,
305
+ ...handlers,
306
+ }),
307
+ [
308
+ state.items,
309
+ state.response,
310
+ state.error,
311
+ state.count,
312
+ state.selection,
313
+ state.page,
314
+ state.perPage,
315
+ state.isLoading,
316
+ state.initializingState,
317
+ state.sortBy,
318
+ state.sortOrder,
319
+ state.search,
320
+ state.filters,
321
+ handlers,
322
+ attrs,
323
+ ]
324
+ );
325
+
326
+ useEffect(() => {
327
+ if (!state.initializingState) {
328
+ return;
329
+ }
330
+ if (!initRef.current) {
331
+ initRef.current = true;
332
+
333
+ // Initialize state manager
334
+ if (stateManager?.init) {
335
+ const context = getContext(state);
336
+ stateManager.init(context);
337
+ }
338
+
339
+ if (!initialItems.length) handlers.setPage(state.page);
340
+ }
341
+ }, []);
342
+
343
+ // Watch for changes in filters prop and update internal state
344
+ useEffect(() => {
345
+ // Skip on initial mount (handled by initializeState)
346
+ if (!initRef.current) return;
347
+
348
+ // Only update if filters prop actually changed from internal state
349
+ if (!isEqual(filters, state.filters)) {
350
+ const newState = { ...state, filters, page: 1 };
351
+ setState(newState);
352
+ fetchData({}, newState);
353
+ }
354
+ }, [filters]);
355
+
356
+ // Update list state in context
357
+ useEffect(() => {
358
+ setListState(memoizedState);
359
+ }, [
360
+ setListState,
361
+ state.items,
362
+ state.count,
363
+ state.error,
364
+ state.isLoading,
365
+ state.selection,
366
+ state.page,
367
+ state.perPage,
368
+ state.sortBy,
369
+ state.sortOrder,
370
+ ]);
371
+
372
+ return typeof children === "function" ? children(memoizedState) : children;
373
+ };
374
+
375
+ export default ReactList;
@@ -0,0 +1,39 @@
1
+ import { memo, useCallback, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListLoadMore = memo(({ children }) => {
5
+ const { listState } = useListContext();
6
+ const { data, count, pagination, setPage, loader, error } = listState;
7
+ const { page, perPage } = pagination;
8
+ const { isLoading } = loader;
9
+
10
+ const hasMoreItems = useMemo(
11
+ () => page * perPage < count,
12
+ [page, perPage, count]
13
+ );
14
+
15
+ const loadMore = useCallback(() => {
16
+ if (hasMoreItems && !isLoading) {
17
+ setPage(page + 1);
18
+ }
19
+ }, [hasMoreItems, isLoading, setPage, page]);
20
+
21
+ const scope = useMemo(
22
+ () => ({
23
+ isLoading,
24
+ loadMore,
25
+ hasMoreItems,
26
+ }),
27
+ [isLoading, loadMore, hasMoreItems]
28
+ );
29
+
30
+ if (!data || data.length === 0) {
31
+ return null;
32
+ }
33
+
34
+ if (error) {
35
+ return null;
36
+ }
37
+
38
+ return children(scope);
39
+ });
@@ -0,0 +1,29 @@
1
+ import { memo, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListLoader = memo(({ children, position = "overlay" }) => {
5
+ const { listState } = useListContext();
6
+ const { loader } = listState;
7
+ const { isLoading, initializingState } = loader;
8
+
9
+ const scope = useMemo(
10
+ () => ({
11
+ isLoading,
12
+ }),
13
+ [isLoading]
14
+ );
15
+
16
+ if (!initializingState && !isLoading) {
17
+ return null;
18
+ }
19
+
20
+ return (
21
+ <div>
22
+ {children || (
23
+ <div>
24
+ <p>Loading...</p>
25
+ </div>
26
+ )}
27
+ </div>
28
+ );
29
+ });
@@ -0,0 +1,158 @@
1
+ import { memo, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListPagination = memo(
5
+ ({
6
+ children,
7
+ pageLinks = 5,
8
+ renderFirst,
9
+ renderPrev,
10
+ renderPages,
11
+ renderPage,
12
+ renderNext,
13
+ renderLast,
14
+ }) => {
15
+ const { listState } = useListContext();
16
+ const { data, count, pagination, setPage, loader, error } = listState;
17
+ const { page, perPage } = pagination;
18
+ const { initialLoading, isLoading } = loader;
19
+
20
+ const paginationState = useMemo(() => {
21
+ const pagesCount = Math.ceil(count / perPage);
22
+ const halfWay = Math.floor(pageLinks / 2);
23
+ const hasNext = page * perPage < count;
24
+ const hasPrev = page !== 1;
25
+ return { pagesCount, halfWay, hasNext, hasPrev };
26
+ }, [count, perPage, page, pageLinks]);
27
+
28
+ const pagesToDisplay = useMemo(() => {
29
+ const { pagesCount, halfWay } = paginationState;
30
+ const pages = Array.from({ length: Math.min(pageLinks, pagesCount) });
31
+
32
+ if (page <= halfWay) {
33
+ return pages.map((_, index) => index + 1);
34
+ } else if (pagesCount - page < halfWay) {
35
+ return pages.map((_, index) => pagesCount - index).reverse();
36
+ } else {
37
+ return pages.map((_, index) => page - halfWay + index);
38
+ }
39
+ }, [page, pageLinks, paginationState]);
40
+
41
+ const navigation = useMemo(
42
+ () => ({
43
+ prev: () => setPage(page - 1),
44
+ next: () => setPage(page + 1),
45
+ first: () => setPage(1),
46
+ last: () => setPage(paginationState.pagesCount),
47
+ setPage: (newPage) => setPage(newPage),
48
+ }),
49
+ [setPage, page, paginationState.pagesCount]
50
+ );
51
+
52
+ const scope = useMemo(
53
+ () => ({
54
+ page,
55
+ perPage,
56
+ count,
57
+ ...paginationState,
58
+ pagesToDisplay,
59
+ ...navigation,
60
+ }),
61
+ [page, perPage, count, paginationState, pagesToDisplay, navigation]
62
+ );
63
+
64
+ if (initialLoading) return null;
65
+
66
+ if (!data || data.length === 0) {
67
+ return null;
68
+ }
69
+
70
+ if (error) {
71
+ return null;
72
+ }
73
+
74
+ if (children) {
75
+ return children(scope);
76
+ }
77
+
78
+ return (
79
+ <div className="react-list-pagination">
80
+ {renderFirst ? (
81
+ renderFirst(scope)
82
+ ) : (
83
+ <button
84
+ type="button"
85
+ disabled={!paginationState.hasPrev}
86
+ onClick={navigation.first}
87
+ >
88
+ First
89
+ </button>
90
+ )}
91
+
92
+ {renderPrev ? (
93
+ renderPrev(scope)
94
+ ) : (
95
+ <button
96
+ type="button"
97
+ disabled={!paginationState.hasPrev}
98
+ onClick={navigation.prev}
99
+ >
100
+ Prev
101
+ </button>
102
+ )}
103
+
104
+ {renderPages ? (
105
+ renderPages(scope)
106
+ ) : (
107
+ <div>
108
+ {pagesToDisplay.map((pageNum) => {
109
+ const isActive = pageNum === page;
110
+ const pageScope = { ...scope, page: pageNum, isActive };
111
+
112
+ return renderPage ? (
113
+ renderPage(pageScope)
114
+ ) : (
115
+ <div key={`page-${pageNum}`}>
116
+ {isActive ? (
117
+ <span>{pageNum}</span>
118
+ ) : (
119
+ <button
120
+ type="button"
121
+ onClick={() => navigation.setPage(pageNum)}
122
+ >
123
+ {pageNum}
124
+ </button>
125
+ )}
126
+ </div>
127
+ );
128
+ })}
129
+ </div>
130
+ )}
131
+
132
+ {renderNext ? (
133
+ renderNext(scope)
134
+ ) : (
135
+ <button
136
+ type="button"
137
+ disabled={!paginationState.hasNext}
138
+ onClick={navigation.next}
139
+ >
140
+ Next
141
+ </button>
142
+ )}
143
+
144
+ {renderLast ? (
145
+ renderLast(scope)
146
+ ) : (
147
+ <button
148
+ type="button"
149
+ disabled={!paginationState.hasNext}
150
+ onClick={navigation.last}
151
+ >
152
+ Last
153
+ </button>
154
+ )}
155
+ </div>
156
+ );
157
+ }
158
+ );
@@ -0,0 +1,65 @@
1
+ import { memo, useCallback, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListPerPage = memo(
5
+ ({ children, options = [10, 25, 50, 100] }) => {
6
+ const { listState } = useListContext();
7
+ const { data, pagination, setPerPage, loader, error } = listState;
8
+ const { perPage } = pagination;
9
+ const { initialLoading } = loader;
10
+
11
+ const serializedOptions = useMemo(() => {
12
+ return options.map((item) => {
13
+ if (typeof item !== "object") {
14
+ return {
15
+ value: item,
16
+ label: item,
17
+ };
18
+ }
19
+ return item;
20
+ });
21
+ }, [options]);
22
+
23
+ const handlePerPageChange = useCallback(
24
+ (e) => {
25
+ setPerPage(Number(e.target.value));
26
+ },
27
+ [setPerPage]
28
+ );
29
+
30
+ const scope = useMemo(
31
+ () => ({
32
+ perPage,
33
+ setPerPage,
34
+ options: serializedOptions,
35
+ }),
36
+ [perPage, setPerPage, serializedOptions]
37
+ );
38
+
39
+ if (initialLoading) return null;
40
+
41
+ if (!data || data.length === 0) {
42
+ return null;
43
+ }
44
+
45
+ if (error) {
46
+ return null;
47
+ }
48
+
49
+ return (
50
+ <div className="react-list-per-page">
51
+ {children ? (
52
+ children(scope)
53
+ ) : (
54
+ <select value={perPage} onChange={handlePerPageChange}>
55
+ {serializedOptions.map((option) => (
56
+ <option key={`option-${option.value}`} value={option.value}>
57
+ {option.label} items per page
58
+ </option>
59
+ ))}
60
+ </select>
61
+ )}
62
+ </div>
63
+ );
64
+ }
65
+ );
@@ -0,0 +1,34 @@
1
+ import { memo, useCallback, useMemo } from "react";
2
+ import { useListContext } from "../context/list-provider";
3
+
4
+ export const ReactListRefresh = memo(({ children }) => {
5
+ const { listState } = useListContext();
6
+ const { loader, refresh } = listState;
7
+ const { isLoading, initialLoading } = loader;
8
+
9
+ const handleRefresh = useCallback(() => {
10
+ refresh({ isRefresh: true });
11
+ }, [refresh]);
12
+
13
+ const scope = useMemo(
14
+ () => ({
15
+ isLoading,
16
+ refresh: handleRefresh,
17
+ }),
18
+ [isLoading, handleRefresh]
19
+ );
20
+
21
+ if (initialLoading) return null;
22
+
23
+ if (children) {
24
+ return children(scope);
25
+ }
26
+
27
+ return (
28
+ <div className="react-list-refresh">
29
+ <button onClick={handleRefresh} disabled={isLoading}>
30
+ {isLoading ? "Loading..." : "Refresh"}
31
+ </button>
32
+ </div>
33
+ );
34
+ });