@angular-architects/ngrx-toolkit 19.2.3 → 19.4.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.
Files changed (123) 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/{index.d.ts → src/index.ts} +36 -4
  15. package/src/lib/assertions/assertions.ts +9 -0
  16. package/{lib/devtools/features/with-disabled-name-indicies.d.ts → src/lib/devtools/features/with-disabled-name-indicies.ts} +5 -1
  17. package/{lib/devtools/features/with-glitch-tracking.d.ts → src/lib/devtools/features/with-glitch-tracking.ts} +6 -1
  18. package/{lib/devtools/features/with-mapper.d.ts → src/lib/devtools/features/with-mapper.ts} +7 -1
  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/{lib/devtools/provide-devtools-config.d.ts → src/lib/devtools/provide-devtools-config.ts} +16 -4
  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/{lib/devtools/with-dev-tools-stub.d.ts → src/lib/devtools/with-dev-tools-stub.ts} +2 -1
  40. package/src/lib/devtools/with-devtools.ts +81 -0
  41. package/src/lib/flattening-operator.ts +42 -0
  42. package/src/lib/immutable-state/deep-freeze.ts +43 -0
  43. package/src/lib/immutable-state/is-dev-mode.ts +6 -0
  44. package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +260 -0
  45. package/src/lib/immutable-state/with-immutable-state.ts +115 -0
  46. package/src/lib/mutation/http-mutation.spec.ts +473 -0
  47. package/src/lib/mutation/http-mutation.ts +172 -0
  48. package/src/lib/mutation/mutation.ts +26 -0
  49. package/src/lib/mutation/rx-mutation.spec.ts +594 -0
  50. package/src/lib/mutation/rx-mutation.ts +208 -0
  51. package/src/lib/shared/prettify.ts +3 -0
  52. package/{lib/shared/signal-store-models.d.ts → src/lib/shared/signal-store-models.ts} +8 -4
  53. package/src/lib/shared/throw-if-null.ts +7 -0
  54. package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
  55. package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
  56. package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
  57. package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
  58. package/src/lib/storage-sync/internal/models.ts +62 -0
  59. package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
  60. package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
  61. package/src/lib/storage-sync/tests/with-storage-async.spec.ts +308 -0
  62. package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +268 -0
  63. package/src/lib/storage-sync/with-storage-sync.ts +233 -0
  64. package/src/lib/with-call-state.spec.ts +42 -0
  65. package/src/lib/with-call-state.ts +195 -0
  66. package/src/lib/with-conditional.spec.ts +125 -0
  67. package/{lib/with-conditional.d.ts → src/lib/with-conditional.ts} +31 -7
  68. package/src/lib/with-data-service.spec.ts +564 -0
  69. package/src/lib/with-data-service.ts +433 -0
  70. package/src/lib/with-feature-factory.spec.ts +69 -0
  71. package/{lib/with-feature-factory.d.ts → src/lib/with-feature-factory.ts} +32 -4
  72. package/src/lib/with-mutations.spec.ts +537 -0
  73. package/src/lib/with-mutations.ts +146 -0
  74. package/src/lib/with-pagination.spec.ts +90 -0
  75. package/src/lib/with-pagination.ts +353 -0
  76. package/src/lib/with-redux.spec.ts +258 -0
  77. package/src/lib/with-redux.ts +387 -0
  78. package/src/lib/with-reset.spec.ts +112 -0
  79. package/src/lib/with-reset.ts +62 -0
  80. package/src/lib/with-undo-redo.spec.ts +287 -0
  81. package/src/lib/with-undo-redo.ts +199 -0
  82. package/src/test-setup.ts +8 -0
  83. package/tsconfig.json +29 -0
  84. package/tsconfig.lib.json +17 -0
  85. package/tsconfig.lib.prod.json +9 -0
  86. package/tsconfig.spec.json +17 -0
  87. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
  88. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
  89. package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1787
  90. package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
  91. package/lib/assertions/assertions.d.ts +0 -2
  92. package/lib/devtools/internal/current-action-names.d.ts +0 -1
  93. package/lib/devtools/internal/default-tracker.d.ts +0 -13
  94. package/lib/devtools/internal/devtools-feature.d.ts +0 -24
  95. package/lib/devtools/internal/devtools-syncer.service.d.ts +0 -35
  96. package/lib/devtools/internal/glitch-tracker.service.d.ts +0 -18
  97. package/lib/devtools/internal/models.d.ts +0 -24
  98. package/lib/devtools/rename-devtools-name.d.ts +0 -7
  99. package/lib/devtools/update-state.d.ts +0 -15
  100. package/lib/devtools/with-devtools.d.ts +0 -24
  101. package/lib/immutable-state/deep-freeze.d.ts +0 -11
  102. package/lib/immutable-state/is-dev-mode.d.ts +0 -1
  103. package/lib/immutable-state/with-immutable-state.d.ts +0 -60
  104. package/lib/shared/throw-if-null.d.ts +0 -1
  105. package/lib/storage-sync/features/with-indexed-db.d.ts +0 -2
  106. package/lib/storage-sync/features/with-local-storage.d.ts +0 -3
  107. package/lib/storage-sync/internal/indexeddb.service.d.ts +0 -29
  108. package/lib/storage-sync/internal/local-storage.service.d.ts +0 -8
  109. package/lib/storage-sync/internal/models.d.ts +0 -45
  110. package/lib/storage-sync/internal/session-storage.service.d.ts +0 -8
  111. package/lib/storage-sync/with-storage-sync.d.ts +0 -45
  112. package/lib/with-call-state.d.ts +0 -58
  113. package/lib/with-data-service.d.ts +0 -109
  114. package/lib/with-pagination.d.ts +0 -98
  115. package/lib/with-redux.d.ts +0 -147
  116. package/lib/with-reset.d.ts +0 -29
  117. package/lib/with-undo-redo.d.ts +0 -31
  118. package/redux-connector/index.d.ts +0 -2
  119. package/redux-connector/src/lib/create-redux.d.ts +0 -13
  120. package/redux-connector/src/lib/model.d.ts +0 -40
  121. package/redux-connector/src/lib/rxjs-interop/redux-method.d.ts +0 -14
  122. package/redux-connector/src/lib/signal-redux-store.d.ts +0 -11
  123. package/redux-connector/src/lib/util.d.ts +0 -5
@@ -0,0 +1,233 @@
1
+ import { isPlatformServer } from '@angular/common';
2
+ import { computed, effect, inject, PLATFORM_ID, signal } from '@angular/core';
3
+ import {
4
+ EmptyFeatureResult,
5
+ getState,
6
+ signalStoreFeature,
7
+ SignalStoreFeature,
8
+ SignalStoreFeatureResult,
9
+ watchState,
10
+ withHooks,
11
+ withMethods,
12
+ withProps,
13
+ } from '@ngrx/signals';
14
+ import {
15
+ withLocalStorage,
16
+ withSessionStorage,
17
+ } from './features/with-local-storage';
18
+ import {
19
+ AsyncFeatureResult,
20
+ AsyncStorageStrategy,
21
+ SYNC_STATUS,
22
+ SyncFeatureResult,
23
+ SyncStorageStrategy,
24
+ } from './internal/models';
25
+
26
+ export type SyncConfig<State> = {
27
+ /**
28
+ * The key which is used to access the storage.
29
+ */
30
+ key: string;
31
+ /**
32
+ * Flag indicating if the store should read from storage on init and write to storage on every state change.
33
+ *
34
+ * `true` by default
35
+ */
36
+ autoSync?: boolean;
37
+ /**
38
+ * Function to select that portion of the state which should be stored.
39
+ *
40
+ * Returns the whole state object by default
41
+ */
42
+ select?: (state: State) => unknown;
43
+ /**
44
+ * Function used to parse the state coming from storage.
45
+ *
46
+ * `JSON.parse()` by default
47
+ */
48
+ parse?: (stateString: string) => State;
49
+ /**
50
+ * Function used to transform the state into a string representation.
51
+ *
52
+ * `JSON.stringify()` by default
53
+ */
54
+ stringify?: (state: State) => string;
55
+
56
+ /**
57
+ * @deprecated Use {@link withSessionStorage} instead.
58
+ * Factory function used to switch to sessionStorage.
59
+ *
60
+ * `localStorage` by default
61
+ */
62
+ storage?: () => Storage;
63
+ };
64
+
65
+ // only key
66
+ export function withStorageSync<Input extends SignalStoreFeatureResult>(
67
+ key: string,
68
+ ): SignalStoreFeature<Input, SyncFeatureResult>;
69
+
70
+ // key + indexeddb
71
+ export function withStorageSync<Input extends SignalStoreFeatureResult>(
72
+ key: string,
73
+ storageStrategy: AsyncStorageStrategy<Input['state']>,
74
+ ): SignalStoreFeature<Input, AsyncFeatureResult>;
75
+
76
+ // key + localStorage(or sessionStorage)
77
+ export function withStorageSync<Input extends SignalStoreFeatureResult>(
78
+ key: string,
79
+ storageStrategy: SyncStorageStrategy<Input['state']>,
80
+ ): SignalStoreFeature<Input, SyncFeatureResult>;
81
+
82
+ // config + localStorage
83
+ export function withStorageSync<Input extends SignalStoreFeatureResult>(
84
+ config: SyncConfig<Input['state']>,
85
+ ): SignalStoreFeature<Input, SyncFeatureResult>;
86
+
87
+ // config + indexeddb
88
+ export function withStorageSync<Input extends SignalStoreFeatureResult>(
89
+ config: SyncConfig<Input['state']>,
90
+ storageStrategy: AsyncStorageStrategy<Input['state']>,
91
+ ): SignalStoreFeature<Input, AsyncFeatureResult>;
92
+
93
+ // config + localStorage(or sessionStorage)
94
+ export function withStorageSync<Input extends SignalStoreFeatureResult>(
95
+ config: SyncConfig<Input['state']>,
96
+ storageStrategy: SyncStorageStrategy<Input['state']>,
97
+ ): SignalStoreFeature<Input, SyncFeatureResult>;
98
+
99
+ export function withStorageSync<Input extends SignalStoreFeatureResult>(
100
+ configOrKey: SyncConfig<Input['state']> | string,
101
+ storageStrategy?:
102
+ | AsyncStorageStrategy<Input['state']>
103
+ | SyncStorageStrategy<Input['state']>,
104
+ ): SignalStoreFeature<
105
+ Input,
106
+ EmptyFeatureResult & (SyncFeatureResult | AsyncFeatureResult)
107
+ > {
108
+ if (
109
+ typeof configOrKey !== 'string' &&
110
+ configOrKey.storage &&
111
+ storageStrategy
112
+ ) {
113
+ throw new Error(
114
+ 'You can either pass a storage strategy or a config with storage, but not both.',
115
+ );
116
+ }
117
+ const config: Required<SyncConfig<Input['state']>> = {
118
+ autoSync: true,
119
+ select: (state: Input['state']) => state,
120
+ parse: JSON.parse,
121
+ stringify: JSON.stringify,
122
+ storage: () => localStorage,
123
+ ...(typeof configOrKey === 'string' ? { key: configOrKey } : configOrKey),
124
+ };
125
+
126
+ const factory =
127
+ storageStrategy ??
128
+ (config.storage() === localStorage
129
+ ? withLocalStorage()
130
+ : withSessionStorage());
131
+
132
+ if (factory.type === 'sync') {
133
+ return createSyncStorageSync(factory, config);
134
+ } else {
135
+ return createAsyncStorageSync(factory, config);
136
+ }
137
+ }
138
+
139
+ function createSyncStorageSync<Input extends SignalStoreFeatureResult>(
140
+ factory: SyncStorageStrategy<Input['state']>,
141
+ config: Required<SyncConfig<Input['state']>>,
142
+ ) {
143
+ return signalStoreFeature(
144
+ withMethods((store, platformId = inject(PLATFORM_ID)) => {
145
+ return factory(config, store, isPlatformServer(platformId));
146
+ }),
147
+ withHooks({
148
+ onInit(store, platformId = inject(PLATFORM_ID)) {
149
+ if (isPlatformServer(platformId)) {
150
+ return;
151
+ }
152
+
153
+ if (config.autoSync) {
154
+ store.readFromStorage();
155
+ watchState(store, () => store.writeToStorage());
156
+ }
157
+ },
158
+ }),
159
+ ) satisfies SignalStoreFeature<EmptyFeatureResult, SyncFeatureResult>;
160
+ }
161
+
162
+ function createAsyncStorageSync<Input extends SignalStoreFeatureResult>(
163
+ factory: AsyncStorageStrategy<Input['state']>,
164
+ config: Required<SyncConfig<Input['state']>>,
165
+ ) {
166
+ return signalStoreFeature(
167
+ withProps(() => {
168
+ const props = {
169
+ /*
170
+ // we need to have that as property (and not state)
171
+ // Otherwise the state watcher fires when updating the sync status
172
+ */
173
+ [SYNC_STATUS]: signal<'idle' | 'syncing' | 'synced'>('idle'),
174
+ };
175
+
176
+ const resolves = [] as (() => void)[];
177
+
178
+ effect(() => {
179
+ const syncStatus = props[SYNC_STATUS]();
180
+ if (syncStatus === 'synced') {
181
+ resolves.forEach((resolve) => resolve());
182
+ resolves.splice(0, resolves.length);
183
+ }
184
+ });
185
+
186
+ return {
187
+ ...props,
188
+ isSynced: computed(() => props[SYNC_STATUS]() === 'synced'),
189
+ whenSynced: () =>
190
+ new Promise<void>((resolve) => {
191
+ if (props[SYNC_STATUS]() === 'synced') {
192
+ resolve();
193
+ } else {
194
+ resolves.push(resolve);
195
+ }
196
+ }),
197
+ };
198
+ }),
199
+ withMethods((store, platformId = inject(PLATFORM_ID)) => {
200
+ return factory(config, store, isPlatformServer(platformId));
201
+ }),
202
+ withHooks({
203
+ async onInit(store, platformId = inject(PLATFORM_ID)) {
204
+ if (isPlatformServer(platformId)) {
205
+ return;
206
+ }
207
+
208
+ const initialState = getState(store);
209
+ if (config.autoSync) {
210
+ let startWatching = false;
211
+ watchState(store, () => {
212
+ if (!startWatching) {
213
+ if (getState(store) === initialState) {
214
+ return;
215
+ }
216
+
217
+ console.warn(
218
+ `Writing to Store (${config.key}) happened before the state was initially read from storage.`,
219
+ 'Please ensure that the store is not in syncing state via `store.whenSynced()` before writing to the state.',
220
+ 'Alternatively, you can disable autoSync by passing `autoSync: false` in the config.',
221
+ );
222
+ return;
223
+ }
224
+ return store.writeToStorage();
225
+ });
226
+
227
+ await store.readFromStorage();
228
+ startWatching = true;
229
+ }
230
+ },
231
+ }),
232
+ ) satisfies SignalStoreFeature<EmptyFeatureResult, AsyncFeatureResult>;
233
+ }
@@ -0,0 +1,42 @@
1
+ import { patchState, signalStore } from '@ngrx/signals';
2
+ import { setLoaded, setLoading, withCallState } from './with-call-state';
3
+
4
+ describe('withCallState', () => {
5
+ it('should use and update a callState', () => {
6
+ const DataStore = signalStore({ protectedState: false }, withCallState());
7
+ const dataStore = new DataStore();
8
+
9
+ patchState(dataStore, setLoading());
10
+
11
+ expect(dataStore.callState()).toBe('loading');
12
+ expect(dataStore.loading()).toBe(true);
13
+ });
14
+
15
+ it('should use the callState for a collection', () => {
16
+ const DataStore = signalStore(
17
+ { protectedState: false },
18
+ withCallState({ collection: 'entities' }),
19
+ );
20
+ const dataStore = new DataStore();
21
+
22
+ patchState(dataStore, setLoaded('entities'));
23
+
24
+ expect(dataStore.entitiesCallState()).toBe('loaded');
25
+ expect(dataStore.entitiesLoaded()).toBe(true);
26
+ });
27
+
28
+ it('should use the callState for multiple collections with an array', () => {
29
+ const DataStore = signalStore(
30
+ { protectedState: false },
31
+ withCallState({ collections: ['entities', 'products'] }),
32
+ );
33
+ const dataStore = new DataStore();
34
+
35
+ patchState(dataStore, setLoaded('entities'), setLoaded('products'));
36
+
37
+ expect(dataStore.entitiesCallState()).toBe('loaded');
38
+ expect(dataStore.productsCallState()).toBe('loaded');
39
+ expect(dataStore.entitiesLoaded()).toBe(true);
40
+ expect(dataStore.productsLoaded()).toBe(true);
41
+ });
42
+ });
@@ -0,0 +1,195 @@
1
+ import { Signal, computed } from '@angular/core';
2
+ import {
3
+ EmptyFeatureResult,
4
+ SignalStoreFeature,
5
+ signalStoreFeature,
6
+ withComputed,
7
+ withState,
8
+ } from '@ngrx/signals';
9
+
10
+ export type CallState = 'init' | 'loading' | 'loaded' | { error: string };
11
+
12
+ export type CallStateSlice = {
13
+ callState: CallState;
14
+ };
15
+
16
+ export type NamedCallStateSlice<Collection extends string> = {
17
+ [K in keyof CallStateSlice as Collection extends ''
18
+ ? `${Collection}${K}`
19
+ : `${Collection}${Capitalize<K>}`]: CallStateSlice[K];
20
+ };
21
+
22
+ export type CallStateSignals = {
23
+ loading: Signal<boolean>;
24
+ loaded: Signal<boolean>;
25
+ error: Signal<string | null>;
26
+ };
27
+
28
+ export type NamedCallStateSignals<Prop extends string> = {
29
+ [K in keyof CallStateSignals as Prop extends ''
30
+ ? `${Prop}${K}`
31
+ : `${Prop}${Capitalize<K>}`]: CallStateSignals[K];
32
+ };
33
+
34
+ export type SetCallState<Prop extends string | undefined> = Prop extends string
35
+ ? NamedCallStateSlice<Prop>
36
+ : CallStateSlice;
37
+
38
+ export function deriveCallStateKeys<Collection extends string>(
39
+ collection?: Collection,
40
+ ) {
41
+ return {
42
+ callStateKey: collection ? `${collection}CallState` : 'callState',
43
+ loadingKey: collection ? `${collection}Loading` : 'loading',
44
+ loadedKey: collection ? `${collection}Loaded` : 'loaded',
45
+ errorKey: collection ? `${collection}Error` : 'error',
46
+ };
47
+ }
48
+
49
+ export function getCallStateKeys(config?: { collection?: string }) {
50
+ const prop = config?.collection;
51
+ return deriveCallStateKeys(prop);
52
+ }
53
+
54
+ export function getCollectionArray(
55
+ config: { collection?: string } | { collections?: string[] },
56
+ ) {
57
+ return 'collections' in config
58
+ ? config.collections
59
+ : 'collection' in config && config.collection
60
+ ? [config.collection]
61
+ : undefined;
62
+ }
63
+
64
+ export function withCallState<Collection extends string>(config: {
65
+ collections: Collection[];
66
+ }): SignalStoreFeature<
67
+ EmptyFeatureResult,
68
+ EmptyFeatureResult & {
69
+ state: NamedCallStateSlice<Collection>;
70
+ props: NamedCallStateSignals<Collection>;
71
+ }
72
+ >;
73
+ export function withCallState<Collection extends string>(config: {
74
+ collection: Collection;
75
+ }): SignalStoreFeature<
76
+ EmptyFeatureResult,
77
+ EmptyFeatureResult & {
78
+ state: NamedCallStateSlice<Collection>;
79
+ props: NamedCallStateSignals<Collection>;
80
+ }
81
+ >;
82
+ export function withCallState(): SignalStoreFeature<
83
+ EmptyFeatureResult,
84
+ EmptyFeatureResult & {
85
+ state: CallStateSlice;
86
+ props: CallStateSignals;
87
+ }
88
+ >;
89
+ export function withCallState<Collection extends string>(
90
+ config?:
91
+ | {
92
+ collection: Collection;
93
+ }
94
+ | {
95
+ collections: Collection[];
96
+ },
97
+ ): SignalStoreFeature {
98
+ return signalStoreFeature(
99
+ withState(() => {
100
+ if (!config) {
101
+ return { callState: 'init' };
102
+ }
103
+ const collections = getCollectionArray(config);
104
+ if (collections) {
105
+ return collections.reduce(
106
+ (acc, cur) => ({
107
+ ...acc,
108
+ ...{ [cur ? `${cur}CallState` : 'callState']: 'init' },
109
+ }),
110
+ {},
111
+ );
112
+ }
113
+
114
+ return { callState: 'init' };
115
+ }),
116
+ withComputed((state: Record<string, Signal<unknown>>) => {
117
+ if (config) {
118
+ const collections = getCollectionArray(config);
119
+ if (collections) {
120
+ return collections.reduce<Record<string, Signal<unknown>>>(
121
+ (acc, cur: string) => {
122
+ const { callStateKey, errorKey, loadedKey, loadingKey } =
123
+ deriveCallStateKeys(cur);
124
+ const callState = state[callStateKey] as Signal<CallState>;
125
+ return {
126
+ ...acc,
127
+ [loadingKey]: computed(() => callState() === 'loading'),
128
+ [loadedKey]: computed(() => callState() === 'loaded'),
129
+ [errorKey]: computed(() => {
130
+ const v = callState();
131
+ return typeof v === 'object' ? v.error : null;
132
+ }),
133
+ };
134
+ },
135
+ {},
136
+ );
137
+ }
138
+ }
139
+ const { callStateKey, errorKey, loadedKey, loadingKey } =
140
+ deriveCallStateKeys();
141
+ const callState = state[callStateKey] as Signal<CallState>;
142
+ return {
143
+ [loadingKey]: computed(() => callState() === 'loading'),
144
+ [loadedKey]: computed(() => callState() === 'loaded'),
145
+ [errorKey]: computed(() => {
146
+ const v = callState();
147
+ return typeof v === 'object' ? v.error : null;
148
+ }),
149
+ };
150
+ }),
151
+ );
152
+ }
153
+
154
+ export function setLoading<Prop extends string | undefined = undefined>(
155
+ prop?: Prop,
156
+ ): SetCallState<Prop> {
157
+ if (prop) {
158
+ return { [`${prop}CallState`]: 'loading' } as SetCallState<Prop>;
159
+ }
160
+
161
+ return { callState: 'loading' } as SetCallState<Prop>;
162
+ }
163
+
164
+ export function setLoaded<Prop extends string | undefined = undefined>(
165
+ prop?: Prop,
166
+ ): SetCallState<Prop> {
167
+ if (prop) {
168
+ return { [`${prop}CallState`]: 'loaded' } as SetCallState<Prop>;
169
+ } else {
170
+ return { callState: 'loaded' } as SetCallState<Prop>;
171
+ }
172
+ }
173
+
174
+ export function setError<Prop extends string | undefined = undefined>(
175
+ error: unknown,
176
+ prop?: Prop,
177
+ ): SetCallState<Prop> {
178
+ let errorMessage: string;
179
+
180
+ if (!error) {
181
+ errorMessage = '';
182
+ } else if (typeof error === 'object' && 'message' in error) {
183
+ errorMessage = String(error.message);
184
+ } else {
185
+ errorMessage = String(error);
186
+ }
187
+
188
+ if (prop) {
189
+ return {
190
+ [`${prop}CallState`]: { error: errorMessage },
191
+ } as SetCallState<Prop>;
192
+ } else {
193
+ return { callState: { error: errorMessage } } as SetCallState<Prop>;
194
+ }
195
+ }
@@ -0,0 +1,125 @@
1
+ import { inject, InjectionToken } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import {
4
+ getState,
5
+ patchState,
6
+ signalStore,
7
+ signalStoreFeature,
8
+ withHooks,
9
+ withMethods,
10
+ withState,
11
+ } from '@ngrx/signals';
12
+ import { withDevtools } from './devtools/with-devtools';
13
+ import { emptyFeature, withConditional } from './with-conditional';
14
+
15
+ describe('withConditional', () => {
16
+ const withUser = signalStoreFeature(
17
+ withState({ id: 0, name: '' }),
18
+ withHooks((store) => ({
19
+ onInit() {
20
+ patchState(store, { id: 1, name: 'Konrad' });
21
+ },
22
+ })),
23
+ );
24
+
25
+ const withFakeUser = signalStoreFeature(
26
+ withState({ id: 0, name: 'Tommy Fake' }),
27
+ );
28
+
29
+ for (const isReal of [true, false]) {
30
+ it(`should ${isReal ? '' : 'not '} enable withUser`, () => {
31
+ const REAL_USER_TOKEN = new InjectionToken('REAL_USER', {
32
+ providedIn: 'root',
33
+ factory: () => isReal,
34
+ });
35
+ const UserStore = signalStore(
36
+ { providedIn: 'root' },
37
+ withConditional(() => inject(REAL_USER_TOKEN), withUser, withFakeUser),
38
+ );
39
+ const userStore = TestBed.inject(UserStore);
40
+
41
+ if (isReal) {
42
+ expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' });
43
+ } else {
44
+ expect(getState(userStore)).toEqual({ id: 0, name: 'Tommy Fake' });
45
+ }
46
+ });
47
+ }
48
+
49
+ it(`should access the store`, () => {
50
+ const UserStore = signalStore(
51
+ { providedIn: 'root' },
52
+ withMethods(() => ({
53
+ useRealUser: () => true,
54
+ })),
55
+ withConditional((store) => store.useRealUser(), withUser, withFakeUser),
56
+ );
57
+ const userStore = TestBed.inject(UserStore);
58
+
59
+ expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' });
60
+ });
61
+
62
+ it('should be used inside a signalStoreFeature', () => {
63
+ const withConditionalUser = (activate: boolean) =>
64
+ signalStoreFeature(
65
+ withConditional(() => activate, withUser, withFakeUser),
66
+ );
67
+
68
+ const UserStore = signalStore(
69
+ { providedIn: 'root' },
70
+ withConditionalUser(true),
71
+ );
72
+ const userStore = TestBed.inject(UserStore);
73
+
74
+ expect(getState(userStore)).toEqual({ id: 1, name: 'Konrad' });
75
+ });
76
+
77
+ it('should ensure that both features return the same type', () => {
78
+ const withUser = signalStoreFeature(
79
+ withState({ id: 0, name: '' }),
80
+ withHooks((store) => ({
81
+ onInit() {
82
+ patchState(store, { id: 1, name: 'Konrad' });
83
+ },
84
+ })),
85
+ );
86
+
87
+ const withFakeUser = signalStoreFeature(
88
+ withState({ id: 0, firstname: 'Tommy Fake' }),
89
+ );
90
+
91
+ // @ts-expect-error withFakeUser has a different state shape
92
+ signalStore(withConditional(() => true, withUser, withFakeUser));
93
+ });
94
+
95
+ it('should also work with empty features', () => {
96
+ signalStore(
97
+ withConditional(
98
+ () => true,
99
+ withDevtools('dummy'),
100
+ signalStoreFeature(withState({})),
101
+ ),
102
+ );
103
+ });
104
+
105
+ it('should work with `emptyFeature` if falsy is skipped', () => {
106
+ signalStore(
107
+ withConditional(
108
+ () => true,
109
+ signalStoreFeature(withState({})),
110
+ emptyFeature,
111
+ ),
112
+ );
113
+ });
114
+
115
+ it('should not work with `emptyFeature` if feature is not empty', () => {
116
+ signalStore(
117
+ withConditional(
118
+ () => true,
119
+ // @ts-expect-error feature is not empty
120
+ () => signalStoreFeature(withState({ x: 1 })),
121
+ emptyFeature,
122
+ ),
123
+ );
124
+ });
125
+ });
@@ -1,4 +1,11 @@
1
- import { SignalStoreFeature, SignalStoreFeatureResult, StateSignals } from '@ngrx/signals';
1
+ import {
2
+ signalStoreFeature,
3
+ SignalStoreFeature,
4
+ SignalStoreFeatureResult,
5
+ StateSignals,
6
+ withState,
7
+ } from '@ngrx/signals';
8
+
2
9
  /**
3
10
  * `withConditional` activates a feature based on a given condition.
4
11
  *
@@ -42,9 +49,26 @@ import { SignalStoreFeature, SignalStoreFeatureResult, StateSignals } from '@ngr
42
49
  * @param featureIfFalse - The feature to activate if the condition evaluates to `false`.
43
50
  * @returns A `SignalStoreFeature` that applies the selected feature based on the condition.
44
51
  */
45
- export declare function withConditional<Input extends SignalStoreFeatureResult, Output extends SignalStoreFeatureResult>(condition: (store: StateSignals<Input['state']> & Input['props'] & Input['methods']) => boolean, featureIfTrue: SignalStoreFeature<NoInfer<Input>, Output>, featureIfFalse: SignalStoreFeature<NoInfer<Input>, NoInfer<Output>>): SignalStoreFeature<Input, Output>;
46
- export declare const emptyFeature: SignalStoreFeature<import("@ngrx/signals").EmptyFeatureResult, {
47
- state: {};
48
- props: {};
49
- methods: {};
50
- }>;
52
+ export function withConditional<
53
+ Input extends SignalStoreFeatureResult,
54
+ Output extends SignalStoreFeatureResult,
55
+ >(
56
+ condition: (
57
+ store: StateSignals<Input['state']> & Input['props'] & Input['methods'],
58
+ ) => boolean,
59
+ featureIfTrue: SignalStoreFeature<NoInfer<Input>, Output>,
60
+ featureIfFalse: SignalStoreFeature<NoInfer<Input>, NoInfer<Output>>,
61
+ ): SignalStoreFeature<Input, Output> {
62
+ return (store) => {
63
+ const conditionStore = {
64
+ ...store['stateSignals'],
65
+ ...store['props'],
66
+ ...store['methods'],
67
+ };
68
+ return condition(conditionStore)
69
+ ? featureIfTrue(store)
70
+ : featureIfFalse(store);
71
+ };
72
+ }
73
+
74
+ export const emptyFeature = signalStoreFeature(withState({}));