@angular-architects/ngrx-toolkit 0.1.0 → 0.1.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/esm2022/angular-architects-ngrx-toolkit.mjs +5 -0
- package/esm2022/index.mjs +9 -0
- package/esm2022/lib/assertions/assertions.mjs +6 -0
- package/esm2022/lib/redux-connector/create-redux.mjs +41 -0
- package/esm2022/lib/redux-connector/index.mjs +2 -0
- package/esm2022/lib/redux-connector/model.mjs +2 -0
- package/esm2022/lib/redux-connector/rxjs-interop/index.mjs +2 -0
- package/esm2022/lib/redux-connector/rxjs-interop/redux-method.mjs +22 -0
- package/esm2022/lib/redux-connector/signal-redux-store.mjs +43 -0
- package/esm2022/lib/redux-connector/util.mjs +13 -0
- package/esm2022/lib/shared/empty.mjs +2 -0
- package/esm2022/lib/with-call-state.mjs +58 -0
- package/esm2022/lib/with-data-service.mjs +161 -0
- package/esm2022/lib/with-devtools.mjs +79 -0
- package/esm2022/lib/with-redux.mjs +95 -0
- package/esm2022/lib/with-storage-sync.mjs +56 -0
- package/esm2022/lib/with-undo-redo.mjs +93 -0
- package/fesm2022/angular-architects-ngrx-toolkit.mjs +653 -0
- package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +1 -0
- package/{src/index.ts → index.d.ts} +2 -3
- package/lib/assertions/assertions.d.ts +2 -0
- package/lib/redux-connector/create-redux.d.ts +13 -0
- package/{src/lib/redux-connector/index.ts → lib/redux-connector/index.d.ts} +0 -1
- package/lib/redux-connector/model.d.ts +36 -0
- package/{src/lib/redux-connector/rxjs-interop/index.ts → lib/redux-connector/rxjs-interop/index.d.ts} +0 -1
- package/lib/redux-connector/rxjs-interop/redux-method.d.ts +11 -0
- package/lib/redux-connector/signal-redux-store.d.ts +11 -0
- package/lib/redux-connector/util.d.ts +5 -0
- package/lib/shared/empty.d.ts +1 -0
- package/lib/with-call-state.d.ts +56 -0
- package/lib/with-data-service.d.ts +115 -0
- package/lib/with-devtools.d.ts +32 -0
- package/lib/with-redux.d.ts +57 -0
- package/lib/with-storage-sync.d.ts +58 -0
- package/lib/with-undo-redo.d.ts +55 -0
- package/package.json +16 -3
- package/.eslintrc.json +0 -43
- package/jest.config.ts +0 -22
- package/ng-package.json +0 -7
- package/project.json +0 -37
- package/src/lib/assertions/assertions.ts +0 -9
- package/src/lib/redux-connector/create-redux.ts +0 -94
- package/src/lib/redux-connector/model.ts +0 -67
- package/src/lib/redux-connector/rxjs-interop/redux-method.ts +0 -61
- package/src/lib/redux-connector/signal-redux-store.ts +0 -54
- package/src/lib/redux-connector/util.ts +0 -22
- package/src/lib/shared/empty.ts +0 -2
- package/src/lib/with-call-state.spec.ts +0 -24
- package/src/lib/with-call-state.ts +0 -136
- package/src/lib/with-data-service.ts +0 -312
- package/src/lib/with-devtools.spec.ts +0 -157
- package/src/lib/with-devtools.ts +0 -128
- package/src/lib/with-redux.spec.ts +0 -100
- package/src/lib/with-redux.ts +0 -261
- package/src/lib/with-storage-sync.spec.ts +0 -237
- package/src/lib/with-storage-sync.ts +0 -160
- package/src/lib/with-undo-redo.ts +0 -184
- package/src/test-setup.ts +0 -8
- package/tsconfig.json +0 -29
- package/tsconfig.lib.json +0 -17
- package/tsconfig.lib.prod.json +0 -9
- package/tsconfig.spec.json +0 -16
package/src/lib/with-redux.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { Observable, Subject } from 'rxjs';
|
|
2
|
-
import { SignalStoreFeature } from '@ngrx/signals';
|
|
3
|
-
import {
|
|
4
|
-
EmptyFeatureResult,
|
|
5
|
-
SignalStoreFeatureResult,
|
|
6
|
-
} from '@ngrx/signals/src/signal-store-models';
|
|
7
|
-
import { StateSignal } from '@ngrx/signals/src/state-signal';
|
|
8
|
-
import { assertActionFnSpecs } from './assertions/assertions';
|
|
9
|
-
|
|
10
|
-
/** Actions **/
|
|
11
|
-
|
|
12
|
-
type Payload = Record<string, unknown>;
|
|
13
|
-
|
|
14
|
-
type ActionFn<
|
|
15
|
-
Type extends string = string,
|
|
16
|
-
ActionPayload extends Payload = Payload
|
|
17
|
-
> = ((payload: ActionPayload) => ActionPayload & { type: Type }) & {
|
|
18
|
-
type: Type;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type ActionFns = Record<string, ActionFn>;
|
|
22
|
-
|
|
23
|
-
export type ActionsFnSpecs = Record<string, Payload>;
|
|
24
|
-
|
|
25
|
-
type ActionFnCreator<Spec extends ActionsFnSpecs> = {
|
|
26
|
-
[ActionName in keyof Spec]: ((
|
|
27
|
-
payload: Spec[ActionName]
|
|
28
|
-
) => Spec[ActionName] & { type: ActionName }) & { type: ActionName & string };
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type ActionFnPayload<Action> = Action extends (payload: infer Payload) => void
|
|
32
|
-
? Payload
|
|
33
|
-
: never;
|
|
34
|
-
|
|
35
|
-
type ActionFnsCreator<Spec extends ActionsFnSpecs> = Spec extends {
|
|
36
|
-
private: Record<string, Payload>;
|
|
37
|
-
public: Record<string, Payload>;
|
|
38
|
-
}
|
|
39
|
-
? ActionFnCreator<Spec['private']> & ActionFnCreator<Spec['public']>
|
|
40
|
-
: ActionFnCreator<Spec>;
|
|
41
|
-
|
|
42
|
-
type PublicActionFns<Spec extends ActionsFnSpecs> = Spec extends {
|
|
43
|
-
public: Record<string, Payload>;
|
|
44
|
-
}
|
|
45
|
-
? ActionFnCreator<Spec['public']>
|
|
46
|
-
: ActionFnCreator<Spec>;
|
|
47
|
-
|
|
48
|
-
export function payload<Type extends Payload>(): Type {
|
|
49
|
-
return {} as Type;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const noPayload = {};
|
|
53
|
-
|
|
54
|
-
/** Reducer **/
|
|
55
|
-
|
|
56
|
-
type ReducerFunction<ReducerAction, State> = (
|
|
57
|
-
state: State,
|
|
58
|
-
action: ActionFnPayload<ReducerAction>
|
|
59
|
-
) => void;
|
|
60
|
-
|
|
61
|
-
type ReducerFactory<StateActionFns extends ActionFns, State> = (
|
|
62
|
-
actions: StateActionFns,
|
|
63
|
-
on: <ReducerAction extends { type: string }>(
|
|
64
|
-
action: ReducerAction,
|
|
65
|
-
reducerFn: ReducerFunction<ReducerAction, State>
|
|
66
|
-
) => void
|
|
67
|
-
) => void;
|
|
68
|
-
|
|
69
|
-
/** Effect **/
|
|
70
|
-
|
|
71
|
-
type EffectsFactory<StateActionFns extends ActionFns> = (
|
|
72
|
-
actions: StateActionFns,
|
|
73
|
-
create: <EffectAction extends { type: string }>(
|
|
74
|
-
action: EffectAction
|
|
75
|
-
) => Observable<ActionFnPayload<EffectAction>>
|
|
76
|
-
) => Record<string, Observable<unknown>>;
|
|
77
|
-
|
|
78
|
-
function createActionFns<Spec extends ActionsFnSpecs>(
|
|
79
|
-
actionFnSpecs: Spec,
|
|
80
|
-
reducerRegistry: Record<
|
|
81
|
-
string,
|
|
82
|
-
(state: unknown, payload: ActionFnPayload<unknown>) => void
|
|
83
|
-
>,
|
|
84
|
-
effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>>,
|
|
85
|
-
state: unknown
|
|
86
|
-
) {
|
|
87
|
-
const actionFns: Record<string, ActionFn> = {};
|
|
88
|
-
|
|
89
|
-
for (const type in actionFnSpecs) {
|
|
90
|
-
const actionFn = (payload: Payload) => {
|
|
91
|
-
const fullPayload = { ...payload, type };
|
|
92
|
-
const reducer = reducerRegistry[type];
|
|
93
|
-
if (reducer) {
|
|
94
|
-
(reducer as (state: unknown, payload: unknown) => void)(
|
|
95
|
-
state,
|
|
96
|
-
fullPayload as unknown
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
const effectSubject = effectsRegistry[type];
|
|
100
|
-
if (effectSubject) {
|
|
101
|
-
(effectSubject as unknown as Subject<unknown>).next(fullPayload);
|
|
102
|
-
}
|
|
103
|
-
return fullPayload;
|
|
104
|
-
};
|
|
105
|
-
actionFn.type = type.toString();
|
|
106
|
-
actionFns[type] = actionFn;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return actionFns;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function createPublicAndAllActionsFns<Spec extends ActionsFnSpecs>(
|
|
113
|
-
actionFnSpecs: Spec,
|
|
114
|
-
reducerRegistry: Record<
|
|
115
|
-
string,
|
|
116
|
-
(state: unknown, payload: ActionFnPayload<unknown>) => void
|
|
117
|
-
>,
|
|
118
|
-
effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>>,
|
|
119
|
-
state: unknown
|
|
120
|
-
): { all: ActionFns; publics: ActionFns } {
|
|
121
|
-
if ('public' in actionFnSpecs || 'private' in actionFnSpecs) {
|
|
122
|
-
const privates = actionFnSpecs['private'] || {};
|
|
123
|
-
const publics = actionFnSpecs['public'] || {};
|
|
124
|
-
|
|
125
|
-
assertActionFnSpecs(privates);
|
|
126
|
-
assertActionFnSpecs(publics);
|
|
127
|
-
|
|
128
|
-
const privateActionFns = createActionFns(
|
|
129
|
-
privates,
|
|
130
|
-
reducerRegistry,
|
|
131
|
-
effectsRegistry,
|
|
132
|
-
state
|
|
133
|
-
);
|
|
134
|
-
const publicActionFns = createActionFns(
|
|
135
|
-
publics,
|
|
136
|
-
reducerRegistry,
|
|
137
|
-
effectsRegistry,
|
|
138
|
-
state
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
all: { ...privateActionFns, ...publicActionFns },
|
|
143
|
-
publics: publicActionFns,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const actionFns = createActionFns(
|
|
148
|
-
actionFnSpecs,
|
|
149
|
-
reducerRegistry,
|
|
150
|
-
effectsRegistry,
|
|
151
|
-
state
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
return { all: actionFns, publics: actionFns };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function fillReducerRegistry(
|
|
158
|
-
reducer: ReducerFactory<ActionFns, unknown>,
|
|
159
|
-
actionFns: ActionFns,
|
|
160
|
-
reducerRegistry: Record<
|
|
161
|
-
string,
|
|
162
|
-
(state: unknown, payload: ActionFnPayload<unknown>) => void
|
|
163
|
-
>
|
|
164
|
-
) {
|
|
165
|
-
function on(
|
|
166
|
-
action: { type: string },
|
|
167
|
-
reducerFn: (state: unknown, payload: ActionFnPayload<unknown>) => void
|
|
168
|
-
) {
|
|
169
|
-
reducerRegistry[action.type] = reducerFn;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
reducer(actionFns, on);
|
|
173
|
-
|
|
174
|
-
return reducerRegistry;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function fillEffects(
|
|
178
|
-
effects: EffectsFactory<ActionFns>,
|
|
179
|
-
actionFns: ActionFns,
|
|
180
|
-
effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>> = {}
|
|
181
|
-
): Observable<unknown>[] {
|
|
182
|
-
function create(action: { type: string }) {
|
|
183
|
-
const subject = new Subject<ActionFnPayload<unknown>>();
|
|
184
|
-
effectsRegistry[action.type] = subject;
|
|
185
|
-
return subject.asObservable();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const effectObservables = effects(actionFns, create);
|
|
189
|
-
return Object.values(effectObservables);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function startSubscriptions(observables: Observable<unknown>[]) {
|
|
193
|
-
return observables.map((observable) => observable.subscribe());
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function processRedux<Spec extends ActionsFnSpecs, ReturnType>(
|
|
197
|
-
actionFnSpecs: Spec,
|
|
198
|
-
reducer: ReducerFactory<ActionFns, unknown>,
|
|
199
|
-
effects: EffectsFactory<ActionFns>,
|
|
200
|
-
store: unknown
|
|
201
|
-
) {
|
|
202
|
-
const reducerRegistry: Record<
|
|
203
|
-
string,
|
|
204
|
-
(state: unknown, payload: ActionFnPayload<unknown>) => void
|
|
205
|
-
> = {};
|
|
206
|
-
const effectsRegistry: Record<string, Subject<ActionFnPayload<unknown>>> = {};
|
|
207
|
-
const actionsMap = createPublicAndAllActionsFns(
|
|
208
|
-
actionFnSpecs,
|
|
209
|
-
reducerRegistry,
|
|
210
|
-
effectsRegistry,
|
|
211
|
-
store
|
|
212
|
-
);
|
|
213
|
-
const actionFns = actionsMap.all;
|
|
214
|
-
const publicActionsFns = actionsMap.publics;
|
|
215
|
-
|
|
216
|
-
fillReducerRegistry(reducer, actionFns, reducerRegistry);
|
|
217
|
-
const effectObservables = fillEffects(effects, actionFns, effectsRegistry);
|
|
218
|
-
const subscriptions = startSubscriptions(effectObservables);
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
methods: publicActionsFns as ReturnType,
|
|
222
|
-
subscriptions: subscriptions,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @param redux redux
|
|
228
|
-
*
|
|
229
|
-
* properties do not start with `with` since they are not extension functions on their own.
|
|
230
|
-
*
|
|
231
|
-
* no dependency to NgRx
|
|
232
|
-
*
|
|
233
|
-
* actions are passed to reducer and effects, but it is also possible to use other actions.
|
|
234
|
-
* effects provide forAction and do not return anything. that is important because effects should stay inaccessible
|
|
235
|
-
*/
|
|
236
|
-
export function withRedux<
|
|
237
|
-
Spec extends ActionsFnSpecs,
|
|
238
|
-
Input extends SignalStoreFeatureResult,
|
|
239
|
-
StateActionFns extends ActionFnsCreator<Spec> = ActionFnsCreator<Spec>,
|
|
240
|
-
PublicStoreActionFns extends PublicActionFns<Spec> = PublicActionFns<Spec>
|
|
241
|
-
>(redux: {
|
|
242
|
-
actions: Spec;
|
|
243
|
-
reducer: ReducerFactory<StateActionFns, StateSignal<Input['state']>>;
|
|
244
|
-
effects: EffectsFactory<StateActionFns>;
|
|
245
|
-
}): SignalStoreFeature<
|
|
246
|
-
Input,
|
|
247
|
-
EmptyFeatureResult & { methods: PublicStoreActionFns }
|
|
248
|
-
> {
|
|
249
|
-
return (store) => {
|
|
250
|
-
const { methods, subscriptions } = processRedux<Spec, PublicStoreActionFns>(
|
|
251
|
-
redux.actions,
|
|
252
|
-
redux.reducer as ReducerFactory<ActionFns, unknown>,
|
|
253
|
-
redux.effects as EffectsFactory<ActionFns>,
|
|
254
|
-
store
|
|
255
|
-
);
|
|
256
|
-
return {
|
|
257
|
-
...store,
|
|
258
|
-
methods,
|
|
259
|
-
};
|
|
260
|
-
};
|
|
261
|
-
}
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { getState, patchState, signalStore, withState } from '@ngrx/signals';
|
|
2
|
-
import { withStorageSync } from './with-storage-sync';
|
|
3
|
-
import { TestBed } from '@angular/core/testing';
|
|
4
|
-
|
|
5
|
-
interface StateObject {
|
|
6
|
-
foo: string;
|
|
7
|
-
age: number;
|
|
8
|
-
}
|
|
9
|
-
const initialState: StateObject = {
|
|
10
|
-
foo: 'bar',
|
|
11
|
-
age: 18,
|
|
12
|
-
};
|
|
13
|
-
const key = 'FooBar';
|
|
14
|
-
|
|
15
|
-
describe('withStorageSync', () => {
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
// make sure to start with a clean storage
|
|
18
|
-
localStorage.removeItem(key);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('adds methods for storage access to the store', () => {
|
|
22
|
-
TestBed.runInInjectionContext(() => {
|
|
23
|
-
const Store = signalStore(withStorageSync({ key }));
|
|
24
|
-
const store = new Store();
|
|
25
|
-
|
|
26
|
-
expect(Object.keys(store)).toEqual([
|
|
27
|
-
'clearStorage',
|
|
28
|
-
'readFromStorage',
|
|
29
|
-
'writeToStorage',
|
|
30
|
-
]);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('offers manual sync using provided methods', () => {
|
|
35
|
-
TestBed.runInInjectionContext(() => {
|
|
36
|
-
// prefill storage
|
|
37
|
-
localStorage.setItem(
|
|
38
|
-
key,
|
|
39
|
-
JSON.stringify({
|
|
40
|
-
foo: 'baz',
|
|
41
|
-
age: 99,
|
|
42
|
-
} as StateObject)
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const Store = signalStore(withStorageSync({ key, autoSync: false }));
|
|
46
|
-
const store = new Store();
|
|
47
|
-
expect(getState(store)).toEqual({});
|
|
48
|
-
|
|
49
|
-
store.readFromStorage();
|
|
50
|
-
expect(getState(store)).toEqual({
|
|
51
|
-
foo: 'baz',
|
|
52
|
-
age: 99,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
patchState(store, { ...initialState });
|
|
56
|
-
TestBed.flushEffects();
|
|
57
|
-
|
|
58
|
-
let storeItem = JSON.parse(localStorage.getItem(key) || '{}');
|
|
59
|
-
expect(storeItem).toEqual({
|
|
60
|
-
foo: 'baz',
|
|
61
|
-
age: 99,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
store.writeToStorage();
|
|
65
|
-
storeItem = JSON.parse(localStorage.getItem(key) || '{}');
|
|
66
|
-
expect(storeItem).toEqual({
|
|
67
|
-
...initialState,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
store.clearStorage();
|
|
71
|
-
storeItem = localStorage.getItem(key);
|
|
72
|
-
expect(storeItem).toEqual(null);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('autoSync', () => {
|
|
77
|
-
it('inits from storage and write to storage on changes when set to `true`', () => {
|
|
78
|
-
TestBed.runInInjectionContext(() => {
|
|
79
|
-
// prefill storage
|
|
80
|
-
localStorage.setItem(
|
|
81
|
-
key,
|
|
82
|
-
JSON.stringify({
|
|
83
|
-
foo: 'baz',
|
|
84
|
-
age: 99,
|
|
85
|
-
} as StateObject)
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const Store = signalStore(withStorageSync(key));
|
|
89
|
-
const store = new Store();
|
|
90
|
-
expect(getState(store)).toEqual({
|
|
91
|
-
foo: 'baz',
|
|
92
|
-
age: 99,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
patchState(store, { ...initialState });
|
|
96
|
-
TestBed.flushEffects();
|
|
97
|
-
|
|
98
|
-
expect(getState(store)).toEqual({
|
|
99
|
-
...initialState,
|
|
100
|
-
});
|
|
101
|
-
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
|
|
102
|
-
expect(storeItem).toEqual({
|
|
103
|
-
...initialState,
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('does not init from storage and does write to storage on changes when set to `false`', () => {
|
|
109
|
-
TestBed.runInInjectionContext(() => {
|
|
110
|
-
// prefill storage
|
|
111
|
-
localStorage.setItem(
|
|
112
|
-
key,
|
|
113
|
-
JSON.stringify({
|
|
114
|
-
foo: 'baz',
|
|
115
|
-
age: 99,
|
|
116
|
-
} as StateObject)
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const Store = signalStore(withStorageSync({ key, autoSync: false }));
|
|
120
|
-
const store = new Store();
|
|
121
|
-
expect(getState(store)).toEqual({});
|
|
122
|
-
|
|
123
|
-
patchState(store, { ...initialState });
|
|
124
|
-
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
|
|
125
|
-
expect(storeItem).toEqual({
|
|
126
|
-
foo: 'baz',
|
|
127
|
-
age: 99,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('select', () => {
|
|
134
|
-
it('syncs the whole state by default', () => {
|
|
135
|
-
TestBed.runInInjectionContext(() => {
|
|
136
|
-
const Store = signalStore(withStorageSync(key));
|
|
137
|
-
const store = new Store();
|
|
138
|
-
|
|
139
|
-
patchState(store, { ...initialState });
|
|
140
|
-
TestBed.flushEffects();
|
|
141
|
-
|
|
142
|
-
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
|
|
143
|
-
expect(storeItem).toEqual({
|
|
144
|
-
...initialState,
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('syncs selected slices when specified', () => {
|
|
150
|
-
TestBed.runInInjectionContext(() => {
|
|
151
|
-
const Store = signalStore(
|
|
152
|
-
withState(initialState),
|
|
153
|
-
withStorageSync({ key, select: ({ foo }) => ({ foo }) })
|
|
154
|
-
);
|
|
155
|
-
const store = new Store();
|
|
156
|
-
|
|
157
|
-
patchState(store, { foo: 'baz' });
|
|
158
|
-
TestBed.flushEffects();
|
|
159
|
-
|
|
160
|
-
const storeItem = JSON.parse(localStorage.getItem(key) || '{}');
|
|
161
|
-
expect(storeItem).toEqual({
|
|
162
|
-
foo: 'baz',
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('parse/stringify', () => {
|
|
169
|
-
it('uses custom parsing/stringification when specified', () => {
|
|
170
|
-
const parse = (stateString: string) => {
|
|
171
|
-
const [foo, age] = stateString.split('_');
|
|
172
|
-
return {
|
|
173
|
-
foo,
|
|
174
|
-
age: +age,
|
|
175
|
-
};
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
TestBed.runInInjectionContext(() => {
|
|
179
|
-
const Store = signalStore(
|
|
180
|
-
withState(initialState),
|
|
181
|
-
withStorageSync({
|
|
182
|
-
key,
|
|
183
|
-
parse,
|
|
184
|
-
stringify: (state) => `${state.foo}_${state.age}`,
|
|
185
|
-
})
|
|
186
|
-
);
|
|
187
|
-
const store = new Store();
|
|
188
|
-
|
|
189
|
-
patchState(store, { foo: 'baz' });
|
|
190
|
-
TestBed.flushEffects();
|
|
191
|
-
|
|
192
|
-
const storeItem = parse(localStorage.getItem(key) || '');
|
|
193
|
-
expect(storeItem).toEqual({
|
|
194
|
-
...initialState,
|
|
195
|
-
foo: 'baz',
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('storage factory', () => {
|
|
202
|
-
it('uses specified storage', () => {
|
|
203
|
-
TestBed.runInInjectionContext(() => {
|
|
204
|
-
// prefill storage
|
|
205
|
-
sessionStorage.setItem(
|
|
206
|
-
key,
|
|
207
|
-
JSON.stringify({
|
|
208
|
-
foo: 'baz',
|
|
209
|
-
age: 99,
|
|
210
|
-
} as StateObject)
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const Store = signalStore(
|
|
214
|
-
withStorageSync({ key, storage: () => sessionStorage })
|
|
215
|
-
);
|
|
216
|
-
const store = new Store();
|
|
217
|
-
expect(getState(store)).toEqual({
|
|
218
|
-
foo: 'baz',
|
|
219
|
-
age: 99,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
patchState(store, { ...initialState });
|
|
223
|
-
TestBed.flushEffects();
|
|
224
|
-
|
|
225
|
-
expect(getState(store)).toEqual({
|
|
226
|
-
...initialState,
|
|
227
|
-
});
|
|
228
|
-
const storeItem = JSON.parse(sessionStorage.getItem(key) || '{}');
|
|
229
|
-
expect(storeItem).toEqual({
|
|
230
|
-
...initialState,
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
store.clearStorage();
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { isPlatformServer } from '@angular/common';
|
|
2
|
-
import { PLATFORM_ID, effect, inject } from '@angular/core';
|
|
3
|
-
import {
|
|
4
|
-
SignalStoreFeature,
|
|
5
|
-
getState,
|
|
6
|
-
patchState,
|
|
7
|
-
signalStoreFeature,
|
|
8
|
-
withHooks,
|
|
9
|
-
withMethods,
|
|
10
|
-
} from '@ngrx/signals';
|
|
11
|
-
import { Emtpy } from './shared/empty';
|
|
12
|
-
|
|
13
|
-
type SignalStoreFeatureInput<State> = Pick<
|
|
14
|
-
Parameters<SignalStoreFeature>[0],
|
|
15
|
-
'signals' | 'methods'
|
|
16
|
-
> & {
|
|
17
|
-
state: State;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const NOOP = () => {};
|
|
21
|
-
|
|
22
|
-
type WithStorageSyncFeatureResult = {
|
|
23
|
-
state: Emtpy;
|
|
24
|
-
signals: Emtpy;
|
|
25
|
-
methods: {
|
|
26
|
-
clearStorage(): void;
|
|
27
|
-
readFromStorage(): void;
|
|
28
|
-
writeToStorage(): void;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const StorageSyncStub: Pick<
|
|
33
|
-
WithStorageSyncFeatureResult,
|
|
34
|
-
'methods'
|
|
35
|
-
>['methods'] = {
|
|
36
|
-
clearStorage: NOOP,
|
|
37
|
-
readFromStorage: NOOP,
|
|
38
|
-
writeToStorage: NOOP,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type SyncConfig<State> = {
|
|
42
|
-
/**
|
|
43
|
-
* The key which is used to access the storage.
|
|
44
|
-
*/
|
|
45
|
-
key: string;
|
|
46
|
-
/**
|
|
47
|
-
* Flag indicating if the store should read from storage on init and write to storage on every state change.
|
|
48
|
-
*
|
|
49
|
-
* `true` by default
|
|
50
|
-
*/
|
|
51
|
-
autoSync?: boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Function to select that portion of the state which should be stored.
|
|
54
|
-
*
|
|
55
|
-
* Returns the whole state object by default
|
|
56
|
-
*/
|
|
57
|
-
select?: (state: State) => Partial<State>;
|
|
58
|
-
/**
|
|
59
|
-
* Function used to parse the state coming from storage.
|
|
60
|
-
*
|
|
61
|
-
* `JSON.parse()` by default
|
|
62
|
-
*/
|
|
63
|
-
parse?: (stateString: string) => State;
|
|
64
|
-
/**
|
|
65
|
-
* Function used to tranform the state into a string representation.
|
|
66
|
-
*
|
|
67
|
-
* `JSON.stringify()` by default
|
|
68
|
-
*/
|
|
69
|
-
stringify?: (state: State) => string;
|
|
70
|
-
/**
|
|
71
|
-
* Factory function used to select the storage.
|
|
72
|
-
*
|
|
73
|
-
* `localstorage` by default
|
|
74
|
-
*/
|
|
75
|
-
storage?: () => Storage;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Enables store synchronization with storage.
|
|
80
|
-
*
|
|
81
|
-
* Only works on browser platform.
|
|
82
|
-
*/
|
|
83
|
-
export function withStorageSync<
|
|
84
|
-
State extends object,
|
|
85
|
-
Input extends SignalStoreFeatureInput<State>
|
|
86
|
-
>(key: string): SignalStoreFeature<Input, WithStorageSyncFeatureResult>;
|
|
87
|
-
export function withStorageSync<
|
|
88
|
-
State extends object,
|
|
89
|
-
Input extends SignalStoreFeatureInput<State>
|
|
90
|
-
>(
|
|
91
|
-
config: SyncConfig<Input['state']>
|
|
92
|
-
): SignalStoreFeature<Input, WithStorageSyncFeatureResult>;
|
|
93
|
-
export function withStorageSync<
|
|
94
|
-
State extends object,
|
|
95
|
-
Input extends SignalStoreFeatureInput<State>
|
|
96
|
-
>(
|
|
97
|
-
configOrKey: SyncConfig<Input['state']> | string
|
|
98
|
-
): SignalStoreFeature<Input, WithStorageSyncFeatureResult> {
|
|
99
|
-
const {
|
|
100
|
-
key,
|
|
101
|
-
autoSync = true,
|
|
102
|
-
select = (state: State) => state,
|
|
103
|
-
parse = JSON.parse,
|
|
104
|
-
stringify = JSON.stringify,
|
|
105
|
-
storage: storageFactory = () => localStorage,
|
|
106
|
-
} = typeof configOrKey === 'string' ? { key: configOrKey } : configOrKey;
|
|
107
|
-
|
|
108
|
-
return signalStoreFeature(
|
|
109
|
-
withMethods((store, platformId = inject(PLATFORM_ID)) => {
|
|
110
|
-
if (isPlatformServer(platformId)) {
|
|
111
|
-
console.warn(
|
|
112
|
-
`'withStorageSync' provides non-functional implementation due to server-side execution`
|
|
113
|
-
);
|
|
114
|
-
return StorageSyncStub;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const storage = storageFactory();
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
/**
|
|
121
|
-
* Removes the item stored in storage.
|
|
122
|
-
*/
|
|
123
|
-
clearStorage(): void {
|
|
124
|
-
storage.removeItem(key);
|
|
125
|
-
},
|
|
126
|
-
/**
|
|
127
|
-
* Reads item from storage and patches the state.
|
|
128
|
-
*/
|
|
129
|
-
readFromStorage(): void {
|
|
130
|
-
const stateString = storage.getItem(key);
|
|
131
|
-
if (stateString) {
|
|
132
|
-
patchState(store, parse(stateString));
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
/**
|
|
136
|
-
* Writes selected portion to storage.
|
|
137
|
-
*/
|
|
138
|
-
writeToStorage(): void {
|
|
139
|
-
const slicedState = select(getState(store) as State);
|
|
140
|
-
storage.setItem(key, stringify(slicedState));
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}),
|
|
144
|
-
withHooks({
|
|
145
|
-
onInit(store, platformId = inject(PLATFORM_ID)) {
|
|
146
|
-
if (isPlatformServer(platformId)) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (autoSync) {
|
|
151
|
-
store.readFromStorage();
|
|
152
|
-
|
|
153
|
-
effect(() => {
|
|
154
|
-
store.writeToStorage();
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
})
|
|
159
|
-
);
|
|
160
|
-
}
|