@angular-architects/ngrx-toolkit 20.0.1 → 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.
Files changed (84) hide show
  1. package/eslint.config.cjs +43 -0
  2. package/jest.config.ts +22 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +4 -21
  5. package/project.json +37 -0
  6. package/redux-connector/docs/README.md +131 -0
  7. package/redux-connector/index.ts +6 -0
  8. package/redux-connector/ng-package.json +5 -0
  9. package/redux-connector/src/lib/create-redux.ts +102 -0
  10. package/redux-connector/src/lib/model.ts +89 -0
  11. package/redux-connector/src/lib/rxjs-interop/redux-method.ts +66 -0
  12. package/redux-connector/src/lib/signal-redux-store.ts +59 -0
  13. package/redux-connector/src/lib/util.ts +22 -0
  14. package/src/index.ts +43 -0
  15. package/src/lib/assertions/assertions.ts +9 -0
  16. package/src/lib/devtools/features/with-disabled-name-indicies.ts +31 -0
  17. package/src/lib/devtools/features/with-glitch-tracking.ts +35 -0
  18. package/src/lib/devtools/features/with-mapper.ts +34 -0
  19. package/src/lib/devtools/internal/current-action-names.ts +1 -0
  20. package/src/lib/devtools/internal/default-tracker.ts +60 -0
  21. package/src/lib/devtools/internal/devtools-feature.ts +37 -0
  22. package/src/lib/devtools/internal/devtools-syncer.service.ts +202 -0
  23. package/src/lib/devtools/internal/glitch-tracker.service.ts +61 -0
  24. package/src/lib/devtools/internal/models.ts +29 -0
  25. package/src/lib/devtools/provide-devtools-config.ts +32 -0
  26. package/src/lib/devtools/rename-devtools-name.ts +21 -0
  27. package/src/lib/devtools/tests/action-name.spec.ts +48 -0
  28. package/src/lib/devtools/tests/basic.spec.ts +111 -0
  29. package/src/lib/devtools/tests/connecting.spec.ts +37 -0
  30. package/src/lib/devtools/tests/helpers.spec.ts +43 -0
  31. package/src/lib/devtools/tests/naming.spec.ts +216 -0
  32. package/src/lib/devtools/tests/provide-devtools-config.spec.ts +25 -0
  33. package/src/lib/devtools/tests/types.spec.ts +19 -0
  34. package/src/lib/devtools/tests/update-state.spec.ts +29 -0
  35. package/src/lib/devtools/tests/with-devtools.spec.ts +5 -0
  36. package/src/lib/devtools/tests/with-glitch-tracking.spec.ts +272 -0
  37. package/src/lib/devtools/tests/with-mapper.spec.ts +69 -0
  38. package/src/lib/devtools/update-state.ts +38 -0
  39. package/src/lib/devtools/with-dev-tools-stub.ts +6 -0
  40. package/src/lib/devtools/with-devtools.ts +81 -0
  41. package/src/lib/immutable-state/deep-freeze.ts +43 -0
  42. package/src/lib/immutable-state/is-dev-mode.ts +6 -0
  43. package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +278 -0
  44. package/src/lib/immutable-state/with-immutable-state.ts +150 -0
  45. package/src/lib/shared/prettify.ts +3 -0
  46. package/src/lib/shared/signal-store-models.ts +30 -0
  47. package/src/lib/shared/throw-if-null.ts +7 -0
  48. package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
  49. package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
  50. package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
  51. package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
  52. package/src/lib/storage-sync/internal/models.ts +62 -0
  53. package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
  54. package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
  55. package/src/lib/storage-sync/tests/with-storage-async.spec.ts +305 -0
  56. package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +273 -0
  57. package/src/lib/storage-sync/with-storage-sync.ts +236 -0
  58. package/src/lib/with-call-state.spec.ts +42 -0
  59. package/src/lib/with-call-state.ts +195 -0
  60. package/src/lib/with-conditional.spec.ts +125 -0
  61. package/src/lib/with-conditional.ts +74 -0
  62. package/src/lib/with-data-service.spec.ts +564 -0
  63. package/src/lib/with-data-service.ts +433 -0
  64. package/src/lib/with-feature-factory.spec.ts +69 -0
  65. package/src/lib/with-feature-factory.ts +56 -0
  66. package/src/lib/with-pagination.spec.ts +135 -0
  67. package/src/lib/with-pagination.ts +373 -0
  68. package/src/lib/with-redux.spec.ts +258 -0
  69. package/src/lib/with-redux.ts +387 -0
  70. package/src/lib/with-reset.spec.ts +112 -0
  71. package/src/lib/with-reset.ts +62 -0
  72. package/src/lib/with-undo-redo.spec.ts +274 -0
  73. package/src/lib/with-undo-redo.ts +200 -0
  74. package/src/test-setup.ts +6 -0
  75. package/tsconfig.json +29 -0
  76. package/tsconfig.lib.json +17 -0
  77. package/tsconfig.lib.prod.json +9 -0
  78. package/tsconfig.spec.json +17 -0
  79. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
  80. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
  81. package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1789
  82. package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
  83. package/index.d.ts +0 -949
  84. package/redux-connector/index.d.ts +0 -59
@@ -0,0 +1,433 @@
1
+ import { ProviderToken, Signal, computed, inject } from '@angular/core';
2
+ import {
3
+ EmptyFeatureResult,
4
+ SignalStoreFeature,
5
+ WritableStateSource,
6
+ patchState,
7
+ signalStoreFeature,
8
+ withComputed,
9
+ withMethods,
10
+ withState,
11
+ } from '@ngrx/signals';
12
+ import {
13
+ EntityId,
14
+ NamedEntityState,
15
+ addEntity,
16
+ removeEntity,
17
+ setAllEntities,
18
+ updateEntity,
19
+ } from '@ngrx/signals/entities';
20
+ import { EntityState } from './shared/signal-store-models';
21
+ import {
22
+ CallState,
23
+ NamedCallStateSlice,
24
+ getCallStateKeys,
25
+ setError,
26
+ setLoaded,
27
+ setLoading,
28
+ } from './with-call-state';
29
+
30
+ export type Filter = Record<string, unknown>;
31
+ export type Entity = { id: EntityId };
32
+
33
+ export interface DataService<E extends Entity, F extends Filter> {
34
+ load(filter: F): Promise<E[]>;
35
+
36
+ loadById(id: EntityId): Promise<E>;
37
+
38
+ create(entity: E): Promise<E>;
39
+
40
+ update(entity: E): Promise<E>;
41
+
42
+ updateAll(entity: E[]): Promise<E[]>;
43
+
44
+ delete(entity: E): Promise<void>;
45
+ }
46
+
47
+ export function capitalize(str: string): string {
48
+ return str ? str[0].toUpperCase() + str.substring(1) : str;
49
+ }
50
+
51
+ export function getDataServiceKeys(options: { collection?: string }) {
52
+ const filterKey = options.collection
53
+ ? `${options.collection}Filter`
54
+ : 'filter';
55
+ const selectedIdsKey = options.collection
56
+ ? `selected${capitalize(options.collection)}Ids`
57
+ : 'selectedIds';
58
+ const selectedEntitiesKey = options.collection
59
+ ? `selected${capitalize(options.collection)}Entities`
60
+ : 'selectedEntities';
61
+
62
+ const updateFilterKey = options.collection
63
+ ? `update${capitalize(options.collection)}Filter`
64
+ : 'updateFilter';
65
+ const updateSelectedKey = options.collection
66
+ ? `updateSelected${capitalize(options.collection)}Entities`
67
+ : 'updateSelected';
68
+ const loadKey = options.collection
69
+ ? `load${capitalize(options.collection)}Entities`
70
+ : 'load';
71
+
72
+ const currentKey = options.collection
73
+ ? `current${capitalize(options.collection)}`
74
+ : 'current';
75
+ const loadByIdKey = options.collection
76
+ ? `load${capitalize(options.collection)}ById`
77
+ : 'loadById';
78
+ const setCurrentKey = options.collection
79
+ ? `setCurrent${capitalize(options.collection)}`
80
+ : 'setCurrent';
81
+ const createKey = options.collection
82
+ ? `create${capitalize(options.collection)}`
83
+ : 'create';
84
+ const updateKey = options.collection
85
+ ? `update${capitalize(options.collection)}`
86
+ : 'update';
87
+ const updateAllKey = options.collection
88
+ ? `updateAll${capitalize(options.collection)}`
89
+ : 'updateAll';
90
+ const deleteKey = options.collection
91
+ ? `delete${capitalize(options.collection)}`
92
+ : 'delete';
93
+
94
+ // TODO: Take these from @ngrx/signals/entities, when they are exported
95
+ const entitiesKey = options.collection
96
+ ? `${options.collection}Entities`
97
+ : 'entities';
98
+ const entityMapKey = options.collection
99
+ ? `${options.collection}EntityMap`
100
+ : 'entityMap';
101
+ const idsKey = options.collection ? `${options.collection}Ids` : 'ids';
102
+
103
+ return {
104
+ filterKey,
105
+ selectedIdsKey,
106
+ selectedEntitiesKey,
107
+ updateFilterKey,
108
+ updateSelectedKey,
109
+ loadKey,
110
+ entitiesKey,
111
+ entityMapKey,
112
+ idsKey,
113
+
114
+ currentKey,
115
+ loadByIdKey,
116
+ setCurrentKey,
117
+ createKey,
118
+ updateKey,
119
+ updateAllKey,
120
+ deleteKey,
121
+ };
122
+ }
123
+
124
+ export type NamedDataServiceState<
125
+ E extends Entity,
126
+ F extends Filter,
127
+ Collection extends string,
128
+ > = {
129
+ [K in Collection as `${K}Filter`]: F;
130
+ } & {
131
+ [K in Collection as `selected${Capitalize<K>}Ids`]: Record<EntityId, boolean>;
132
+ } & {
133
+ [K in Collection as `current${Capitalize<K>}`]: E;
134
+ };
135
+
136
+ export type DataServiceState<E extends Entity, F extends Filter> = {
137
+ filter: F;
138
+ selectedIds: Record<EntityId, boolean>;
139
+ current: E;
140
+ };
141
+
142
+ export type DataServiceComputed<E extends Entity> = {
143
+ selectedEntities: Signal<E[]>;
144
+ };
145
+
146
+ export type NamedDataServiceComputed<
147
+ E extends Entity,
148
+ Collection extends string,
149
+ > = {
150
+ [K in Collection as `selected${Capitalize<K>}Entities`]: Signal<E[]>;
151
+ };
152
+
153
+ export type NamedDataServiceMethods<
154
+ E extends Entity,
155
+ F extends Filter,
156
+ Collection extends string,
157
+ > = {
158
+ [K in Collection as `update${Capitalize<K>}Filter`]: (filter: F) => void;
159
+ } & {
160
+ [K in Collection as `updateSelected${Capitalize<K>}Entities`]: (
161
+ id: EntityId,
162
+ selected: boolean,
163
+ ) => void;
164
+ } & {
165
+ [K in Collection as `load${Capitalize<K>}Entities`]: () => Promise<void>;
166
+ } & {
167
+ [K in Collection as `setCurrent${Capitalize<K>}`]: (entity: E) => void;
168
+ } & {
169
+ [K in Collection as `load${Capitalize<K>}ById`]: (
170
+ id: EntityId,
171
+ ) => Promise<void>;
172
+ } & {
173
+ [K in Collection as `create${Capitalize<K>}`]: (entity: E) => Promise<void>;
174
+ } & {
175
+ [K in Collection as `update${Capitalize<K>}`]: (entity: E) => Promise<void>;
176
+ } & {
177
+ [K in Collection as `updateAll${Capitalize<K>}`]: (
178
+ entity: E[],
179
+ ) => Promise<void>;
180
+ } & {
181
+ [K in Collection as `delete${Capitalize<K>}`]: (entity: E) => Promise<void>;
182
+ };
183
+
184
+ export type DataServiceMethods<E extends Entity, F extends Filter> = {
185
+ updateFilter: (filter: F) => void;
186
+ updateSelected: (id: EntityId, selected: boolean) => void;
187
+ load: () => Promise<void>;
188
+
189
+ setCurrent(entity: E): void;
190
+ loadById(id: EntityId): Promise<void>;
191
+ create(entity: E): Promise<void>;
192
+ update(entity: E): Promise<void>;
193
+ updateAll(entities: E[]): Promise<void>;
194
+ delete(entity: E): Promise<void>;
195
+ };
196
+
197
+ export function withDataService<
198
+ E extends Entity,
199
+ F extends Filter,
200
+ Collection extends string,
201
+ >(options: {
202
+ dataServiceType: ProviderToken<DataService<E, F>>;
203
+ filter: F;
204
+ collection: Collection;
205
+ }): SignalStoreFeature<
206
+ EmptyFeatureResult & {
207
+ state: NamedCallStateSlice<Collection> & NamedEntityState<E, Collection>;
208
+ },
209
+ {
210
+ state: NamedDataServiceState<E, F, Collection>;
211
+ props: NamedDataServiceComputed<E, Collection>;
212
+ methods: NamedDataServiceMethods<E, F, Collection>;
213
+ }
214
+ >;
215
+ export function withDataService<E extends Entity, F extends Filter>(options: {
216
+ dataServiceType: ProviderToken<DataService<E, F>>;
217
+ filter: F;
218
+ }): SignalStoreFeature<
219
+ EmptyFeatureResult & { state: { callState: CallState } & EntityState<E> },
220
+ {
221
+ state: DataServiceState<E, F>;
222
+ props: DataServiceComputed<E>;
223
+ methods: DataServiceMethods<E, F>;
224
+ }
225
+ >;
226
+
227
+ export function withDataService<
228
+ E extends Entity,
229
+ F extends Filter,
230
+ Collection extends string,
231
+ >(options: {
232
+ dataServiceType: ProviderToken<DataService<E, F>>;
233
+ filter: F;
234
+ collection?: Collection;
235
+ }): /* eslint-disable @typescript-eslint/no-explicit-any */
236
+ SignalStoreFeature<any, any> {
237
+ const { dataServiceType, filter, collection: prefix } = options;
238
+ const {
239
+ entitiesKey,
240
+ filterKey,
241
+ loadKey,
242
+ selectedEntitiesKey,
243
+ selectedIdsKey,
244
+ updateFilterKey,
245
+ updateSelectedKey,
246
+
247
+ currentKey,
248
+ createKey,
249
+ updateKey,
250
+ updateAllKey,
251
+ deleteKey,
252
+ loadByIdKey,
253
+ setCurrentKey,
254
+ } = getDataServiceKeys(options);
255
+
256
+ const { callStateKey } = getCallStateKeys({ collection: prefix });
257
+
258
+ return signalStoreFeature(
259
+ withState(() => ({
260
+ [filterKey]: filter,
261
+ [selectedIdsKey]: {} as Record<EntityId, boolean>,
262
+ [currentKey]: undefined as E | undefined,
263
+ })),
264
+ withComputed((store: Record<string, unknown>) => {
265
+ const entities = store[entitiesKey] as Signal<E[]>;
266
+ const selectedIds = store[selectedIdsKey] as Signal<
267
+ Record<EntityId, boolean>
268
+ >;
269
+
270
+ return {
271
+ [selectedEntitiesKey]: computed(() =>
272
+ entities().filter((e) => selectedIds()[e.id]),
273
+ ),
274
+ };
275
+ }),
276
+ withMethods(
277
+ (store: Record<string, unknown> & WritableStateSource<object>) => {
278
+ const dataService = inject(dataServiceType);
279
+ return {
280
+ [updateFilterKey]: (filter: F): void => {
281
+ patchState(store, { [filterKey]: filter });
282
+ },
283
+ [updateSelectedKey]: (id: EntityId, selected: boolean): void => {
284
+ patchState(store, (state: Record<string, unknown>) => ({
285
+ [selectedIdsKey]: {
286
+ ...(state[selectedIdsKey] as Record<EntityId, boolean>),
287
+ [id]: selected,
288
+ },
289
+ }));
290
+ },
291
+ [loadKey]: async (): Promise<void> => {
292
+ const filter = store[filterKey] as Signal<F>;
293
+ (() =>
294
+ store[callStateKey] && patchState(store, setLoading(prefix)))();
295
+
296
+ try {
297
+ const result = await dataService.load(filter());
298
+ patchState(
299
+ store,
300
+ prefix
301
+ ? setAllEntities(result, { collection: prefix })
302
+ : setAllEntities(result),
303
+ );
304
+ (() =>
305
+ store[callStateKey] && patchState(store, setLoaded(prefix)))();
306
+ } catch (e) {
307
+ (() =>
308
+ store[callStateKey] &&
309
+ patchState(store, setError(e, prefix)))();
310
+ throw e;
311
+ }
312
+ },
313
+ [loadByIdKey]: async (id: EntityId): Promise<void> => {
314
+ (() =>
315
+ store[callStateKey] && patchState(store, setLoading(prefix)))();
316
+
317
+ try {
318
+ const current = await dataService.loadById(id);
319
+ (() =>
320
+ store[callStateKey] && patchState(store, setLoaded(prefix)))();
321
+ patchState(store, { [currentKey]: current });
322
+ } catch (e) {
323
+ (() =>
324
+ store[callStateKey] &&
325
+ patchState(store, setError(e, prefix)))();
326
+ throw e;
327
+ }
328
+ },
329
+ [setCurrentKey]: (current: E): void => {
330
+ patchState(store, { [currentKey]: current });
331
+ },
332
+ [createKey]: async (entity: E): Promise<void> => {
333
+ patchState(store, { [currentKey]: entity });
334
+ (() =>
335
+ store[callStateKey] && patchState(store, setLoading(prefix)))();
336
+
337
+ try {
338
+ const created = await dataService.create(entity);
339
+ patchState(store, { [currentKey]: created });
340
+ patchState(
341
+ store,
342
+ prefix
343
+ ? addEntity(created, { collection: prefix })
344
+ : addEntity(created),
345
+ );
346
+ (() =>
347
+ store[callStateKey] && patchState(store, setLoaded(prefix)))();
348
+ } catch (e) {
349
+ (() =>
350
+ store[callStateKey] &&
351
+ patchState(store, setError(e, prefix)))();
352
+ throw e;
353
+ }
354
+ },
355
+ [updateKey]: async (entity: E): Promise<void> => {
356
+ patchState(store, { [currentKey]: entity });
357
+ (() =>
358
+ store[callStateKey] && patchState(store, setLoading(prefix)))();
359
+
360
+ try {
361
+ const updated = await dataService.update(entity);
362
+ patchState(store, { [currentKey]: updated });
363
+
364
+ const updateArg = {
365
+ id: updated.id,
366
+ changes: updated,
367
+ };
368
+
369
+ const updater = (collection: string) =>
370
+ updateEntity(updateArg, { collection });
371
+
372
+ patchState(
373
+ store,
374
+ prefix ? updater(prefix) : updateEntity(updateArg),
375
+ );
376
+ (() =>
377
+ store[callStateKey] && patchState(store, setLoaded(prefix)))();
378
+ } catch (e) {
379
+ (() =>
380
+ store[callStateKey] &&
381
+ patchState(store, setError(e, prefix)))();
382
+ throw e;
383
+ }
384
+ },
385
+ [updateAllKey]: async (entities: E[]): Promise<void> => {
386
+ (() =>
387
+ store[callStateKey] && patchState(store, setLoading(prefix)))();
388
+
389
+ try {
390
+ const result = await dataService.updateAll(entities);
391
+ patchState(
392
+ store,
393
+ prefix
394
+ ? setAllEntities(result, { collection: prefix })
395
+ : setAllEntities(result),
396
+ );
397
+ (() =>
398
+ store[callStateKey] && patchState(store, setLoaded(prefix)))();
399
+ } catch (e) {
400
+ (() =>
401
+ store[callStateKey] &&
402
+ patchState(store, setError(e, prefix)))();
403
+ throw e;
404
+ }
405
+ },
406
+ [deleteKey]: async (entity: E): Promise<void> => {
407
+ patchState(store, { [currentKey]: entity });
408
+ (() =>
409
+ store[callStateKey] && patchState(store, setLoading(prefix)))();
410
+
411
+ try {
412
+ await dataService.delete(entity);
413
+ patchState(store, { [currentKey]: undefined });
414
+ patchState(
415
+ store,
416
+ prefix
417
+ ? removeEntity(entity.id, { collection: prefix })
418
+ : removeEntity(entity.id),
419
+ );
420
+ (() =>
421
+ store[callStateKey] && patchState(store, setLoaded(prefix)))();
422
+ } catch (e) {
423
+ (() =>
424
+ store[callStateKey] &&
425
+ patchState(store, setError(e, prefix)))();
426
+ throw e;
427
+ }
428
+ },
429
+ };
430
+ },
431
+ ),
432
+ );
433
+ }
@@ -0,0 +1,69 @@
1
+ import { computed, Signal } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import {
4
+ getState,
5
+ patchState,
6
+ signalStore,
7
+ signalStoreFeature,
8
+ withComputed,
9
+ withMethods,
10
+ withState,
11
+ } from '@ngrx/signals';
12
+ import { lastValueFrom, of } from 'rxjs';
13
+ import { withFeatureFactory } from './with-feature-factory';
14
+
15
+ type User = {
16
+ id: number;
17
+ name: string;
18
+ };
19
+
20
+ function withMyEntity<Entity>(loadMethod: (id: number) => Promise<Entity>) {
21
+ return signalStoreFeature(
22
+ withState({
23
+ currentId: 1 as number | undefined,
24
+ entity: undefined as undefined | Entity,
25
+ }),
26
+ withMethods((store) => ({
27
+ async load(id: number) {
28
+ const entity = await loadMethod(1);
29
+ patchState(store, { entity, currentId: id });
30
+ },
31
+ })),
32
+ );
33
+ }
34
+
35
+ describe('withFeatureFactory', () => {
36
+ it('should allow a sum feature', () => {
37
+ function withSum(a: Signal<number>, b: Signal<number>) {
38
+ return signalStoreFeature(
39
+ withComputed(() => ({ sum: computed(() => a() + b()) })),
40
+ );
41
+ }
42
+ signalStore(
43
+ withState({ a: 1, b: 2 }),
44
+ withFeatureFactory((store) => withSum(store.a, store.b)),
45
+ );
46
+ });
47
+
48
+ it('should allow to pass elements from a SignalStore to a feature', async () => {
49
+ const UserStore = signalStore(
50
+ { providedIn: 'root' },
51
+ withMethods(() => ({
52
+ findById(id: number) {
53
+ return of({ id: 1, name: 'Konrad' });
54
+ },
55
+ })),
56
+ withFeatureFactory((store) => {
57
+ const loader = (id: number) => lastValueFrom(store.findById(id));
58
+ return withMyEntity<User>(loader);
59
+ }),
60
+ );
61
+
62
+ const userStore = TestBed.inject(UserStore);
63
+ await userStore.load(1);
64
+ expect(getState(userStore)).toEqual({
65
+ currentId: 1,
66
+ entity: { id: 1, name: 'Konrad' },
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,56 @@
1
+ import {
2
+ SignalStoreFeature,
3
+ SignalStoreFeatureResult,
4
+ StateSignals,
5
+ } from '@ngrx/signals';
6
+
7
+ type StoreForFactory<Input extends SignalStoreFeatureResult> = StateSignals<
8
+ Input['state']
9
+ > &
10
+ Input['props'] &
11
+ Input['methods'];
12
+
13
+ /**
14
+ * @deprecated Use `import { withFeature } from '@ngrx/signals'` instead, starting with `ngrx/signals` 19.1: https://ngrx.io/guide/signals/signal-store/custom-store-features#connecting-a-custom-feature-with-the-store
15
+ *
16
+ * Allows to pass properties, methods, or signals from a SignalStore
17
+ * to a feature.
18
+ *
19
+ * Typically, a `signalStoreFeature` can have input constraints on
20
+ *
21
+ * ```typescript
22
+ * function withSum(a: Signal<number>, b: Signal<number>) {
23
+ * return signalStoreFeature(
24
+ * withComputed(() => ({
25
+ * sum: computed(() => a() + b())
26
+ * }))
27
+ * );
28
+ * }
29
+ *
30
+ * signalStore(
31
+ * withState({ a: 1, b: 2 }),
32
+ * withFeatureFactory((store) => withSum(store.a, store.b))
33
+ * );
34
+ * ```
35
+ * @param factoryFn
36
+ */
37
+ export function withFeatureFactory<
38
+ Input extends SignalStoreFeatureResult,
39
+ Output extends SignalStoreFeatureResult,
40
+ >(
41
+ factoryFn: (
42
+ store: StoreForFactory<Input>,
43
+ ) => SignalStoreFeature<Input, Output>,
44
+ ): SignalStoreFeature<Input, Output> {
45
+ return (store) => {
46
+ const storeForFactory = {
47
+ ...store['stateSignals'],
48
+ ...store['props'],
49
+ ...store['methods'],
50
+ } as StoreForFactory<Input>;
51
+
52
+ const feature = factoryFn(storeForFactory);
53
+
54
+ return feature(store);
55
+ };
56
+ }
@@ -0,0 +1,135 @@
1
+ import { patchState, signalStore, type } from '@ngrx/signals';
2
+ import { setAllEntities, withEntities } from '@ngrx/signals/entities';
3
+ import {
4
+ createPageArray,
5
+ firstPage,
6
+ gotoPage,
7
+ nextPage,
8
+ previousPage,
9
+ setPageSize,
10
+ withPagination,
11
+ } from './with-pagination';
12
+
13
+ type Book = { id: number; title: string; author: string };
14
+ const generateBooks = (count = 10) => {
15
+ const books = [] as Book[];
16
+ for (let i = 1; i <= count; i++) {
17
+ books.push({ id: i, title: `Book ${i}`, author: `Author ${i}` });
18
+ }
19
+ return books;
20
+ };
21
+
22
+ describe('withPagination', () => {
23
+ it('should use and update a pagination', () => {
24
+ const Store = signalStore(
25
+ { protectedState: false },
26
+ withEntities({ entity: type<Book>() }),
27
+ withPagination(),
28
+ );
29
+
30
+ const store = new Store();
31
+
32
+ patchState(store, setAllEntities(generateBooks(55)));
33
+ expect(store.currentPage()).toBe(0);
34
+ expect(store.pageCount()).toBe(6);
35
+ });
36
+
37
+ it('should use and update a pagination with collection', () => {
38
+ const Store = signalStore(
39
+ { protectedState: false },
40
+ withEntities({ entity: type<Book>(), collection: 'books' }),
41
+ withPagination({ entity: type<Book>(), collection: 'books' }),
42
+ );
43
+
44
+ const store = new Store();
45
+
46
+ patchState(
47
+ store,
48
+ setAllEntities(generateBooks(55), { collection: 'books' }),
49
+ );
50
+
51
+ patchState(store, gotoPage(5, { collection: 'books' }));
52
+ expect(store.booksCurrentPage()).toBe(5);
53
+ expect(store.selectedPageBooksEntities().length).toBe(5);
54
+ expect(store.booksPageCount()).toBe(6);
55
+ });
56
+
57
+ it('should navigate through pages', () => {
58
+ const Store = signalStore(
59
+ { protectedState: false },
60
+ withEntities({ entity: type<Book>() }),
61
+ withPagination(),
62
+ );
63
+
64
+ const store = new Store();
65
+
66
+ patchState(store, setAllEntities(generateBooks(55)));
67
+ expect(store.currentPage()).toBe(0);
68
+
69
+ patchState(store, nextPage());
70
+ expect(store.currentPage()).toBe(1);
71
+
72
+ patchState(store, previousPage());
73
+ expect(store.currentPage()).toBe(0);
74
+
75
+ patchState(store, nextPage());
76
+ patchState(store, nextPage());
77
+ expect(store.currentPage()).toBe(2);
78
+
79
+ patchState(store, firstPage());
80
+ expect(store.currentPage()).toBe(0);
81
+ });
82
+
83
+ it('should set page size', () => {
84
+ const Store = signalStore(
85
+ { protectedState: false },
86
+ withEntities({ entity: type<Book>() }),
87
+ withPagination(),
88
+ );
89
+
90
+ const store = new Store();
91
+
92
+ patchState(store, setAllEntities(generateBooks(50)), setPageSize(10));
93
+ expect(store.pageCount()).toBe(5);
94
+
95
+ patchState(store, setPageSize(5));
96
+ expect(store.pageCount()).toBe(10);
97
+ });
98
+
99
+ it('should react on entity changes', () => {
100
+ const Store = signalStore(
101
+ { protectedState: false },
102
+ withEntities({ entity: type<Book>() }),
103
+ withPagination(),
104
+ );
105
+
106
+ const store = new Store();
107
+
108
+ patchState(store, setAllEntities(generateBooks(100)));
109
+
110
+ expect(store.pageCount()).toBe(10);
111
+
112
+ patchState(store, setAllEntities(generateBooks(20)));
113
+
114
+ expect(store.pageCount()).toBe(2);
115
+
116
+ patchState(store, setPageSize(5));
117
+
118
+ expect(store.pageCount()).toBe(4);
119
+ });
120
+
121
+ describe('internal pageNavigationArray', () => {
122
+ it('should return an array of page numbers', () => {
123
+ const pages = createPageArray(8, 10, 500, 7);
124
+ expect(pages).toEqual([
125
+ { label: 5, value: 5 },
126
+ { label: '...', value: 6 },
127
+ { label: 7, value: 7 },
128
+ { label: 8, value: 8 },
129
+ { label: 9, value: 9 },
130
+ { label: '...', value: 10 },
131
+ { label: 50, value: 50 },
132
+ ]);
133
+ });
134
+ });
135
+ });