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