@angular-architects/ngrx-toolkit 20.0.0 → 20.0.2
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/eslint.config.cjs +43 -0
- package/jest.config.ts +22 -0
- package/ng-package.json +7 -0
- package/package.json +4 -21
- package/project.json +37 -0
- package/redux-connector/docs/README.md +131 -0
- package/redux-connector/index.ts +6 -0
- package/redux-connector/ng-package.json +5 -0
- package/redux-connector/src/lib/create-redux.ts +102 -0
- package/redux-connector/src/lib/model.ts +89 -0
- package/redux-connector/src/lib/rxjs-interop/redux-method.ts +66 -0
- package/redux-connector/src/lib/signal-redux-store.ts +59 -0
- package/redux-connector/src/lib/util.ts +22 -0
- package/src/index.ts +43 -0
- package/src/lib/assertions/assertions.ts +9 -0
- package/src/lib/devtools/features/with-disabled-name-indicies.ts +31 -0
- package/src/lib/devtools/features/with-glitch-tracking.ts +35 -0
- package/src/lib/devtools/features/with-mapper.ts +34 -0
- package/src/lib/devtools/internal/current-action-names.ts +1 -0
- package/src/lib/devtools/internal/default-tracker.ts +60 -0
- package/src/lib/devtools/internal/devtools-feature.ts +37 -0
- package/src/lib/devtools/internal/devtools-syncer.service.ts +202 -0
- package/src/lib/devtools/internal/glitch-tracker.service.ts +61 -0
- package/src/lib/devtools/internal/models.ts +29 -0
- package/src/lib/devtools/provide-devtools-config.ts +32 -0
- package/src/lib/devtools/rename-devtools-name.ts +21 -0
- package/src/lib/devtools/tests/action-name.spec.ts +48 -0
- package/src/lib/devtools/tests/basic.spec.ts +111 -0
- package/src/lib/devtools/tests/connecting.spec.ts +37 -0
- package/src/lib/devtools/tests/helpers.spec.ts +43 -0
- package/src/lib/devtools/tests/naming.spec.ts +216 -0
- package/src/lib/devtools/tests/provide-devtools-config.spec.ts +25 -0
- package/src/lib/devtools/tests/types.spec.ts +19 -0
- package/src/lib/devtools/tests/update-state.spec.ts +29 -0
- package/src/lib/devtools/tests/with-devtools.spec.ts +5 -0
- package/src/lib/devtools/tests/with-glitch-tracking.spec.ts +272 -0
- package/src/lib/devtools/tests/with-mapper.spec.ts +69 -0
- package/src/lib/devtools/update-state.ts +38 -0
- package/src/lib/devtools/with-dev-tools-stub.ts +6 -0
- package/src/lib/devtools/with-devtools.ts +81 -0
- package/src/lib/immutable-state/deep-freeze.ts +43 -0
- package/src/lib/immutable-state/is-dev-mode.ts +6 -0
- package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +278 -0
- package/src/lib/immutable-state/with-immutable-state.ts +150 -0
- package/src/lib/shared/prettify.ts +3 -0
- package/src/lib/shared/signal-store-models.ts +30 -0
- package/src/lib/shared/throw-if-null.ts +7 -0
- package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
- package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
- package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
- package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
- package/src/lib/storage-sync/internal/models.ts +62 -0
- package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
- package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
- package/src/lib/storage-sync/tests/with-storage-async.spec.ts +305 -0
- package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +273 -0
- package/src/lib/storage-sync/with-storage-sync.ts +236 -0
- package/src/lib/with-call-state.spec.ts +42 -0
- package/src/lib/with-call-state.ts +195 -0
- package/src/lib/with-conditional.spec.ts +125 -0
- package/src/lib/with-conditional.ts +74 -0
- package/src/lib/with-data-service.spec.ts +564 -0
- package/src/lib/with-data-service.ts +433 -0
- package/src/lib/with-feature-factory.spec.ts +69 -0
- package/src/lib/with-feature-factory.ts +56 -0
- package/src/lib/with-pagination.spec.ts +135 -0
- package/src/lib/with-pagination.ts +373 -0
- package/src/lib/with-redux.spec.ts +258 -0
- package/src/lib/with-redux.ts +387 -0
- package/src/lib/with-reset.spec.ts +112 -0
- package/src/lib/with-reset.ts +62 -0
- package/src/lib/with-undo-redo.spec.ts +274 -0
- package/src/lib/with-undo-redo.ts +200 -0
- package/src/test-setup.ts +6 -0
- package/tsconfig.json +29 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +17 -0
- package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
- package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
- package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1780
- package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
- package/index.d.ts +0 -938
- package/redux-connector/index.d.ts +0 -59
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/** With pagination comes in two flavors the first one is local pagination or in memory pagination. For example we have 2000 items which we want
|
|
2
|
+
* to display in a table and the response payload is small enough to be stored in the memory. But we can not display all 2000 items at once
|
|
3
|
+
* so we need to paginate the data. The second flavor is server side pagination where the response payload is too large to be stored in the memory
|
|
4
|
+
* and we need to fetch the data from the server in chunks. In the second case we 'could' also cache the data in the memory but that could lead to
|
|
5
|
+
* other problems like memory leaks and stale data. So we will not cache the data in the memory in the second case.
|
|
6
|
+
* This feature implements the local pagination.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Signal, computed } from '@angular/core';
|
|
10
|
+
import {
|
|
11
|
+
EmptyFeatureResult,
|
|
12
|
+
SignalStoreFeature,
|
|
13
|
+
signalStoreFeature,
|
|
14
|
+
withComputed,
|
|
15
|
+
withState,
|
|
16
|
+
} from '@ngrx/signals';
|
|
17
|
+
import { capitalize } from './with-data-service';
|
|
18
|
+
|
|
19
|
+
// This is a virtual page which is can be used to create a pagination control
|
|
20
|
+
export type Page = { label: string | number; value: number };
|
|
21
|
+
|
|
22
|
+
export type NamedPaginationServiceState<E, Collection extends string> = {
|
|
23
|
+
[K in Collection as `selectedPage${Capitalize<K>}Entities`]: Array<E>;
|
|
24
|
+
} & {
|
|
25
|
+
[K in Collection as `${Lowercase<K>}CurrentPage`]: number;
|
|
26
|
+
} & {
|
|
27
|
+
[K in Collection as `${Lowercase<K>}PageSize`]: number;
|
|
28
|
+
} & {
|
|
29
|
+
[K in Collection as `${Lowercase<K>}TotalCount`]: number;
|
|
30
|
+
} & {
|
|
31
|
+
[K in Collection as `${Lowercase<K>}PageCount`]: number;
|
|
32
|
+
} & {
|
|
33
|
+
[K in Collection as `${Lowercase<K>}PageNavigationArray`]: number;
|
|
34
|
+
} & {
|
|
35
|
+
[K in Collection as `${Lowercase<K>}PageNavigationArrayMax`]: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type NamedPaginationServiceSignals<E, Collection extends string> = {
|
|
39
|
+
[K in Collection as `selectedPage${Capitalize<K>}Entities`]: Signal<E[]>;
|
|
40
|
+
} & {
|
|
41
|
+
[K in Collection as `${Lowercase<K>}CurrentPage`]: Signal<number>;
|
|
42
|
+
} & {
|
|
43
|
+
[K in Collection as `${Lowercase<K>}PageSize`]: Signal<number>;
|
|
44
|
+
} & {
|
|
45
|
+
[K in Collection as `${Lowercase<K>}TotalCount`]: Signal<number>;
|
|
46
|
+
} & {
|
|
47
|
+
[K in Collection as `${Lowercase<K>}PageCount`]: Signal<number>;
|
|
48
|
+
} & {
|
|
49
|
+
[K in Collection as `${Lowercase<K>}PageNavigationArray`]: Signal<Page[]>;
|
|
50
|
+
} & {
|
|
51
|
+
[K in Collection as `${Lowercase<K>}PageNavigationArrayMax`]: Signal<number>;
|
|
52
|
+
} & {
|
|
53
|
+
[K in Collection as `hasNext${Capitalize<K>}Page`]: Signal<boolean>;
|
|
54
|
+
} & {
|
|
55
|
+
[K in Collection as `hasPrevious${Capitalize<K>}Page`]: Signal<boolean>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type PaginationServiceState<E> = {
|
|
59
|
+
selectedPageEntities: Array<E>;
|
|
60
|
+
currentPage: number;
|
|
61
|
+
pageSize: number;
|
|
62
|
+
totalCount: number;
|
|
63
|
+
pageCount: number;
|
|
64
|
+
pageNavigationArray: Page[];
|
|
65
|
+
pageNavigationArrayMax: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type PaginationServiceSignals<E> = {
|
|
69
|
+
selectedPageEntities: Signal<E[]>;
|
|
70
|
+
currentPage: Signal<number>;
|
|
71
|
+
pageSize: Signal<number>;
|
|
72
|
+
totalCount: Signal<number>;
|
|
73
|
+
pageCount: Signal<number>;
|
|
74
|
+
pageNavigationArray: Signal<Page[]>;
|
|
75
|
+
pageNavigationArrayMax: Signal<number>;
|
|
76
|
+
hasNextPage: Signal<boolean>;
|
|
77
|
+
hasPreviousPage: Signal<boolean>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type PageState<Collection extends string | undefined> =
|
|
81
|
+
Collection extends string
|
|
82
|
+
? {
|
|
83
|
+
[K in Collection as `${Lowercase<K>}CurrentPage`]: number;
|
|
84
|
+
}
|
|
85
|
+
: { currentPage: number };
|
|
86
|
+
|
|
87
|
+
type PageSizeState<Collection extends string | undefined> =
|
|
88
|
+
Collection extends string
|
|
89
|
+
? {
|
|
90
|
+
[K in Collection as `${Lowercase<K>}PageSize`]: number;
|
|
91
|
+
}
|
|
92
|
+
: { pageSize: number };
|
|
93
|
+
|
|
94
|
+
export type SetPaginationState<
|
|
95
|
+
E,
|
|
96
|
+
Collection extends string | undefined,
|
|
97
|
+
> = Collection extends string
|
|
98
|
+
? NamedPaginationServiceState<E, Collection>
|
|
99
|
+
: PaginationServiceState<E>;
|
|
100
|
+
|
|
101
|
+
export function withPagination<E, Collection extends string>(options: {
|
|
102
|
+
entity: E;
|
|
103
|
+
collection: Collection;
|
|
104
|
+
}): SignalStoreFeature<
|
|
105
|
+
EmptyFeatureResult,
|
|
106
|
+
EmptyFeatureResult & {
|
|
107
|
+
state: NamedPaginationServiceState<E, Collection>;
|
|
108
|
+
props: NamedPaginationServiceSignals<E, Collection>;
|
|
109
|
+
}
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
export function withPagination<E>(): SignalStoreFeature<
|
|
113
|
+
EmptyFeatureResult,
|
|
114
|
+
EmptyFeatureResult & {
|
|
115
|
+
state: PaginationServiceState<E>;
|
|
116
|
+
props: PaginationServiceSignals<E>;
|
|
117
|
+
}
|
|
118
|
+
>;
|
|
119
|
+
|
|
120
|
+
export function withPagination<E, Collection extends string>(options?: {
|
|
121
|
+
entity: E;
|
|
122
|
+
collection: Collection;
|
|
123
|
+
}): SignalStoreFeature {
|
|
124
|
+
const {
|
|
125
|
+
pageKey,
|
|
126
|
+
pageSizeKey,
|
|
127
|
+
entitiesKey,
|
|
128
|
+
selectedPageEntitiesKey,
|
|
129
|
+
totalCountKey,
|
|
130
|
+
pageCountKey,
|
|
131
|
+
pageNavigationArrayMaxKey,
|
|
132
|
+
pageNavigationArrayKey,
|
|
133
|
+
hasNextPageKey,
|
|
134
|
+
hasPreviousPageKey,
|
|
135
|
+
} = createPaginationKeys<Collection>(options);
|
|
136
|
+
|
|
137
|
+
return signalStoreFeature(
|
|
138
|
+
withState({
|
|
139
|
+
[pageKey]: 0,
|
|
140
|
+
[pageSizeKey]: 10,
|
|
141
|
+
[pageNavigationArrayMaxKey]: 7,
|
|
142
|
+
}),
|
|
143
|
+
withComputed((store: Record<string, unknown>) => {
|
|
144
|
+
const entities = store[entitiesKey] as Signal<E[]>;
|
|
145
|
+
const page = store[pageKey] as Signal<number>;
|
|
146
|
+
const pageSize = store[pageSizeKey] as Signal<number>;
|
|
147
|
+
const pageNavigationArrayMax = store[
|
|
148
|
+
pageNavigationArrayMaxKey
|
|
149
|
+
] as Signal<number>;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
// The derived enitites which are displayed on the current page
|
|
153
|
+
[selectedPageEntitiesKey]: computed<E[]>(() => {
|
|
154
|
+
const pageSizeValue = pageSize();
|
|
155
|
+
const pageValue = page();
|
|
156
|
+
|
|
157
|
+
return entities().slice(
|
|
158
|
+
pageValue * pageSizeValue,
|
|
159
|
+
(pageValue + 1) * pageSizeValue,
|
|
160
|
+
) as E[];
|
|
161
|
+
}),
|
|
162
|
+
[totalCountKey]: computed(() => entities().length),
|
|
163
|
+
[pageCountKey]: computed(() => {
|
|
164
|
+
const totalCountValue = entities().length;
|
|
165
|
+
const pageSizeValue = pageSize();
|
|
166
|
+
|
|
167
|
+
if (totalCountValue === 0) {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Math.ceil(totalCountValue / pageSizeValue);
|
|
172
|
+
}),
|
|
173
|
+
[pageNavigationArrayKey]: computed(() =>
|
|
174
|
+
createPageArray(
|
|
175
|
+
page(),
|
|
176
|
+
pageSize(),
|
|
177
|
+
entities().length,
|
|
178
|
+
pageNavigationArrayMax(),
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
|
|
182
|
+
[hasNextPageKey]: computed(() => {
|
|
183
|
+
return page() < pageSize();
|
|
184
|
+
}),
|
|
185
|
+
|
|
186
|
+
[hasPreviousPageKey]: computed(() => {
|
|
187
|
+
return page() > 1;
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function gotoPage<Collection extends string>(
|
|
195
|
+
page: number,
|
|
196
|
+
options?: {
|
|
197
|
+
collection: Collection;
|
|
198
|
+
},
|
|
199
|
+
): PageState<Collection> {
|
|
200
|
+
const { pageKey } = createPaginationKeys<Collection>(options);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
[pageKey]: page,
|
|
204
|
+
} as PageState<Collection>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function setPageSize<Collection extends string>(
|
|
208
|
+
pageSize: number,
|
|
209
|
+
options?: {
|
|
210
|
+
collection: Collection;
|
|
211
|
+
},
|
|
212
|
+
) {
|
|
213
|
+
const { pageSizeKey } = createPaginationKeys<Collection>(options);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
[pageSizeKey]: pageSize,
|
|
217
|
+
} as PageSizeState<Collection>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type SetPageState<Collection extends string | undefined> = (
|
|
221
|
+
state: PageState<Collection>,
|
|
222
|
+
) => PageState<Collection>;
|
|
223
|
+
|
|
224
|
+
export function nextPage<Collection extends string>(options?: {
|
|
225
|
+
collection: Collection;
|
|
226
|
+
}): SetPageState<Collection> {
|
|
227
|
+
const { pageKey } = createPaginationKeys<Collection>(options);
|
|
228
|
+
|
|
229
|
+
return (state: Record<string, number>) => {
|
|
230
|
+
const currentPage = state[pageKey];
|
|
231
|
+
|
|
232
|
+
return { [pageKey]: currentPage + 1 } as PageState<Collection>;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function previousPage<Collection extends string>(options?: {
|
|
237
|
+
collection: Collection;
|
|
238
|
+
}): SetPageState<Collection> {
|
|
239
|
+
const { pageKey } = createPaginationKeys<Collection>(options);
|
|
240
|
+
|
|
241
|
+
return (state: Record<string, number>) => {
|
|
242
|
+
const currentPage = state[pageKey];
|
|
243
|
+
|
|
244
|
+
return { [pageKey]: currentPage - 1 } as PageState<Collection>;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function firstPage<Collection extends string>(options?: {
|
|
249
|
+
collection: Collection;
|
|
250
|
+
}) {
|
|
251
|
+
const { pageKey } = createPaginationKeys<Collection>(options);
|
|
252
|
+
|
|
253
|
+
return { [pageKey]: 0 } as PageState<Collection>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function setMaxPageNavigationArrayItems<E, Collection extends string>(
|
|
257
|
+
maxPageNavigationArrayItems: number,
|
|
258
|
+
options?: {
|
|
259
|
+
collection: Collection;
|
|
260
|
+
},
|
|
261
|
+
): Partial<SetPaginationState<E, Collection>> {
|
|
262
|
+
const { pageNavigationArrayMaxKey } =
|
|
263
|
+
createPaginationKeys<Collection>(options);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
[pageNavigationArrayMaxKey]: maxPageNavigationArrayItems,
|
|
267
|
+
} as Partial<SetPaginationState<E, Collection>>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createPaginationKeys<Collection extends string>(
|
|
271
|
+
options: { collection: Collection } | undefined,
|
|
272
|
+
) {
|
|
273
|
+
const entitiesKey = options?.collection
|
|
274
|
+
? `${options.collection}Entities`
|
|
275
|
+
: 'entities';
|
|
276
|
+
|
|
277
|
+
const selectedPageEntitiesKey = options?.collection
|
|
278
|
+
? `selectedPage${capitalize(options?.collection)}Entities`
|
|
279
|
+
: 'selectedPageEntities';
|
|
280
|
+
|
|
281
|
+
const pageKey = options?.collection
|
|
282
|
+
? `${options.collection}CurrentPage`
|
|
283
|
+
: 'currentPage';
|
|
284
|
+
|
|
285
|
+
const pageSizeKey = options?.collection
|
|
286
|
+
? `${options.collection}PageSize`
|
|
287
|
+
: 'pageSize';
|
|
288
|
+
|
|
289
|
+
const totalCountKey = options?.collection
|
|
290
|
+
? `${options.collection}TotalCount`
|
|
291
|
+
: 'totalCount';
|
|
292
|
+
|
|
293
|
+
const pageCountKey = options?.collection
|
|
294
|
+
? `${options.collection}PageCount`
|
|
295
|
+
: 'pageCount';
|
|
296
|
+
|
|
297
|
+
const pageNavigationArrayMaxKey = options?.collection
|
|
298
|
+
? `${options.collection}PageNavigationArrayMax`
|
|
299
|
+
: 'pageNavigationArrayMax';
|
|
300
|
+
|
|
301
|
+
const pageNavigationArrayKey = options?.collection
|
|
302
|
+
? `${options.collection}PageNavigationArray`
|
|
303
|
+
: 'pageNavigationArray';
|
|
304
|
+
|
|
305
|
+
const hasNextPageKey = options?.collection
|
|
306
|
+
? `hasNext${capitalize(options.collection)}Page`
|
|
307
|
+
: 'hasNextPage';
|
|
308
|
+
|
|
309
|
+
const hasPreviousPageKey = options?.collection
|
|
310
|
+
? `hasPrevious${capitalize(options.collection)}Page`
|
|
311
|
+
: 'hasPreviousPage';
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
pageKey,
|
|
315
|
+
pageSizeKey,
|
|
316
|
+
entitiesKey,
|
|
317
|
+
selectedPageEntitiesKey,
|
|
318
|
+
totalCountKey,
|
|
319
|
+
pageCountKey,
|
|
320
|
+
pageNavigationArrayKey,
|
|
321
|
+
pageNavigationArrayMaxKey,
|
|
322
|
+
hasNextPageKey,
|
|
323
|
+
hasPreviousPageKey,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function createPageArray(
|
|
328
|
+
currentPage: number,
|
|
329
|
+
itemsPerPage: number,
|
|
330
|
+
totalItems: number,
|
|
331
|
+
paginationRange: number,
|
|
332
|
+
): Page[] {
|
|
333
|
+
// Convert paginationRange to number in case it's a string
|
|
334
|
+
paginationRange = +paginationRange;
|
|
335
|
+
|
|
336
|
+
// Calculate total number of pages
|
|
337
|
+
const totalPages = Math.max(Math.ceil(totalItems / itemsPerPage), 1);
|
|
338
|
+
const halfWay = Math.ceil(paginationRange / 2);
|
|
339
|
+
|
|
340
|
+
const isStart = currentPage <= halfWay;
|
|
341
|
+
const isEnd = totalPages - halfWay < currentPage;
|
|
342
|
+
const isMiddle = !isStart && !isEnd;
|
|
343
|
+
|
|
344
|
+
const ellipsesNeeded = paginationRange < totalPages;
|
|
345
|
+
const pages: Page[] = [];
|
|
346
|
+
|
|
347
|
+
for (let i = 1; i <= totalPages && i <= paginationRange; i++) {
|
|
348
|
+
let pageNumber = i;
|
|
349
|
+
|
|
350
|
+
if (i === paginationRange) {
|
|
351
|
+
pageNumber = totalPages;
|
|
352
|
+
} else if (ellipsesNeeded) {
|
|
353
|
+
if (isEnd) {
|
|
354
|
+
pageNumber = totalPages - paginationRange + i;
|
|
355
|
+
} else if (isMiddle) {
|
|
356
|
+
pageNumber = currentPage - halfWay + i;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const openingEllipsesNeeded = i === 2 && (isMiddle || isEnd);
|
|
361
|
+
const closingEllipsesNeeded =
|
|
362
|
+
i === paginationRange - 1 && (isMiddle || isStart);
|
|
363
|
+
|
|
364
|
+
const label =
|
|
365
|
+
ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)
|
|
366
|
+
? '...'
|
|
367
|
+
: pageNumber;
|
|
368
|
+
|
|
369
|
+
pages.push({ label, value: pageNumber });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return pages;
|
|
373
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HttpClient,
|
|
3
|
+
HttpParams,
|
|
4
|
+
provideHttpClient,
|
|
5
|
+
} from '@angular/common/http';
|
|
6
|
+
import {
|
|
7
|
+
HttpTestingController,
|
|
8
|
+
provideHttpClientTesting,
|
|
9
|
+
} from '@angular/common/http/testing';
|
|
10
|
+
import { inject } from '@angular/core';
|
|
11
|
+
import { TestBed } from '@angular/core/testing';
|
|
12
|
+
import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
|
|
13
|
+
import { map, switchMap } from 'rxjs';
|
|
14
|
+
import {
|
|
15
|
+
createEffects,
|
|
16
|
+
createReducer,
|
|
17
|
+
noPayload,
|
|
18
|
+
payload,
|
|
19
|
+
withRedux,
|
|
20
|
+
} from './with-redux';
|
|
21
|
+
|
|
22
|
+
interface Flight {
|
|
23
|
+
id: number;
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
delayed: boolean;
|
|
27
|
+
date: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let currentId = 1;
|
|
31
|
+
|
|
32
|
+
const createFlight = (flight: Partial<Flight> = {}) => {
|
|
33
|
+
return {
|
|
34
|
+
...{
|
|
35
|
+
id: currentId++,
|
|
36
|
+
from: 'Vienna',
|
|
37
|
+
to: 'London',
|
|
38
|
+
delayed: false,
|
|
39
|
+
date: new Date(2024, 0, 1),
|
|
40
|
+
},
|
|
41
|
+
...flight,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('with redux', () => {
|
|
46
|
+
it('should load flights', () => {
|
|
47
|
+
TestBed.configureTestingModule({
|
|
48
|
+
providers: [provideHttpClient(), provideHttpClientTesting()],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
TestBed.runInInjectionContext(() => {
|
|
52
|
+
const controller = TestBed.inject(HttpTestingController);
|
|
53
|
+
const FlightsStore = signalStore(
|
|
54
|
+
withState({ flights: [] as Flight[] }),
|
|
55
|
+
withRedux({
|
|
56
|
+
actions: {
|
|
57
|
+
public: {
|
|
58
|
+
loadFlights: payload<{ from: string; to: string }>(),
|
|
59
|
+
delayFirst: noPayload,
|
|
60
|
+
},
|
|
61
|
+
private: {
|
|
62
|
+
flightsLoaded: payload<{ flights: Flight[] }>(),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
reducer: (actions, on) => {
|
|
67
|
+
on(actions.flightsLoaded, (state, { flights }) => {
|
|
68
|
+
patchState(state, { flights });
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
effects: (actions, create) => {
|
|
73
|
+
const httpClient = inject(HttpClient);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
loadFlights$: create(actions.loadFlights).pipe(
|
|
77
|
+
switchMap(({ from, to }) => {
|
|
78
|
+
return httpClient.get<Flight[]>(
|
|
79
|
+
'https://www.angulararchitects.io',
|
|
80
|
+
{
|
|
81
|
+
params: new HttpParams().set('from', from).set('to', to),
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}),
|
|
85
|
+
map((flights) => actions.flightsLoaded({ flights })),
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const flightsStore = new FlightsStore();
|
|
93
|
+
flightsStore.loadFlights({ from: 'Vienna', to: 'London' });
|
|
94
|
+
const flight = createFlight();
|
|
95
|
+
controller
|
|
96
|
+
.expectOne((req) =>
|
|
97
|
+
req.url.startsWith('https://www.angulararchitects.io'),
|
|
98
|
+
)
|
|
99
|
+
.flush([flight]);
|
|
100
|
+
|
|
101
|
+
expect(flightsStore.flights()).toEqual([flight]);
|
|
102
|
+
|
|
103
|
+
controller.verify();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should allow a noPayload action to call without parameters', () => {
|
|
108
|
+
const FlightsStore = signalStore(
|
|
109
|
+
withState({ flights: [] as Flight[] }),
|
|
110
|
+
withRedux({
|
|
111
|
+
actions: {
|
|
112
|
+
init: noPayload,
|
|
113
|
+
},
|
|
114
|
+
reducer() {
|
|
115
|
+
return {};
|
|
116
|
+
},
|
|
117
|
+
effects() {
|
|
118
|
+
return {};
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const flightStore = TestBed.configureTestingModule({
|
|
124
|
+
providers: [FlightsStore],
|
|
125
|
+
}).inject(FlightsStore);
|
|
126
|
+
|
|
127
|
+
flightStore.init();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should allow multiple effects listening to the same action', () => {
|
|
131
|
+
const FlightsStore = signalStore(
|
|
132
|
+
withState({ flights: [] as Flight[], effect1: false, effect2: false }),
|
|
133
|
+
withRedux({
|
|
134
|
+
actions: {
|
|
135
|
+
init: noPayload,
|
|
136
|
+
updateEffect1: payload<{ value: boolean }>(),
|
|
137
|
+
updateEffect2: payload<{ value: boolean }>(),
|
|
138
|
+
},
|
|
139
|
+
reducer(actions, on) {
|
|
140
|
+
on(actions.updateEffect1, (state, { value }) => {
|
|
141
|
+
patchState(state, { effect1: value });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
on(actions.updateEffect2, (state, { value }) => {
|
|
145
|
+
patchState(state, { effect2: value });
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
effects(actions, create) {
|
|
149
|
+
return {
|
|
150
|
+
init1$: create(actions.init).pipe(
|
|
151
|
+
map(() => actions.updateEffect1({ value: true })),
|
|
152
|
+
),
|
|
153
|
+
init2$: create(actions.init).pipe(
|
|
154
|
+
map(() => actions.updateEffect2({ value: true })),
|
|
155
|
+
),
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const flightStore = TestBed.configureTestingModule({
|
|
162
|
+
providers: [FlightsStore],
|
|
163
|
+
}).inject(FlightsStore);
|
|
164
|
+
|
|
165
|
+
flightStore.init();
|
|
166
|
+
|
|
167
|
+
expect(flightStore.effect1()).toBe(true);
|
|
168
|
+
expect(flightStore.effect2()).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should be possible to separate actions, reducer and effects', () => {
|
|
172
|
+
interface FlightState {
|
|
173
|
+
flights: Flight[];
|
|
174
|
+
effect1: boolean;
|
|
175
|
+
effect2: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const initialState: FlightState = {
|
|
179
|
+
flights: [],
|
|
180
|
+
effect1: false,
|
|
181
|
+
effect2: false,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const actions = {
|
|
185
|
+
init: noPayload,
|
|
186
|
+
updateEffect1: payload<{ value: boolean }>(),
|
|
187
|
+
updateEffect2: payload<{ value: boolean }>(),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const effects = createEffects(actions, (actions, create) => {
|
|
191
|
+
return {
|
|
192
|
+
init1$: create(actions.init).pipe(
|
|
193
|
+
map(() => actions.updateEffect1({ value: true })),
|
|
194
|
+
),
|
|
195
|
+
init2$: create(actions.init).pipe(
|
|
196
|
+
map(() => actions.updateEffect2({ value: true })),
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const reducer = createReducer<FlightState, typeof actions>(
|
|
202
|
+
(actions, on) => {
|
|
203
|
+
on(actions.updateEffect1, (state, { value }) => {
|
|
204
|
+
patchState(state, { effect1: value });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
on(actions.updateEffect2, (state, { value }) => {
|
|
208
|
+
patchState(state, { effect2: value });
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const FlightsStore = signalStore(
|
|
214
|
+
withState(initialState),
|
|
215
|
+
withRedux({
|
|
216
|
+
actions,
|
|
217
|
+
effects,
|
|
218
|
+
reducer,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const flightStore = TestBed.configureTestingModule({
|
|
223
|
+
providers: [FlightsStore],
|
|
224
|
+
}).inject(FlightsStore);
|
|
225
|
+
|
|
226
|
+
flightStore.init();
|
|
227
|
+
|
|
228
|
+
expect(flightStore.effect1()).toBe(true);
|
|
229
|
+
expect(flightStore.effect2()).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should not override methods defined before', () => {
|
|
233
|
+
const FlightsStore = signalStore(
|
|
234
|
+
withMethods(() => ({
|
|
235
|
+
sayHi() {
|
|
236
|
+
return 'hi';
|
|
237
|
+
},
|
|
238
|
+
})),
|
|
239
|
+
withRedux({
|
|
240
|
+
actions: {
|
|
241
|
+
init: noPayload,
|
|
242
|
+
},
|
|
243
|
+
reducer() {
|
|
244
|
+
return {};
|
|
245
|
+
},
|
|
246
|
+
effects() {
|
|
247
|
+
return {};
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const flightStore = TestBed.configureTestingModule({
|
|
253
|
+
providers: [FlightsStore],
|
|
254
|
+
}).inject(FlightsStore);
|
|
255
|
+
|
|
256
|
+
expect(flightStore.sayHi()).toBe('hi');
|
|
257
|
+
});
|
|
258
|
+
});
|