Skip to content

Commit ac95be2

Browse files
committed
feat: add functions onNgrxForms and wrapReducerWithFormStateUpdate to allow better integration with createReducer from ngrx 8
1 parent 51f98f2 commit ac95be2

File tree

6 files changed

+212
-6
lines changed

6 files changed

+212
-6
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@angular/forms": "8.0.0",
6363
"@angular/platform-browser": "8.0.0",
6464
"@angular/platform-browser-dynamic": "8.0.0",
65-
"@ngrx/store": "7.4.0",
65+
"@ngrx/store": "8.0.1",
6666
"@types/jasmine": "2.8.6",
6767
"@types/node": "10.12.18",
6868
"codecov": "3.1.0",

src/ngrx-forms.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
NgrxSelectViewAdapter,
4141
NgrxStatusCssClassesDirective,
4242
NgrxValueConverters,
43+
onNgrxForms,
4344
removeArrayControl,
4445
removeGroupControl,
4546
reset,
@@ -54,6 +55,7 @@ import {
5455
updateGroup,
5556
updateRecursive,
5657
validate,
58+
wrapReducerWithFormStateUpdate,
5759
} from './ngrx-forms';
5860

5961
describe('ngrx-forms', () => {
@@ -68,6 +70,8 @@ describe('ngrx-forms', () => {
6870
it(`should export ${formGroupReducer.name}`, () => expect(formGroupReducer).toBeDefined());
6971
it(`should export ${formArrayReducer.name}`, () => expect(formArrayReducer).toBeDefined());
7072
it(`should export ${formStateReducer.name}`, () => expect(formStateReducer).toBeDefined());
73+
it(`should export ${onNgrxForms.name}`, () => expect(onNgrxForms).toBeDefined());
74+
it(`should export ${wrapReducerWithFormStateUpdate.name}`, () => expect(wrapReducerWithFormStateUpdate).toBeDefined());
7175
it(`should export ${addArrayControl.name}`, () => expect(addArrayControl).toBeDefined());
7276
it(`should export ${addGroupControl.name}`, () => expect(addGroupControl).toBeDefined());
7377
it(`should export ${clearAsyncError.name}`, () => expect(clearAsyncError).toBeDefined());

src/ngrx-forms.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ export {
2323
export { formControlReducer } from './control/reducer';
2424
export { formGroupReducer } from './group/reducer';
2525
export { formArrayReducer } from './array/reducer';
26-
export { formStateReducer, createFormStateReducerWithUpdate } from './reducer';
26+
export {
27+
createFormStateReducerWithUpdate,
28+
formStateReducer,
29+
onNgrxForms,
30+
wrapReducerWithFormStateUpdate,
31+
} from './reducer';
2732

2833
export * from './update-function/update-array';
2934
export * from './update-function/update-group';

src/reducer.spec.ts

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { Action, createReducer } from '@ngrx/store';
12
import { MarkAsTouchedAction, SetValueAction } from './actions';
2-
import { createFormStateReducerWithUpdate, formStateReducer } from './reducer';
3+
import { createFormStateReducerWithUpdate, formStateReducer, onNgrxForms, wrapReducerWithFormStateUpdate } from './reducer';
34
import { FORM_CONTROL_ID, FORM_CONTROL_INNER5_ID, FORM_CONTROL_INNER_ID, FormGroupValue, INITIAL_STATE } from './update-function/test-util';
45
import { updateGroup } from './update-function/update-group';
56

@@ -125,3 +126,139 @@ describe(createFormStateReducerWithUpdate.name, () => {
125126
expect(() => createFormStateReducerWithUpdate<any>(s => s)(undefined, { type: '' })).toThrowError();
126127
});
127128
});
129+
130+
describe(onNgrxForms.name, () => {
131+
it('should call the reducer for controls', () => {
132+
const state = {
133+
prop: 'value',
134+
form: INITIAL_STATE.controls.inner,
135+
};
136+
137+
const resultState = onNgrxForms<typeof state>().reducer(state, new MarkAsTouchedAction(FORM_CONTROL_INNER_ID));
138+
expect(resultState.form).not.toBe(INITIAL_STATE.controls.inner);
139+
});
140+
141+
it('should call the reducer for groups', () => {
142+
const state = {
143+
prop: 'value',
144+
form: INITIAL_STATE,
145+
};
146+
147+
const resultState = onNgrxForms<typeof state>().reducer(state, new MarkAsTouchedAction(FORM_CONTROL_ID));
148+
expect(resultState.form).not.toBe(INITIAL_STATE);
149+
});
150+
151+
it('should call the reducer for arrays', () => {
152+
const state = {
153+
prop: 'value',
154+
form: INITIAL_STATE.controls.inner5,
155+
};
156+
157+
const resultState = onNgrxForms<typeof state>().reducer(state, new MarkAsTouchedAction(FORM_CONTROL_INNER5_ID));
158+
expect(resultState.form).not.toBe(INITIAL_STATE.controls.inner5);
159+
});
160+
161+
it('should work with createReducer', () => {
162+
const state = {
163+
prop: 'value',
164+
control: INITIAL_STATE.controls.inner,
165+
group: INITIAL_STATE,
166+
array: INITIAL_STATE.controls.inner5,
167+
};
168+
169+
const reducer = createReducer(
170+
state,
171+
onNgrxForms(),
172+
);
173+
174+
let resultState = reducer(state, new MarkAsTouchedAction(FORM_CONTROL_INNER_ID));
175+
expect(resultState.control).not.toBe(INITIAL_STATE.controls.inner);
176+
177+
resultState = reducer(state, new MarkAsTouchedAction(FORM_CONTROL_ID));
178+
expect(resultState.group).not.toBe(INITIAL_STATE);
179+
180+
resultState = reducer(state, new MarkAsTouchedAction(FORM_CONTROL_INNER5_ID));
181+
expect(resultState.array).not.toBe(INITIAL_STATE.controls.inner5);
182+
});
183+
});
184+
185+
describe(wrapReducerWithFormStateUpdate.name, () => {
186+
const initialState = {
187+
prop: 'value',
188+
control: INITIAL_STATE.controls.inner,
189+
group: INITIAL_STATE,
190+
array: INITIAL_STATE.controls.inner5,
191+
};
192+
193+
function reducer(state = initialState, _: Action) {
194+
return state;
195+
}
196+
197+
it('should update a control after the reducer', () => {
198+
const wrappedReducer = wrapReducerWithFormStateUpdate(reducer, s => s.control, s => {
199+
expect(s).toBe(INITIAL_STATE.controls.inner);
200+
return ({ ...s });
201+
});
202+
203+
const resultState = wrappedReducer(undefined, { type: '' });
204+
expect(resultState.control).not.toBe(INITIAL_STATE.controls.inner);
205+
});
206+
207+
it('should update a group after the reducer', () => {
208+
const wrappedReducer = wrapReducerWithFormStateUpdate(reducer, s => s.group, s => {
209+
expect(s).toBe(INITIAL_STATE);
210+
return ({ ...s });
211+
});
212+
213+
const resultState = wrappedReducer(undefined, { type: '' });
214+
expect(resultState.group).not.toBe(INITIAL_STATE);
215+
});
216+
217+
it('should update an array after the reducer', () => {
218+
const wrappedReducer = wrapReducerWithFormStateUpdate(reducer, s => s.array, s => {
219+
expect(s).toBe(INITIAL_STATE.controls.inner5);
220+
return ({ ...s });
221+
});
222+
223+
const resultState = wrappedReducer(undefined, { type: '' });
224+
expect(resultState.array).not.toBe(INITIAL_STATE.controls.inner5);
225+
});
226+
227+
it('should set the updated form state', () => {
228+
const updatedControl = { ...INITIAL_STATE.controls.inner };
229+
const wrappedReducer = wrapReducerWithFormStateUpdate(reducer, s => s.control, () => updatedControl);
230+
const resultState = wrappedReducer(undefined, { type: '' });
231+
expect(resultState.control).toBe(updatedControl);
232+
});
233+
234+
it('should not update the state if the form state is not updated', () => {
235+
const wrappedReducer = wrapReducerWithFormStateUpdate(reducer, s => s.control, s => s);
236+
const resultState = wrappedReducer(undefined, { type: '' });
237+
expect(resultState).toBe(initialState);
238+
});
239+
240+
it('should work with createReducer', () => {
241+
const state = {
242+
prop: 'value',
243+
control: INITIAL_STATE.controls.inner,
244+
group: INITIAL_STATE,
245+
array: INITIAL_STATE.controls.inner5,
246+
};
247+
248+
const reducer = createReducer(
249+
state,
250+
onNgrxForms(),
251+
);
252+
253+
const wrappedReducer = wrapReducerWithFormStateUpdate(reducer, s => s.control, s => ({ ...s }));
254+
255+
let resultState = wrappedReducer(state, new MarkAsTouchedAction(FORM_CONTROL_INNER_ID));
256+
expect(resultState.control).not.toBe(INITIAL_STATE.controls.inner);
257+
258+
resultState = wrappedReducer(state, new MarkAsTouchedAction(FORM_CONTROL_ID));
259+
expect(resultState.group).not.toBe(INITIAL_STATE);
260+
261+
resultState = wrappedReducer(state, new MarkAsTouchedAction(FORM_CONTROL_INNER5_ID));
262+
expect(resultState.array).not.toBe(INITIAL_STATE.controls.inner5);
263+
});
264+
});

src/reducer.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Action, ActionReducer } from '@ngrx/store';
22

3+
import { ALL_NGRX_FORMS_ACTION_TYPES } from './actions';
34
import { formArrayReducer } from './array/reducer';
45
import { formControlReducer } from './control/reducer';
56
import { formGroupReducer } from './group/reducer';
@@ -73,3 +74,62 @@ export function createFormStateReducerWithUpdate<TValue>(
7374
return newState === state ? state : updateFnArr.reduce((s, f) => f(s), newState);
7475
};
7576
}
77+
78+
/**
79+
* This function returns an object that can be passed to ngrx's `createReducer`
80+
* function (available starting with ngrx version 8). By doing this all form
81+
* state properties on the state will be updated whenever necessary (i.e.
82+
* whenever an ngrx-forms action is dispatched).
83+
*
84+
* To manually update a form state (e.g. to validate it) use
85+
* `wrapReducerWithFormStateUpdate`.
86+
*/
87+
export function onNgrxForms<TState = any>(): { reducer: ActionReducer<TState>; types: string[] } {
88+
function reduceNestedFormState(state: TState, key: keyof TState, action: Action): TState {
89+
const value = state[key];
90+
91+
if (!isFormState(value)) {
92+
return state;
93+
}
94+
95+
return {
96+
...state,
97+
[key]: formStateReducer(value, action),
98+
};
99+
}
100+
101+
return {
102+
reducer: (state, action) =>
103+
Object.keys(state!).reduce((s, key) => reduceNestedFormState(s, key as keyof TState, action)!, state!),
104+
types: ALL_NGRX_FORMS_ACTION_TYPES,
105+
};
106+
}
107+
108+
/**
109+
* This function wraps a reducer and returns another reducer that first calls
110+
* the given reducer and then calls the given update function for the form state
111+
* that is specified by the form state locator function.
112+
*/
113+
export function wrapReducerWithFormStateUpdate<TState, TFormState extends AbstractControlState<any>>(
114+
reducer: ActionReducer<TState>,
115+
formStateLocator: (state: TState) => TFormState,
116+
updateFn: (formState: TFormState) => TFormState,
117+
): ActionReducer<TState> {
118+
return (state, action) => {
119+
const updatedState = reducer(state, action);
120+
121+
const formState = formStateLocator(updatedState);
122+
const formStateKey = Object.keys(updatedState).find(key => updatedState[key as keyof TState] as any === formState)!;
123+
124+
const updatedFormState = updateFn(formState);
125+
126+
if (updatedFormState === formState) {
127+
return updatedState;
128+
}
129+
130+
return {
131+
...updatedState,
132+
[formStateKey]: updatedFormState,
133+
};
134+
};
135+
}

0 commit comments

Comments
 (0)