From 6a1b93ae03264d605da9444ba296f766dad88f56 Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Fri, 27 Oct 2017 11:16:48 +0200 Subject: [PATCH 01/11] feat: initial array form states + state tests --- src/state.spec.ts | 206 ++++++++++++++++++++++++++++++++++++++++++++++ src/state.ts | 49 +++++++++++ 2 files changed, 255 insertions(+) create mode 100644 src/state.spec.ts diff --git a/src/state.spec.ts b/src/state.spec.ts new file mode 100644 index 00000000..a71345f3 --- /dev/null +++ b/src/state.spec.ts @@ -0,0 +1,206 @@ +import { cast, createFormArrayState, createFormControlState, createFormGroupState } from './state'; + +describe('state', () => { + const FORM_CONTROL_ID = 'test ID'; + + describe('control', () => { + const INITIAL_FORM_CONTROL_VALUE = 'abc'; + const INITIAL_STATE = createFormControlState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); + + it('should set the correct id', () => { + expect(INITIAL_STATE.id).toBe(FORM_CONTROL_ID); + }); + + it('should set the correct value', () => { + expect(INITIAL_STATE.value).toBe(INITIAL_FORM_CONTROL_VALUE); + }); + + it('should mark control as valid', () => { + expect(INITIAL_STATE.isValid).toBe(true); + expect(INITIAL_STATE.isInvalid).toBe(false); + }); + + it('should mark control as enabled', () => { + expect(INITIAL_STATE.isEnabled).toBe(true); + expect(INITIAL_STATE.isDisabled).toBe(false); + }); + + it('should mark control as unfocused', () => { + expect(INITIAL_STATE.isFocused).toBe(false); + expect(INITIAL_STATE.isUnfocused).toBe(true); + }); + + it('should set empty errors', () => { + expect(INITIAL_STATE.errors).toEqual({}); + }); + + it('should mark control as pristine', () => { + expect(INITIAL_STATE.isPristine).toBe(true); + expect(INITIAL_STATE.isDirty).toBe(false); + }); + + it('should mark control as untouched', () => { + expect(INITIAL_STATE.isTouched).toBe(false); + expect(INITIAL_STATE.isUntouched).toBe(true); + }); + + it('should mark control as unsubmitted', () => { + expect(INITIAL_STATE.isSubmitted).toBe(false); + expect(INITIAL_STATE.isUnsubmitted).toBe(true); + }); + + it('should set empty user-defined properties', () => { + expect(INITIAL_STATE.userDefinedProperties).toEqual({}); + }); + }); + + describe('group', () => { + const CONTROL_VALUE = 'abc'; + const GROUP_VALUE = { control: 'bcd' }; + const ARRAY_VALUE = ['def']; + const INITIAL_VALUE = { control: 'abc', group: GROUP_VALUE, array: ARRAY_VALUE }; + const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_VALUE); + + it('should set the correct id', () => { + expect(INITIAL_STATE.id).toBe(FORM_CONTROL_ID); + }); + + it('should set the correct value', () => { + expect(INITIAL_STATE.value).toBe(INITIAL_VALUE); + }); + + it('should mark control as valid', () => { + expect(INITIAL_STATE.isValid).toBe(true); + expect(INITIAL_STATE.isInvalid).toBe(false); + }); + + it('should mark control as enabled', () => { + expect(INITIAL_STATE.isEnabled).toBe(true); + expect(INITIAL_STATE.isDisabled).toBe(false); + }); + + it('should set empty errors', () => { + expect(INITIAL_STATE.errors).toEqual({}); + }); + + it('should mark control as pristine', () => { + expect(INITIAL_STATE.isPristine).toBe(true); + expect(INITIAL_STATE.isDirty).toBe(false); + }); + + it('should mark control as untouched', () => { + expect(INITIAL_STATE.isTouched).toBe(false); + expect(INITIAL_STATE.isUntouched).toBe(true); + }); + + it('should mark control as unsubmitted', () => { + expect(INITIAL_STATE.isSubmitted).toBe(false); + expect(INITIAL_STATE.isUnsubmitted).toBe(true); + }); + + it('should set empty user-defined properties', () => { + expect(INITIAL_STATE.userDefinedProperties).toEqual({}); + }); + + it('should create control child', () => { + expect(INITIAL_STATE.controls.control.value).toEqual(CONTROL_VALUE); + expect(cast(INITIAL_STATE.controls.control).isFocused).toBeDefined(); + }); + + it('should create group child', () => { + expect(INITIAL_STATE.controls.group.value).toEqual(GROUP_VALUE); + const controls = cast(INITIAL_STATE.controls.group).controls; + expect(controls).toBeDefined(); + expect(Array.isArray(controls)).toBe(false); + }); + + it('should create array child', () => { + expect(INITIAL_STATE.controls.array.value).toEqual(ARRAY_VALUE); + const controls = cast(INITIAL_STATE.controls.array).controls; + expect(controls).toBeDefined(); + expect(Array.isArray(controls)).toBe(true); + }); + }); + + describe('array', () => { + const INITIAL_VALUE = ['a', 'b']; + const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_VALUE); + + it('should set the correct id', () => { + expect(INITIAL_STATE.id).toBe(FORM_CONTROL_ID); + }); + + it('should set the correct value', () => { + expect(INITIAL_STATE.value).toBe(INITIAL_VALUE); + }); + + it('should mark control as valid', () => { + expect(INITIAL_STATE.isValid).toBe(true); + expect(INITIAL_STATE.isInvalid).toBe(false); + }); + + it('should mark control as enabled', () => { + expect(INITIAL_STATE.isEnabled).toBe(true); + expect(INITIAL_STATE.isDisabled).toBe(false); + }); + + it('should set empty errors', () => { + expect(INITIAL_STATE.errors).toEqual({}); + }); + + it('should mark control as pristine', () => { + expect(INITIAL_STATE.isPristine).toBe(true); + expect(INITIAL_STATE.isDirty).toBe(false); + }); + + it('should mark control as untouched', () => { + expect(INITIAL_STATE.isTouched).toBe(false); + expect(INITIAL_STATE.isUntouched).toBe(true); + }); + + it('should mark control as unsubmitted', () => { + expect(INITIAL_STATE.isSubmitted).toBe(false); + expect(INITIAL_STATE.isUnsubmitted).toBe(true); + }); + + it('should set empty user-defined properties', () => { + expect(INITIAL_STATE.userDefinedProperties).toEqual({}); + }); + + it('should create control child', () => { + expect(INITIAL_STATE.controls[0].value).toEqual(INITIAL_VALUE[0]); + expect(cast(INITIAL_STATE.controls[0]).isFocused).toBeDefined(); + }); + + it('should create group child', () => { + const initialValue = [{ control: 'a' }, { control: 'b' }]; + const initialState = createFormArrayState<{ control: string }>(FORM_CONTROL_ID, initialValue); + expect(initialState.controls[0].value).toEqual(initialValue[0]); + const controls = cast(initialState.controls[0]).controls; + expect(controls).toBeDefined(); + expect(Array.isArray(controls)).toBe(false); + }); + + it('should create array child', () => { + const initialValue = [['a'], ['b']]; + const initialState = createFormArrayState(FORM_CONTROL_ID, initialValue); + expect(initialState.controls[0].value).toEqual(initialValue[0]); + const controls = cast(initialState.controls[0]).controls; + expect(controls).toBeDefined(); + expect(Array.isArray(controls)).toBe(true); + }); + + it('should create mixed children', () => { + const initialValue = [['a'], { control: 'b' }]; + const initialState = createFormArrayState(FORM_CONTROL_ID, initialValue); + expect(initialState.controls[0].value).toEqual(initialValue[0]); + const controls = cast(initialState.controls[0]).controls; + expect(controls).toBeDefined(); + expect(Array.isArray(controls)).toBe(true); + expect(initialState.controls[1].value).toEqual(initialValue[1]); + const controls2 = cast(initialState.controls[1]).controls; + expect(controls2).toBeDefined(); + expect(Array.isArray(controls2)).toBe(false); + }); + }); +}); diff --git a/src/state.ts b/src/state.ts index b2fcd758..4d8ebdd2 100644 --- a/src/state.ts +++ b/src/state.ts @@ -31,9 +31,16 @@ export class FormGroupState extends AbstractControlStat readonly controls: FormGroupControls; } +export class FormArrayState extends AbstractControlState { + readonly controls: Array>; +} + export function cast( state: AbstractControlState, ): FormControlState; +export function cast( + state: AbstractControlState, +): FormArrayState; export function cast( state: AbstractControlState, ): FormGroupState; @@ -72,6 +79,10 @@ export function createFormGroupState( initialValue: TValue, ): FormGroupState { function createState(key: string, value: any) { + if (value !== null && Array.isArray(value)) { + return createFormArrayState(`${id}.${key}`, value); + } + if (value !== null && typeof value === 'object') { return createFormGroupState(`${id}.${key}`, value); } @@ -101,3 +112,41 @@ export function createFormGroupState( userDefinedProperties: {}, }; } + +export function createFormArrayState( + id: NgrxFormControlId, + initialValue: TValue[], +): FormArrayState { + function createState(index: number, value: any) { + if (value !== null && Array.isArray(value)) { + return createFormArrayState(`${id}.${index}`, value); + } + + if (value !== null && typeof value === 'object') { + return createFormGroupState(`${id}.${index}`, value); + } + + return createFormControlState(`${id}.${index}`, value); + } + + const controls = initialValue + .map((value, i) => createState(i, value) as AbstractControlState); + + return { + id, + value: initialValue, + isValid: true, + isInvalid: false, + isEnabled: true, + isDisabled: false, + errors: {}, + isPristine: true, + isDirty: false, + isTouched: false, + isUntouched: true, + controls, + isSubmitted: false, + isUnsubmitted: true, + userDefinedProperties: {}, + }; +} From 19bfb9f8abf85a78d7a249ecc6b2e6a9086b2421 Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sat, 28 Oct 2017 15:24:51 +0200 Subject: [PATCH 02/11] feat: first set of form array reducers + tests (some of which are still failing) --- src/array/reducer.spec.ts | 157 ++++++++++++++ src/array/reducer.ts | 28 +++ src/array/reducer/disable.spec.ts | 97 +++++++++ src/array/reducer/disable.ts | 28 +++ src/array/reducer/enable.spec.ts | 166 +++++++++++++++ src/array/reducer/enable.ts | 30 +++ src/array/reducer/mark-as-dirty.spec.ts | 102 +++++++++ src/array/reducer/mark-as-dirty.ts | 30 +++ src/array/reducer/mark-as-pristine.spec.ts | 195 ++++++++++++++++++ src/array/reducer/mark-as-pristine.ts | 28 +++ src/array/reducer/util.ts | 137 ++++++++++++ src/group/reducer.spec.ts | 10 +- src/group/reducer/add-control.spec.ts | 16 +- src/group/reducer/add-control.ts | 4 +- src/group/reducer/disable.spec.ts | 114 +++++++--- src/group/reducer/enable.spec.ts | 95 ++++++++- src/group/reducer/mark-as-dirty.spec.ts | 43 +++- src/group/reducer/mark-as-pristine.spec.ts | 160 ++++++++++++-- src/group/reducer/mark-as-submitted.spec.ts | 55 ++++- src/group/reducer/mark-as-touched.spec.ts | 55 ++++- src/group/reducer/mark-as-unsubmitted.spec.ts | 150 ++++++++++++-- src/group/reducer/mark-as-untouched.spec.ts | 151 ++++++++++++-- src/group/reducer/remove-control.spec.ts | 64 +++++- src/group/reducer/set-errors.spec.ts | 24 ++- src/group/reducer/set-value.spec.ts | 69 ++++++- src/group/reducer/set-value.ts | 4 +- src/group/reducer/util.ts | 31 ++- src/state.spec.ts | 10 + src/state.ts | 50 ++--- 29 files changed, 1937 insertions(+), 166 deletions(-) create mode 100644 src/array/reducer.spec.ts create mode 100644 src/array/reducer.ts create mode 100644 src/array/reducer/disable.spec.ts create mode 100644 src/array/reducer/disable.ts create mode 100644 src/array/reducer/enable.spec.ts create mode 100644 src/array/reducer/enable.ts create mode 100644 src/array/reducer/mark-as-dirty.spec.ts create mode 100644 src/array/reducer/mark-as-dirty.ts create mode 100644 src/array/reducer/mark-as-pristine.spec.ts create mode 100644 src/array/reducer/mark-as-pristine.ts create mode 100644 src/array/reducer/util.ts diff --git a/src/array/reducer.spec.ts b/src/array/reducer.spec.ts new file mode 100644 index 00000000..053c1ec9 --- /dev/null +++ b/src/array/reducer.spec.ts @@ -0,0 +1,157 @@ +import { + DisableAction, + EnableAction, + FocusAction, + MarkAsDirtyAction, + MarkAsPristineAction, + MarkAsSubmittedAction, + MarkAsTouchedAction, + MarkAsUnsubmittedAction, + MarkAsUntouchedAction, + SetErrorsAction, + SetUserDefinedPropertyAction, + SetValueAction, + UnfocusAction, +} from '../actions'; +import { cast, createFormArrayState } from '../state'; +import { formArrayReducerInternal } from './reducer'; + +describe('form group reducer', () => { + const FORM_CONTROL_ID = 'test ID'; + const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; + type FormArrayValue = string[]; + const INITIAL_FORM_CONTROL_VALUE: FormArrayValue = ['']; + const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); + + it('should skip any action with non-equal control ID', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new SetValueAction('A' + FORM_CONTROL_ID, 'A') as any); + expect(resultState).toBe(INITIAL_STATE); + }); + + it('should forward focus actions to children', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new FocusAction(FORM_CONTROL_0_ID) as any); + expect(cast(resultState.controls[0]).isFocused).toEqual(true); + expect(cast(resultState.controls[0]).isUnfocused).toEqual(false); + }); + + it('should forward unfocus actions to children', () => { + const state = { + ...INITIAL_STATE, + controls: [ + { + ...INITIAL_STATE.controls[0], + isFocused: true, + isUnfocused: false, + }, + ], + }; + const resultState = formArrayReducerInternal(state, new UnfocusAction(FORM_CONTROL_0_ID) as any); + expect(cast(resultState.controls[0]).isFocused).toEqual(false); + expect(cast(resultState.controls[0]).isUnfocused).toEqual(true); + }); + + it('should not update state if no child was updated', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new SetValueAction(FORM_CONTROL_0_ID, '') as any); + expect(resultState).toBe(INITIAL_STATE); + }); + + it('should not update state value if no child value was updated', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); + expect(resultState.value).toBe(INITIAL_STATE.value); + }); + + it('should not reset child states', () => { + const value = 'A'; + const state = formArrayReducerInternal(INITIAL_STATE, new SetValueAction(FORM_CONTROL_0_ID, value) as any); + const resultState = formArrayReducerInternal(state, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].value).toBe(value); + }); + + it('should not be stateful', () => { + formArrayReducerInternal(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, [])); + expect(() => formArrayReducerInternal(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_ID))).not.toThrowError(); + }); + + describe(SetValueAction.name, () => { + it('should update state', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, ['A'])); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(SetErrorsAction.name, () => { + it('should update state', () => { + const errors = { required: true }; + const resultState = formArrayReducerInternal(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_ID, errors)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(MarkAsDirtyAction.name, () => { + it('should update state', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(MarkAsPristineAction.name, () => { + it('should update state', () => { + const state = { ...INITIAL_STATE, isDirty: true, isPristine: false }; + const resultState = formArrayReducerInternal(state, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(EnableAction.name, () => { + it('should update state', () => { + const state = { ...INITIAL_STATE, isEnabled: false, isDisabled: true }; + const resultState = formArrayReducerInternal(state, new EnableAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(DisableAction.name, () => { + it('should update state', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new DisableAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(MarkAsTouchedAction.name, () => { + it('should update state', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(MarkAsUntouchedAction.name, () => { + it('should update state', () => { + const state = { ...INITIAL_STATE, isTouched: true, isUntouched: false }; + const resultState = formArrayReducerInternal(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(MarkAsSubmittedAction.name, () => { + it('should update state', () => { + const resultState = formArrayReducerInternal(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(MarkAsUnsubmittedAction.name, () => { + it('should update state', () => { + const state = { ...INITIAL_STATE, isSubmitted: true, isUnsubmitted: false }; + const resultState = formArrayReducerInternal(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); + + describe(SetUserDefinedPropertyAction.name, () => { + it('should update state', () => { + const action = new SetUserDefinedPropertyAction(FORM_CONTROL_ID, 'prop', 12); + const resultState = formArrayReducerInternal(INITIAL_STATE, action); + expect(resultState).not.toBe(INITIAL_STATE); + }); + }); +}); diff --git a/src/array/reducer.ts b/src/array/reducer.ts new file mode 100644 index 00000000..daf06cd4 --- /dev/null +++ b/src/array/reducer.ts @@ -0,0 +1,28 @@ +import { Action } from '@ngrx/store'; + +import { Actions, FocusAction, UnfocusAction } from '../actions'; +import { FormArrayState } from '../state'; +import { disableReducer } from './reducer/disable'; +import { enableReducer } from './reducer/enable'; +import { markAsDirtyReducer } from './reducer/mark-as-dirty'; +import { markAsPristineReducer } from './reducer/mark-as-pristine'; +import { childReducer } from './reducer/util'; + +export function formArrayReducerInternal(state: FormArrayState, action: Actions) { + switch (action.type) { + case FocusAction.TYPE: + case UnfocusAction.TYPE: + return childReducer(state, action); + } + + state = enableReducer(state, action); + state = disableReducer(state, action); + state = markAsDirtyReducer(state, action); + state = markAsPristineReducer(state, action); + + return state; +} + +export function formArrayReducer(state: FormArrayState, action: Action) { + return formArrayReducerInternal(state, action as any); +} diff --git a/src/array/reducer/disable.spec.ts b/src/array/reducer/disable.spec.ts new file mode 100644 index 00000000..275404d1 --- /dev/null +++ b/src/array/reducer/disable.spec.ts @@ -0,0 +1,97 @@ +import { DisableAction } from '../../actions'; +import { createFormArrayState } from '../../state'; +import { disableReducer } from './disable'; + +describe('form group disableReducer', () => { + const FORM_CONTROL_ID = 'test ID'; + const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; + const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; + const INITIAL_FORM_ARRAY_VALUE = ['', '']; + const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }]; + const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [['']]; + const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); + const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); + const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); + + it('should update state if enabled', () => { + const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_ID)); + expect(resultState.isEnabled).toEqual(false); + expect(resultState.isDisabled).toEqual(true); + }); + + it('should not update state if disabled', () => { + const state = { ...INITIAL_STATE, isEnabled: false, isDisabled: true }; + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_ID)); + expect(resultState).toBe(state); + }); + + it('should mark the state as valid and clear all errors', () => { + const errors = { required: true }; + const state = { ...INITIAL_STATE, isValid: false, isInvalid: true, errors }; + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_ID)); + expect(resultState.isValid).toEqual(true); + expect(resultState.isInvalid).toEqual(false); + expect(resultState.errors).toEqual({}); + }); + + it('should disable control children', () => { + const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isEnabled).toBe(false); + expect(resultState.controls[0].isDisabled).toBe(true); + expect(resultState.controls[1].isEnabled).toBe(false); + expect(resultState.controls[1].isDisabled).toBe(true); + }); + + it('should disable group children', () => { + const resultState = disableReducer(INITIAL_STATE_NESTED_GROUP, new DisableAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isEnabled).toBe(false); + expect(resultState.controls[0].isDisabled).toBe(true); + }); + + it('should disable array children', () => { + const resultState = disableReducer(INITIAL_STATE_NESTED_ARRAY, new DisableAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isEnabled).toBe(false); + expect(resultState.controls[0].isDisabled).toBe(true); + }); + + it('should disable if all children are disabled when control child is disabled', () => { + const state = { + ...INITIAL_STATE, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_0_ID)); + expect(resultState.isEnabled).toBe(false); + expect(resultState.isDisabled).toBe(true); + }); + + it('should not disable if not all children are disabled when direct control child is disabled', () => { + const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_0_ID)); + expect(resultState.isEnabled).toBe(true); + expect(resultState.isDisabled).toBe(false); + }); + + it('should disable if all children are disabled when direct group child is disabled', () => { + const resultState = disableReducer(INITIAL_STATE_NESTED_GROUP, new DisableAction(FORM_CONTROL_0_ID)); + expect(resultState.isEnabled).toBe(false); + expect(resultState.isDisabled).toBe(true); + }); + + it('should disable if all children are disabled when direct array child is disabled', () => { + const resultState = disableReducer(INITIAL_STATE_NESTED_ARRAY, new DisableAction(FORM_CONTROL_0_ID)); + expect(resultState.isEnabled).toBe(false); + expect(resultState.isDisabled).toBe(true); + }); + + it('should forward actions to children', () => { + const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_0_ID)); + expect(resultState.controls[0].isEnabled).toEqual(false); + expect(resultState.controls[0].isDisabled).toEqual(true); + }); +}); diff --git a/src/array/reducer/disable.ts b/src/array/reducer/disable.ts new file mode 100644 index 00000000..0f26125d --- /dev/null +++ b/src/array/reducer/disable.ts @@ -0,0 +1,28 @@ +import { Actions, DisableAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; + +export function disableReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== DisableAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + if (state.isDisabled) { + return state; + } + + return computeArrayState( + state.id, + dispatchActionPerChild(state.controls, controlId => new DisableAction(controlId)), + state.value, + {}, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/enable.spec.ts b/src/array/reducer/enable.spec.ts new file mode 100644 index 00000000..a8e7cbfb --- /dev/null +++ b/src/array/reducer/enable.spec.ts @@ -0,0 +1,166 @@ +import { EnableAction } from '../../actions'; +import { createFormArrayState } from '../../state'; +import { enableReducer } from './enable'; + +describe('form group enableReducer', () => { + const FORM_CONTROL_ID = 'test ID'; + const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; + const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; + const INITIAL_FORM_ARRAY_VALUE = ['', '']; + const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }, { inner2: '' }]; + const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [[''], ['']]; + const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); + const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); + const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); + + it('should enable itself and all children recursively', () => { + const state = { + ...INITIAL_STATE, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...INITIAL_STATE.controls[0], + isEnabled: false, + isDisabled: true, + }, + { + ...INITIAL_STATE.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE); + }); + + it('should not update state if all children are enabled recursively', () => { + const resultState = enableReducer(INITIAL_STATE, new EnableAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE); + }); + + it('should enable children if the group itself is already enabled', () => { + const state = { + ...INITIAL_STATE, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE); + }); + + it('should enable if direct control child gets enabled', () => { + const state = { + ...INITIAL_STATE, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_1_ID)); + expect(resultState).toEqual(INITIAL_STATE); + }); + + it('should enable without enabling any other children if direct control child gets enabled', () => { + const state = { + ...INITIAL_STATE, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...INITIAL_STATE.controls[0], + isEnabled: false, + isDisabled: true, + }, + { + ...INITIAL_STATE.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_0_ID)); + expect(resultState.isEnabled).toBe(true); + expect(resultState.isDisabled).toBe(false); + expect(resultState.controls[1].isEnabled).toBe(false); + expect(resultState.controls[1].isDisabled).toBe(true); + }); + + it('should enable without enabling any other children if direct group child gets enabled', () => { + const state = { + ...INITIAL_STATE_NESTED_GROUP, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...INITIAL_STATE_NESTED_GROUP.controls[0], + isEnabled: false, + isDisabled: true, + }, + { + ...INITIAL_STATE_NESTED_GROUP.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_0_ID)); + expect(resultState.isEnabled).toBe(true); + expect(resultState.isDisabled).toBe(false); + expect(resultState.controls[1].isEnabled).toBe(false); + expect(resultState.controls[1].isDisabled).toBe(true); + }); + + it('should enable without enabling any other children if direct array child gets enabled', () => { + const state = { + ...INITIAL_STATE_NESTED_ARRAY, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...INITIAL_STATE_NESTED_ARRAY.controls[0], + isEnabled: false, + isDisabled: true, + }, + { + ...INITIAL_STATE_NESTED_ARRAY.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_0_ID)); + expect(resultState.isEnabled).toBe(true); + expect(resultState.isDisabled).toBe(false); + expect(resultState.controls[1].isEnabled).toBe(false); + expect(resultState.controls[1].isDisabled).toBe(true); + }); + + it('should forward actions to children', () => { + const state = { + ...INITIAL_STATE, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isEnabled: false, + isDisabled: true, + }, + ], + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_1_ID)); + expect(resultState.controls[1].isEnabled).toEqual(true); + expect(resultState.controls[1].isDisabled).toEqual(false); + }); +}); diff --git a/src/array/reducer/enable.ts b/src/array/reducer/enable.ts new file mode 100644 index 00000000..7f3acbae --- /dev/null +++ b/src/array/reducer/enable.ts @@ -0,0 +1,30 @@ +import { Actions, EnableAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; + +export function enableReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== EnableAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + const controls = dispatchActionPerChild(state.controls, controlId => new EnableAction(controlId)); + + if (controls === state.controls) { + return state; + } + + return computeArrayState( + state.id, + controls, + state.value, + state.errors, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/mark-as-dirty.spec.ts b/src/array/reducer/mark-as-dirty.spec.ts new file mode 100644 index 00000000..feb4c0b7 --- /dev/null +++ b/src/array/reducer/mark-as-dirty.spec.ts @@ -0,0 +1,102 @@ +import { MarkAsDirtyAction } from '../../actions'; +import { createFormArrayState } from '../../state'; +import { markAsDirtyReducer } from './mark-as-dirty'; + +describe('form group markAsDirtyReducer', () => { + const FORM_CONTROL_ID = 'test ID'; + const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; + const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; + const INITIAL_FORM_ARRAY_VALUE = ['', '']; + const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }]; + const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [['']]; + const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); + const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); + const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); + const INITIAL_STATE_DIRTY = { + ...INITIAL_STATE, + isDirty: true, + isPristine: false, + controls: [ + { + ...INITIAL_STATE.controls[0], + isDirty: true, + isPristine: false, + }, + { + ...INITIAL_STATE.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + + it('should mark itself and all children recursively as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE_DIRTY); + }); + + it('should not update state if all children are marked as dirty recursively', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_DIRTY, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE_DIRTY); + }); + + it('should mark children as dirty if the group itself is already marked as dirty', () => { + const state = { + ...INITIAL_STATE, + isDirty: true, + isPristine: false, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsDirtyReducer(state, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE_DIRTY); + }); + + it('should mark control children as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isDirty).toEqual(true); + expect(resultState.controls[0].isPristine).toEqual(false); + }); + + it('should mark group children as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_NESTED_GROUP, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isDirty).toEqual(true); + expect(resultState.controls[0].isPristine).toEqual(false); + }); + + it('should mark array children as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_NESTED_ARRAY, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isDirty).toEqual(true); + expect(resultState.controls[0].isPristine).toEqual(false); + }); + + it('should mark state as dirty if direct control child is marked as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + + it('should mark state as dirty if direct group child is marked as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_NESTED_GROUP, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + + it('should mark state as dirty if direct array child is marked as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_NESTED_ARRAY, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + + it('should forward actions to children', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); + expect(resultState.controls[0].isDirty).toEqual(true); + expect(resultState.controls[0].isPristine).toEqual(false); + }); +}); diff --git a/src/array/reducer/mark-as-dirty.ts b/src/array/reducer/mark-as-dirty.ts new file mode 100644 index 00000000..1856acea --- /dev/null +++ b/src/array/reducer/mark-as-dirty.ts @@ -0,0 +1,30 @@ +import { Actions, MarkAsDirtyAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; + +export function markAsDirtyReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== MarkAsDirtyAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + const controls = dispatchActionPerChild(state.controls, controlId => new MarkAsDirtyAction(controlId)); + + if (controls === state.controls) { + return state; + } + + return computeArrayState( + state.id, + controls, + state.value, + state.errors, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/mark-as-pristine.spec.ts b/src/array/reducer/mark-as-pristine.spec.ts new file mode 100644 index 00000000..9e39153f --- /dev/null +++ b/src/array/reducer/mark-as-pristine.spec.ts @@ -0,0 +1,195 @@ +import { MarkAsPristineAction } from '../../actions'; +import { cast, createFormArrayState } from '../../state'; +import { markAsPristineReducer } from './mark-as-pristine'; + +describe('form group markAsPristineReducer', () => { + const FORM_CONTROL_ID = 'test ID'; + const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; + const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; + const INITIAL_FORM_ARRAY_VALUE = ['', '']; + const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }, { inner2: '' }]; + const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [[''], ['']]; + const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); + const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); + const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); + + it('should update state if dirty', () => { + const state = { ...INITIAL_STATE, isDirty: true, isPristine: false }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(resultState.isDirty).toEqual(false); + expect(resultState.isPristine).toEqual(true); + }); + + it('should not update state if pristine', () => { + const resultState = markAsPristineReducer(INITIAL_STATE, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE); + }); + + it('should mark direct control children as pristine', () => { + const state = { + ...INITIAL_STATE, + isDirty: true, + isPristine: false, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isDirty).toEqual(false); + expect(resultState.controls[0].isPristine).toEqual(true); + }); + + it('should mark direct group children as pristine', () => { + const state = { + ...INITIAL_STATE_NESTED_GROUP, + isDirty: true, + isPristine: false, + controls: [ + INITIAL_STATE_NESTED_GROUP.controls[0], + { + ...INITIAL_STATE_NESTED_GROUP.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isDirty).toEqual(false); + expect(resultState.controls[0].isPristine).toEqual(true); + }); + + it('should mark direct array children as pristine', () => { + const state = { + ...INITIAL_STATE_NESTED_ARRAY, + isDirty: true, + isPristine: false, + controls: [ + INITIAL_STATE_NESTED_ARRAY.controls[0], + { + ...INITIAL_STATE_NESTED_ARRAY.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isDirty).toEqual(false); + expect(resultState.controls[0].isPristine).toEqual(true); + }); + + it('should mark state as pristine if all children are pristine when direct control child is updated', () => { + const state = { + ...INITIAL_STATE, + isDirty: true, + isPristine: false, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_1_ID)); + expect(resultState.isDirty).toEqual(false); + expect(resultState.isPristine).toEqual(true); + }); + + it('should not mark state as pristine if not all children are pristine when direct control child is updated', () => { + const state = { + ...INITIAL_STATE, + isDirty: true, + isPristine: false, + controls: [ + { + ...INITIAL_STATE.controls[0], + isDirty: true, + isPristine: false, + }, + { + ...INITIAL_STATE.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + + it('should mark state as pristine if all children are pristine when direct group child is updated', () => { + const state = { + ...INITIAL_STATE_NESTED_GROUP, + isDirty: true, + isPristine: false, + controls: [ + { + ...INITIAL_STATE_NESTED_GROUP.controls[0], + isDirty: true, + isPristine: false, + }, + { + ...INITIAL_STATE_NESTED_GROUP.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + + it('should mark state as pristine if all children are pristine when direct array child is updated', () => { + const state = { + ...INITIAL_STATE_NESTED_ARRAY, + isDirty: true, + isPristine: false, + controls: [ + { + ...INITIAL_STATE_NESTED_ARRAY.controls[0], + isDirty: true, + isPristine: false, + }, + { + ...INITIAL_STATE_NESTED_ARRAY.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + + it('should forward actions to children', () => { + const state = { + ...INITIAL_STATE, + isDirty: true, + isPristine: false, + controls: [ + { + ...INITIAL_STATE.controls[0], + isDirty: true, + isPristine: false, + }, + { + ...INITIAL_STATE.controls[1], + isDirty: true, + isPristine: false, + }, + ], + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); + expect(resultState.controls[0].isDirty).toEqual(false); + expect(resultState.controls[0].isPristine).toEqual(true); + }); +}); diff --git a/src/array/reducer/mark-as-pristine.ts b/src/array/reducer/mark-as-pristine.ts new file mode 100644 index 00000000..6ebb9bfb --- /dev/null +++ b/src/array/reducer/mark-as-pristine.ts @@ -0,0 +1,28 @@ +import { Actions, MarkAsPristineAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; + +export function markAsPristineReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== MarkAsPristineAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + if (state.isPristine) { + return state; + } + + return computeArrayState( + state.id, + dispatchActionPerChild(state.controls, controlId => new MarkAsPristineAction(controlId)), + state.value, + state.errors, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/util.ts b/src/array/reducer/util.ts new file mode 100644 index 00000000..2dd0c3b2 --- /dev/null +++ b/src/array/reducer/util.ts @@ -0,0 +1,137 @@ +import { Actions } from '../../actions'; +import { formControlReducerInternal } from '../../control/reducer'; +import { formGroupReducerInternal } from '../../group/reducer'; +import { AbstractControlState, FormArrayState, FormGroupControls, KeyValue, ValidationErrors } from '../../state'; +import { isEmpty } from '../../util'; +import { formArrayReducerInternal } from '../reducer'; + +export function getFormArrayValue( + controls: Array>, + originalValue: TValue[], +): TValue[] { + let hasChanged = Object.keys(originalValue).length !== Object.keys(controls).length; + const newValue = controls.map((state, i) => { + hasChanged = hasChanged || originalValue[i] !== state.value; + return state.value; + }); + + return hasChanged ? newValue : originalValue; +} + +export function getFormArrayErrors( + controls: Array>, + originalErrors: ValidationErrors, +): ValidationErrors { + let hasChanged = false; + const groupErrors = + Object.keys(originalErrors) + .filter(key => !key.startsWith('_')) + .reduce((res, key) => Object.assign(res, { [key]: originalErrors[key] }), {}); + + const newErrors = controls.reduce((res, state, i) => { + const controlErrors = state.errors; + hasChanged = hasChanged || originalErrors['_' + i] !== controlErrors; + if (!isEmpty(controlErrors)) { + res['_' + i] = controlErrors; + } + return res; + }, groupErrors as ValidationErrors); + + hasChanged = hasChanged || Object.keys(originalErrors).length !== Object.keys(newErrors).length; + + return hasChanged ? newErrors : originalErrors; +} + +export function computeArrayState( + id: string, + controls: Array>, + value: TValue[], + errors: ValidationErrors, + userDefinedProperties: KeyValue, +) { + value = getFormArrayValue(controls, value); + errors = getFormArrayErrors(controls, errors); + const isValid = isEmpty(errors); + const isDirty = controls.some(state => state.isDirty); + const isEnabled = controls.some(state => state.isEnabled); + const isTouched = controls.some(state => state.isTouched); + const isSubmitted = controls.some(state => state.isSubmitted); + return { + id, + value, + errors, + isValid, + isInvalid: !isValid, + isDirty, + isPristine: !isDirty, + isEnabled, + isDisabled: !isEnabled, + isTouched, + isUntouched: !isTouched, + isSubmitted, + isUnsubmitted: !isSubmitted, + controls, + userDefinedProperties, + }; +} + +export function isArrayState(state: AbstractControlState): boolean { + return state.hasOwnProperty('controls') && Array.isArray((state as any).controls); +} + +export function isGroupState(state: AbstractControlState): boolean { + return state.hasOwnProperty('controls'); +} + +export function callChildReducer( + state: AbstractControlState, + action: Actions, +): AbstractControlState { + if (isArrayState(state)) { + return formArrayReducerInternal(state as any, action as any); + } + + if (isGroupState(state)) { + return formGroupReducerInternal(state as any, action); + } + + return formControlReducerInternal(state as any, action); +} + +export function dispatchActionPerChild( + controls: Array>, + actionCreator: (controlId: string) => Actions, +) { + let hasChanged = false; + const newControls = controls + .map(state => { + const newState = callChildReducer(state, actionCreator(state.id)); + hasChanged = hasChanged || state !== newState; + return newState; + }); + return hasChanged ? newControls : controls; +} + +function callChildReducers( + controls: Array>, + action: Actions, +): Array> { + let hasChanged = false; + const newControls = controls + .map(state => { + const newState = callChildReducer(state, action); + hasChanged = hasChanged || state !== newState; + return newState; + }); + return hasChanged ? newControls : controls; +} + +export function childReducer(state: FormArrayState, action: Actions) { + const controls = callChildReducers(state.controls, action); + + if (state.controls === controls) { + return state; + } + + return computeArrayState(state.id, controls, state.value, state.errors, state.userDefinedProperties); +} diff --git a/src/group/reducer.spec.ts b/src/group/reducer.spec.ts index ac8366c9..a987772b 100644 --- a/src/group/reducer.spec.ts +++ b/src/group/reducer.spec.ts @@ -15,7 +15,7 @@ import { SetValueAction, UnfocusAction, } from '../actions'; -import { createFormGroupState, FormControlState } from '../state'; +import { cast, createFormGroupState } from '../state'; import { formGroupReducerInternal } from './reducer'; describe('form group reducer', () => { @@ -34,8 +34,8 @@ describe('form group reducer', () => { it('should forward focus actions to children', () => { const resultState = formGroupReducerInternal(INITIAL_STATE, new FocusAction(FORM_CONTROL_INNER_ID) as any); - expect((resultState.controls.inner as FormControlState).isFocused).toEqual(true); - expect((resultState.controls.inner as FormControlState).isUnfocused).toEqual(false); + expect(cast(resultState.controls.inner).isFocused).toEqual(true); + expect(cast(resultState.controls.inner).isUnfocused).toEqual(false); }); it('should forward unfocus actions to children', () => { @@ -50,8 +50,8 @@ describe('form group reducer', () => { }, }; const resultState = formGroupReducerInternal(state, new UnfocusAction(FORM_CONTROL_INNER_ID) as any); - expect((resultState.controls.inner as FormControlState).isFocused).toEqual(false); - expect((resultState.controls.inner as FormControlState).isUnfocused).toEqual(true); + expect(cast(resultState.controls.inner).isFocused).toEqual(false); + expect(cast(resultState.controls.inner).isUnfocused).toEqual(true); }); it('should not update state if no child was updated', () => { diff --git a/src/group/reducer/add-control.spec.ts b/src/group/reducer/add-control.spec.ts index 9d80f7d8..a190077b 100644 --- a/src/group/reducer/add-control.spec.ts +++ b/src/group/reducer/add-control.spec.ts @@ -1,10 +1,10 @@ -import { createFormGroupState } from '../../state'; import { AddControlAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { addControlReducer } from './add-control'; describe('form group addControlReducer', () => { const FORM_CONTROL_ID = 'test ID'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); @@ -22,6 +22,18 @@ describe('form group addControlReducer', () => { const resultState = addControlReducer(INITIAL_STATE, action); expect(resultState.value).toEqual({ inner: '', inner3: value }); expect(resultState.controls.inner3.value).toBe(value); + expect(cast(resultState.controls.inner3)!.controls).toBeDefined(); + expect(Array.isArray(cast(resultState.controls.inner3)!.controls)).toBe(false); + }); + + it('should create child state for array children', () => { + const value = ['A']; + const action = new AddControlAction(FORM_CONTROL_ID, 'inner5', value); + const resultState = addControlReducer(INITIAL_STATE, action); + expect(resultState.value).toEqual({ inner: '', inner5: value }); + expect(resultState.controls.inner5.value).toBe(value); + expect(cast(resultState.controls.inner5)!.controls).toBeDefined(); + expect(Array.isArray(cast(resultState.controls.inner5)!.controls)).toBe(true); }); it('should throw if trying to add existing control', () => { diff --git a/src/group/reducer/add-control.ts b/src/group/reducer/add-control.ts index 68933963..1f17202b 100644 --- a/src/group/reducer/add-control.ts +++ b/src/group/reducer/add-control.ts @@ -1,6 +1,6 @@ -import { FormGroupState, KeyValue } from '../../state'; import { Actions, AddControlAction } from '../../actions'; -import { computeGroupState, childReducer, createChildState } from './util'; +import { createChildState, FormGroupState, KeyValue } from '../../state'; +import { childReducer, computeGroupState } from './util'; export function addControlReducer( state: FormGroupState, diff --git a/src/group/reducer/disable.spec.ts b/src/group/reducer/disable.spec.ts index eb8ede86..375bd53c 100644 --- a/src/group/reducer/disable.spec.ts +++ b/src/group/reducer/disable.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { DisableAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { disableReducer } from './disable'; describe('form group disableReducer', () => { @@ -7,9 +7,11 @@ describe('form group disableReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); @@ -46,10 +48,22 @@ describe('form group disableReducer', () => { expect(resultState.controls.inner3.isDisabled).toBe(true); }); - it('should disable nested children', () => { + it('should disable direct array children', () => { const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_ID)); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isEnabled).toBe(false); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isDisabled).toBe(true); + expect(resultState.controls.inner5.isEnabled).toBe(false); + expect(resultState.controls.inner5.isDisabled).toBe(true); + }); + + it('should disable nested children in group', () => { + const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_ID)); + expect(cast(resultState.controls.inner3)!.controls.inner4.isEnabled).toBe(false); + expect(cast(resultState.controls.inner3)!.controls.inner4.isDisabled).toBe(true); + }); + + it('should disable nested children in array', () => { + const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_ID)); + expect(cast(resultState.controls.inner5)!.controls[0].isEnabled).toBe(false); + expect(cast(resultState.controls.inner5)!.controls[0].isDisabled).toBe(true); }); it('should disable if all children are disabled when direct control child is disabled', () => { @@ -79,6 +93,18 @@ describe('form group disableReducer', () => { isEnabled: false, isDisabled: true, }, + inner5: { + ...INITIAL_STATE_FULL.controls.inner5, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...cast(INITIAL_STATE_FULL.controls.inner5)!.controls[0], + isEnabled: false, + isDisabled: true, + }, + ], + }, }, }; const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER3_ID)); @@ -86,7 +112,7 @@ describe('form group disableReducer', () => { expect(resultState.isDisabled).toBe(true); }); - it('should disable if all children are disabled when nested child is disabled', () => { + it('should disable if all children are disabled when direct array child is disabled', () => { const state = { ...INITIAL_STATE_FULL, controls: { @@ -101,32 +127,26 @@ describe('form group disableReducer', () => { isEnabled: false, isDisabled: true, }, + inner3: { + ...INITIAL_STATE_FULL.controls.inner3, + isEnabled: false, + isDisabled: true, + controls: { + inner4: { + ...cast(INITIAL_STATE_FULL.controls.inner3)!.controls.inner4, + isEnabled: false, + isDisabled: true, + }, + }, + }, }, }; - const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isEnabled).toBe(false); - expect(resultState.isDisabled).toBe(true); - }); - - it('should not disable if not all children are disabled when nested child is disabled', () => { - const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isEnabled).toBe(true); - expect(resultState.isDisabled).toBe(false); - }); - - it('should disable if all children are disabled when direct control child is disabled', () => { - const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_INNER_ID)); + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - it('should not disable if not all children are disabled when direct control child is disabled', () => { - const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER_ID)); - expect(resultState.isEnabled).toBe(true); - expect(resultState.isDisabled).toBe(false); - }); - - it('should disable if all children are disabled when direct group child is disabled', () => { + it('should disable if all children are disabled when nested child in group is disabled', () => { const state = { ...INITIAL_STATE_FULL, controls: { @@ -141,14 +161,32 @@ describe('form group disableReducer', () => { isEnabled: false, isDisabled: true, }, + inner5: { + ...INITIAL_STATE_FULL.controls.inner5, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...cast(INITIAL_STATE_FULL.controls.inner5)!.controls[0], + isEnabled: false, + isDisabled: true, + }, + ], + }, }, }; - const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER3_ID)); + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER4_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - it('should disable if all children are disabled when nested child is disabled', () => { + it('should not disable if not all children are disabled when nested child in group is disabled', () => { + const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER4_ID)); + expect(resultState.isEnabled).toBe(true); + expect(resultState.isDisabled).toBe(false); + }); + + it('should disable if all children are disabled when nested child in array is disabled', () => { const state = { ...INITIAL_STATE_FULL, controls: { @@ -163,15 +201,27 @@ describe('form group disableReducer', () => { isEnabled: false, isDisabled: true, }, + inner3: { + ...INITIAL_STATE_FULL.controls.inner3, + isEnabled: false, + isDisabled: true, + controls: { + inner4: { + ...cast(INITIAL_STATE_FULL.controls.inner3)!.controls.inner4, + isEnabled: false, + isDisabled: true, + }, + }, + }, }, }; - const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER4_ID)); + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER5_0_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - it('should not disable if not all children are disabled when nested child is disabled', () => { - const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER4_ID)); + it('should not disable if not all children are disabled when nested child in array is disabled', () => { + const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER5_0_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); }); diff --git a/src/group/reducer/enable.spec.ts b/src/group/reducer/enable.spec.ts index 7a49e34c..fbf26a5d 100644 --- a/src/group/reducer/enable.spec.ts +++ b/src/group/reducer/enable.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { EnableAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { enableReducer } from './enable'; describe('form group enableReducer', () => { @@ -8,14 +8,17 @@ describe('form group enableReducer', () => { const FORM_CONTROL_INNER2_ID = FORM_CONTROL_ID + '.inner2'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); it('should enable itself and all children recursively', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; const state = { ...INITIAL_STATE_FULL, isEnabled: false, @@ -40,6 +43,18 @@ describe('form group enableReducer', () => { }, }, }, + inner5: { + ...inner5State, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...inner5State.controls[0], + isEnabled: false, + isDisabled: true, + }, + ], + }, }, }; const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_ID)); @@ -111,8 +126,42 @@ describe('form group enableReducer', () => { expect(resultState.controls.inner.isDisabled).toBe(true); }); - it('should enable without enabling any other children if nested child gets enabled', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + it('should enable without enabling any other children if direct array child gets enabled', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isEnabled: false, + isDisabled: true, + controls: { + ...INITIAL_STATE_FULL.controls, + inner: { + ...INITIAL_STATE_FULL.controls.inner, + isEnabled: false, + isDisabled: true, + }, + inner5: { + ...inner5State, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...inner5State.controls[0], + isEnabled: false, + isDisabled: true, + }, + ], + }, + }, + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER5_ID)); + expect(resultState.isEnabled).toBe(true); + expect(resultState.isDisabled).toBe(false); + expect(resultState.controls.inner.isEnabled).toBe(false); + expect(resultState.controls.inner.isDisabled).toBe(true); + }); + + it('should enable without enabling any other children if nested child in group gets enabled', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isEnabled: false, @@ -146,6 +195,40 @@ describe('form group enableReducer', () => { expect(resultState.controls.inner.isDisabled).toBe(true); }); + it('should enable without enabling any other children if nested child in array gets enabled', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isEnabled: false, + isDisabled: true, + controls: { + ...INITIAL_STATE_FULL.controls, + inner: { + ...INITIAL_STATE_FULL.controls.inner, + isEnabled: false, + isDisabled: true, + }, + inner5: { + ...inner5State, + isEnabled: false, + isDisabled: true, + controls: [ + { + ...inner5State.controls[0], + isEnabled: false, + isDisabled: true, + }, + ], + }, + }, + }; + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER5_0_ID)); + expect(resultState.isEnabled).toBe(true); + expect(resultState.isDisabled).toBe(false); + expect(resultState.controls.inner.isEnabled).toBe(false); + expect(resultState.controls.inner.isDisabled).toBe(true); + }); + it('should forward actions to children', () => { const state = { ...INITIAL_STATE_FULL, diff --git a/src/group/reducer/mark-as-dirty.spec.ts b/src/group/reducer/mark-as-dirty.spec.ts index 0ec6e190..c8e15562 100644 --- a/src/group/reducer/mark-as-dirty.spec.ts +++ b/src/group/reducer/mark-as-dirty.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { MarkAsDirtyAction } from '../../actions'; +import { cast, createFormGroupState, FormGroupState } from '../../state'; import { markAsDirtyReducer } from './mark-as-dirty'; describe('form group markAsDirtyReducer', () => { @@ -7,12 +7,15 @@ describe('form group markAsDirtyReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - const INITIAL_STATE_FULL_INNER3 = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + const INITIAL_STATE_FULL_INNER3 = cast(INITIAL_STATE_FULL.controls.inner3)!; + const INITIAL_STATE_FULL_INNER5 = cast(INITIAL_STATE_FULL.controls.inner5)!; const INITIAL_STATE_FULL_DIRTY = { ...INITIAL_STATE_FULL, isDirty: true, @@ -42,6 +45,18 @@ describe('form group markAsDirtyReducer', () => { }, }, }, + inner5: { + ...INITIAL_STATE_FULL_INNER5, + isDirty: true, + isPristine: false, + controls: [ + { + ...INITIAL_STATE_FULL_INNER5.controls[0], + isDirty: true, + isPristine: false, + }, + ], + }, }, }; @@ -85,6 +100,12 @@ describe('form group markAsDirtyReducer', () => { expect(resultState.controls.inner3.isPristine).toEqual(false); }); + it('should mark direct array children as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_ID)); + expect(resultState.controls.inner5.isDirty).toEqual(true); + expect(resultState.controls.inner5.isPristine).toEqual(false); + }); + it('should mark nested children as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_ID)); expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isDirty).toBe(true); @@ -103,12 +124,24 @@ describe('form group markAsDirtyReducer', () => { expect(resultState.isPristine).toEqual(false); }); - it('should mark state as dirty if nested child is marked as dirty', () => { + it('should mark state as dirty if direct array child is marked as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_INNER5_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + + it('should mark state as dirty if nested child in group is marked as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_INNER4_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); + it('should mark state as dirty if nested child in array is marked as dirty', () => { + const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_INNER5_0_ID)); + expect(resultState.isDirty).toEqual(true); + expect(resultState.isPristine).toEqual(false); + }); + it('should forward actions to children', () => { const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_INNER_ID)); expect(resultState.controls.inner.isDirty).toEqual(true); diff --git a/src/group/reducer/mark-as-pristine.spec.ts b/src/group/reducer/mark-as-pristine.spec.ts index 928fe9d4..f9e25c5a 100644 --- a/src/group/reducer/mark-as-pristine.spec.ts +++ b/src/group/reducer/mark-as-pristine.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { MarkAsPristineAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { markAsPristineReducer } from './mark-as-pristine'; describe('form group markAsPristineReducer', () => { @@ -7,9 +7,11 @@ describe('form group markAsPristineReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); @@ -45,6 +47,7 @@ describe('form group markAsPristineReducer', () => { }); it('should mark direct group children as pristine', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isDirty: true, @@ -52,9 +55,16 @@ describe('form group markAsPristineReducer', () => { controls: { ...INITIAL_STATE_FULL.controls, inner3: { - ...INITIAL_STATE_FULL.controls.inner3, + ...inner3State, isDirty: true, isPristine: false, + controls: { + inner4: { + ...inner3State.controls.inner4, + isDirty: true, + isPristine: false, + }, + }, }, }, }; @@ -63,8 +73,35 @@ describe('form group markAsPristineReducer', () => { expect(resultState.controls.inner3.isPristine).toEqual(true); }); - it('should mark nested children as pristine', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + it('should mark direct array children as pristine', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isDirty: true, + isPristine: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isDirty: true, + isPristine: false, + controls: [ + { + ...inner5State.controls[0], + isDirty: true, + isPristine: false, + }, + ], + }, + }, + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(resultState.controls.inner5.isDirty).toEqual(false); + expect(resultState.controls.inner5.isPristine).toEqual(true); + }); + + it('should mark nested children in group as pristine', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isDirty: true, @@ -87,8 +124,35 @@ describe('form group markAsPristineReducer', () => { }, }; const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isDirty).toBe(false); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isPristine).toBe(true); + expect(cast(resultState.controls.inner3)!.controls.inner4.isDirty).toBe(false); + expect(cast(resultState.controls.inner3)!.controls.inner4.isPristine).toBe(true); + }); + + it('should mark nested children in array as pristine', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isDirty: true, + isPristine: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isDirty: true, + isPristine: false, + controls: [ + { + ...inner5State.controls[0], + isDirty: true, + isPristine: false, + }, + ], + }, + }, + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); + expect(cast(resultState.controls.inner5)!.controls[0].isDirty).toBe(false); + expect(cast(resultState.controls.inner5)!.controls[0].isPristine).toBe(true); }); it('should mark state as pristine if all children are pristine when direct control child is updated', () => { @@ -111,6 +175,7 @@ describe('form group markAsPristineReducer', () => { }); it('should not mark state as pristine if not all children are pristine when direct control child is updated', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isDirty: true, @@ -123,9 +188,17 @@ describe('form group markAsPristineReducer', () => { isPristine: false, }, inner3: { - ...INITIAL_STATE_FULL.controls.inner3, + ...inner3State, isDirty: true, isPristine: false, + controls: { + ...inner3State.controls, + inner4: { + ...inner3State.controls.inner4, + isDirty: true, + isPristine: false, + }, + }, }, }, }; @@ -135,6 +208,7 @@ describe('form group markAsPristineReducer', () => { }); it('should mark state as pristine if all children are pristine when direct group child is updated', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isDirty: true, @@ -142,9 +216,17 @@ describe('form group markAsPristineReducer', () => { controls: { ...INITIAL_STATE_FULL.controls, inner3: { - ...INITIAL_STATE_FULL.controls.inner3, + ...inner3State, isDirty: true, isPristine: false, + controls: { + ...inner3State.controls, + inner4: { + ...inner3State.controls.inner4, + isDirty: true, + isPristine: false, + }, + }, }, }, }; @@ -153,8 +235,35 @@ describe('form group markAsPristineReducer', () => { expect(resultState.isPristine).toEqual(true); }); - it('should mark state as pristine if all children are pristine when nested child is updated', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + it('should mark state as pristine if all children are pristine when direct array child is updated', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isDirty: true, + isPristine: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isDirty: true, + isPristine: false, + controls: [ + { + ...inner5State.controls[0], + isDirty: true, + isPristine: false, + }, + ], + }, + }, + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER5_ID)); + expect(resultState.isDirty).toEqual(false); + expect(resultState.isPristine).toEqual(true); + }); + + it('should mark state as pristine if all children are pristine when nested child in group is updated', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isDirty: true, @@ -181,6 +290,33 @@ describe('form group markAsPristineReducer', () => { expect(resultState.isPristine).toEqual(true); }); + it('should mark state as pristine if all children are pristine when nested child in array is updated', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isDirty: true, + isPristine: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isDirty: true, + isPristine: false, + controls: [ + { + ...inner5State.controls[0], + isDirty: true, + isPristine: false, + }, + ], + }, + }, + }; + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER5_0_ID)); + expect(resultState.isDirty).toEqual(false); + expect(resultState.isPristine).toEqual(true); + }); + it('should forward actions to children', () => { const state = { ...INITIAL_STATE_FULL, diff --git a/src/group/reducer/mark-as-submitted.spec.ts b/src/group/reducer/mark-as-submitted.spec.ts index 937f6898..740d8b46 100644 --- a/src/group/reducer/mark-as-submitted.spec.ts +++ b/src/group/reducer/mark-as-submitted.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { MarkAsSubmittedAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { markAsSubmittedReducer } from './mark-as-submitted'; describe('form group markAsSubmittedReducer', () => { @@ -7,12 +7,15 @@ describe('form group markAsSubmittedReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - const INITIAL_STATE_FULL_INNER3 = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + const INITIAL_STATE_FULL_INNER3 = cast(INITIAL_STATE_FULL.controls.inner3)!; + const INITIAL_STATE_FULL_INNER5 = cast(INITIAL_STATE_FULL.controls.inner5)!; const INITIAL_STATE_FULL_SUBMITTED = { ...INITIAL_STATE_FULL, isSubmitted: true, @@ -42,6 +45,18 @@ describe('form group markAsSubmittedReducer', () => { }, }, }, + inner5: { + ...INITIAL_STATE_FULL_INNER5, + isSubmitted: true, + isUnsubmitted: false, + controls: [ + { + ...INITIAL_STATE_FULL_INNER5.controls[0], + isSubmitted: true, + isUnsubmitted: false, + }, + ], + }, }, }; @@ -85,10 +100,22 @@ describe('form group markAsSubmittedReducer', () => { expect(resultState.controls.inner3.isUnsubmitted).toEqual(false); }); - it('should mark nested children as submitted', () => { + it('should mark direct array children as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls.inner5.isSubmitted).toEqual(true); + expect(resultState.controls.inner5.isUnsubmitted).toEqual(false); + }); + + it('should mark nested children in group as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(cast(resultState.controls.inner3)!.controls.inner4.isSubmitted).toBe(true); + expect(cast(resultState.controls.inner3)!.controls.inner4.isUnsubmitted).toBe(false); + }); + + it('should mark nested children in array as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isSubmitted).toBe(true); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isUnsubmitted).toBe(false); + expect(cast(resultState.controls.inner5)!.controls[0].isSubmitted).toBe(true); + expect(cast(resultState.controls.inner5)!.controls[0].isUnsubmitted).toBe(false); }); it('should mark state as submitted if direct control child is marked as submitted', () => { @@ -103,12 +130,24 @@ describe('form group markAsSubmittedReducer', () => { expect(resultState.isUnsubmitted).toEqual(false); }); - it('should mark state as submitted if nested child is marked as submitted', () => { + it('should mark state as submitted if direct array child is marked as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_INNER5_ID)); + expect(resultState.isSubmitted).toEqual(true); + expect(resultState.isUnsubmitted).toEqual(false); + }); + + it('should mark state as submitted if nested child in group is marked as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_INNER4_ID)); expect(resultState.isSubmitted).toEqual(true); expect(resultState.isUnsubmitted).toEqual(false); }); + it('should mark state as submitted if nested child in array is marked as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_INNER5_0_ID)); + expect(resultState.isSubmitted).toEqual(true); + expect(resultState.isUnsubmitted).toEqual(false); + }); + it('should forward actions to children', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_INNER_ID)); expect(resultState.controls.inner.isSubmitted).toEqual(true); diff --git a/src/group/reducer/mark-as-touched.spec.ts b/src/group/reducer/mark-as-touched.spec.ts index 5c20809c..20831fe6 100644 --- a/src/group/reducer/mark-as-touched.spec.ts +++ b/src/group/reducer/mark-as-touched.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { MarkAsTouchedAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { markAsTouchedReducer } from './mark-as-touched'; describe('form group markAsTouchedReducer', () => { @@ -7,12 +7,15 @@ describe('form group markAsTouchedReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - const INITIAL_STATE_FULL_INNER3 = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + const INITIAL_STATE_FULL_INNER3 = cast(INITIAL_STATE_FULL.controls.inner3)!; + const INITIAL_STATE_FULL_INNER5 = cast(INITIAL_STATE_FULL.controls.inner5)!; const INITIAL_STATE_FULL_TOUCHED = { ...INITIAL_STATE_FULL, isTouched: true, @@ -42,6 +45,18 @@ describe('form group markAsTouchedReducer', () => { }, }, }, + inner5: { + ...INITIAL_STATE_FULL_INNER5, + isTouched: true, + isUntouched: false, + controls: [ + { + ...INITIAL_STATE_FULL_INNER5.controls[0], + isTouched: true, + isUntouched: false, + }, + ], + }, }, }; @@ -85,10 +100,22 @@ describe('form group markAsTouchedReducer', () => { expect(resultState.controls.inner3.isUntouched).toEqual(false); }); - it('should mark nested children as touched', () => { + it('should mark direct array children as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls.inner5.isTouched).toEqual(true); + expect(resultState.controls.inner5.isUntouched).toEqual(false); + }); + + it('should mark nested children in group as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(cast(resultState.controls.inner3)!.controls.inner4.isTouched).toBe(true); + expect(cast(resultState.controls.inner3)!.controls.inner4.isUntouched).toBe(false); + }); + + it('should mark nested children in array as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isTouched).toBe(true); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isUntouched).toBe(false); + expect(cast(resultState.controls.inner5)!.controls[0].isTouched).toBe(true); + expect(cast(resultState.controls.inner5)!.controls[0].isUntouched).toBe(false); }); it('should mark state as touched if direct control child is marked as touched', () => { @@ -103,12 +130,24 @@ describe('form group markAsTouchedReducer', () => { expect(resultState.isUntouched).toEqual(false); }); - it('should mark state as touched if nested child is marked as touched', () => { + it('should mark state as touched if direct array child is marked as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_INNER5_ID)); + expect(resultState.isTouched).toEqual(true); + expect(resultState.isUntouched).toEqual(false); + }); + + it('should mark state as touched if nested child in group is marked as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_INNER4_ID)); expect(resultState.isTouched).toEqual(true); expect(resultState.isUntouched).toEqual(false); }); + it('should mark state as touched if nested child in array is marked as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_INNER5_0_ID)); + expect(resultState.isTouched).toEqual(true); + expect(resultState.isUntouched).toEqual(false); + }); + it('should forward actions to children', () => { const resultState = markAsTouchedReducer(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_INNER_ID)); expect(resultState.controls.inner.isTouched).toEqual(true); diff --git a/src/group/reducer/mark-as-unsubmitted.spec.ts b/src/group/reducer/mark-as-unsubmitted.spec.ts index ef404406..02e797bd 100644 --- a/src/group/reducer/mark-as-unsubmitted.spec.ts +++ b/src/group/reducer/mark-as-unsubmitted.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { MarkAsUnsubmittedAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { markAsUnsubmittedReducer } from './mark-as-unsubmitted'; describe('form group markAsUnsubmittedReducer', () => { @@ -7,9 +7,11 @@ describe('form group markAsUnsubmittedReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); @@ -45,6 +47,7 @@ describe('form group markAsUnsubmittedReducer', () => { }); it('should mark direct group children as unsubmitted', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isSubmitted: true, @@ -52,9 +55,16 @@ describe('form group markAsUnsubmittedReducer', () => { controls: { ...INITIAL_STATE_FULL.controls, inner3: { - ...INITIAL_STATE_FULL.controls.inner3, + ...inner3State, isSubmitted: true, isUnsubmitted: false, + controls: { + inner4: { + ...inner3State.controls.inner4, + isSubmitted: true, + isUnsubmitted: false, + }, + }, }, }, }; @@ -63,8 +73,35 @@ describe('form group markAsUnsubmittedReducer', () => { expect(resultState.controls.inner3.isUnsubmitted).toEqual(true); }); - it('should mark nested children as unsubmitted', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + it('should mark direct array children as unsubmitted', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isSubmitted: true, + isUnsubmitted: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isSubmitted: true, + isUnsubmitted: false, + controls: [ + { + ...inner5State.controls[0], + isSubmitted: true, + isUnsubmitted: false, + }, + ], + }, + }, + }; + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls.inner3.isSubmitted).toEqual(false); + expect(resultState.controls.inner3.isUnsubmitted).toEqual(true); + }); + + it('should mark nested children in group as unsubmitted', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isSubmitted: true, @@ -87,8 +124,35 @@ describe('form group markAsUnsubmittedReducer', () => { }, }; const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isSubmitted).toBe(false); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isUnsubmitted).toBe(true); + expect(cast(resultState.controls.inner3)!.controls.inner4.isSubmitted).toBe(false); + expect(cast(resultState.controls.inner3)!.controls.inner4.isUnsubmitted).toBe(true); + }); + + it('should mark nested children in array as unsubmitted', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isSubmitted: true, + isUnsubmitted: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isSubmitted: true, + isUnsubmitted: false, + controls: [ + { + ...inner5State.controls[0], + isSubmitted: true, + isUnsubmitted: false, + }, + ], + }, + }, + }; + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(cast(resultState.controls.inner5)!.controls[0].isSubmitted).toBe(false); + expect(cast(resultState.controls.inner5)!.controls[0].isUnsubmitted).toBe(true); }); it('should mark state as unsubmitted if all children are unsubmitted when direct control child is updated', () => { @@ -111,7 +175,7 @@ describe('form group markAsUnsubmittedReducer', () => { }); it('should not mark state as unsubmitted if not all children are unsubmitted when direct control child is updated', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isSubmitted: true, @@ -144,6 +208,7 @@ describe('form group markAsUnsubmittedReducer', () => { }); it('should mark state as unsubmitted if all children are unsubmitted when direct group child is updated', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isSubmitted: true, @@ -151,9 +216,16 @@ describe('form group markAsUnsubmittedReducer', () => { controls: { ...INITIAL_STATE_FULL.controls, inner3: { - ...INITIAL_STATE_FULL.controls.inner3, + ...inner3State, isSubmitted: true, isUnsubmitted: false, + controls: { + inner4: { + ...inner3State.controls.inner4, + isSubmitted: true, + isUnsubmitted: false, + }, + }, }, }, }; @@ -162,8 +234,35 @@ describe('form group markAsUnsubmittedReducer', () => { expect(resultState.isUnsubmitted).toEqual(true); }); - it('should mark state as unsubmitted if all children are unsubmitted when nested child is updated', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + it('should mark state as unsubmitted if all children are unsubmitted when direct array child is updated', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isSubmitted: true, + isUnsubmitted: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isSubmitted: true, + isUnsubmitted: false, + controls: [ + { + ...inner5State.controls[0], + isSubmitted: true, + isUnsubmitted: false, + }, + ], + }, + }, + }; + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER5_ID)); + expect(resultState.isSubmitted).toEqual(false); + expect(resultState.isUnsubmitted).toEqual(true); + }); + + it('should mark state as unsubmitted if all children are unsubmitted when nested child in group is updated', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isSubmitted: true, @@ -190,6 +289,33 @@ describe('form group markAsUnsubmittedReducer', () => { expect(resultState.isUnsubmitted).toEqual(true); }); + it('should mark state as unsubmitted if all children are unsubmitted when nested child in array is updated', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isSubmitted: true, + isUnsubmitted: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isSubmitted: true, + isUnsubmitted: false, + controls: [ + { + ...inner5State.controls[0], + isSubmitted: true, + isUnsubmitted: false, + }, + ], + }, + }, + }; + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER5_0_ID)); + expect(resultState.isSubmitted).toEqual(false); + expect(resultState.isUnsubmitted).toEqual(true); + }); + it('should forward actions to children', () => { const state = { ...INITIAL_STATE_FULL, diff --git a/src/group/reducer/mark-as-untouched.spec.ts b/src/group/reducer/mark-as-untouched.spec.ts index a1589add..82aed083 100644 --- a/src/group/reducer/mark-as-untouched.spec.ts +++ b/src/group/reducer/mark-as-untouched.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { MarkAsUntouchedAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { markAsUntouchedReducer } from './mark-as-untouched'; describe('form group markAsUntouchedReducer', () => { @@ -7,9 +7,11 @@ describe('form group markAsUntouchedReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); @@ -45,6 +47,7 @@ describe('form group markAsUntouchedReducer', () => { }); it('should mark direct group children as untouched', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isTouched: true, @@ -52,9 +55,16 @@ describe('form group markAsUntouchedReducer', () => { controls: { ...INITIAL_STATE_FULL.controls, inner3: { - ...INITIAL_STATE_FULL.controls.inner3, + ...inner3State, isTouched: true, isUntouched: false, + controls: { + inner4: { + ...inner3State.controls.inner4, + isTouched: true, + isUntouched: false, + }, + }, }, }, }; @@ -63,8 +73,35 @@ describe('form group markAsUntouchedReducer', () => { expect(resultState.controls.inner3.isUntouched).toEqual(true); }); - it('should mark nested children as untouched', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + it('should mark direct array children as untouched', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isTouched: true, + isUntouched: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isTouched: true, + isUntouched: false, + controls: [ + { + ...inner5State.controls[0], + isTouched: true, + isUntouched: false, + }, + ], + }, + }, + }; + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls.inner5.isTouched).toEqual(false); + expect(resultState.controls.inner5.isUntouched).toEqual(true); + }); + + it('should mark nested children in group as untouched', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isTouched: true, @@ -87,8 +124,35 @@ describe('form group markAsUntouchedReducer', () => { }, }; const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isTouched).toBe(false); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isUntouched).toBe(true); + expect(cast(resultState.controls.inner3)!.controls.inner4.isTouched).toBe(false); + expect(cast(resultState.controls.inner3)!.controls.inner4.isUntouched).toBe(true); + }); + + it('should mark nested children in array as untouched', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isTouched: true, + isUntouched: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isTouched: true, + isUntouched: false, + controls: [ + { + ...inner5State.controls[0], + isTouched: true, + isUntouched: false, + }, + ], + }, + }, + }; + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(cast(resultState.controls.inner5)!.controls[0].isTouched).toBe(false); + expect(cast(resultState.controls.inner5)!.controls[0].isUntouched).toBe(true); }); it('should mark state as untouched if all children are untouched when direct control child is updated', () => { @@ -111,7 +175,7 @@ describe('form group markAsUntouchedReducer', () => { }); it('should not mark state as untouched if not all children are untouched when direct control child is updated', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isTouched: true, @@ -144,6 +208,7 @@ describe('form group markAsUntouchedReducer', () => { }); it('should mark state as untouched if all children are untouched when direct group child is updated', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isTouched: true, @@ -151,9 +216,17 @@ describe('form group markAsUntouchedReducer', () => { controls: { ...INITIAL_STATE_FULL.controls, inner3: { - ...INITIAL_STATE_FULL.controls.inner3, + ...inner3State, isTouched: true, isUntouched: false, + controls: { + ...inner3State.controls, + inner4: { + ...inner3State.controls.inner4, + isTouched: true, + isUntouched: false, + }, + }, }, }, }; @@ -162,8 +235,35 @@ describe('form group markAsUntouchedReducer', () => { expect(resultState.isUntouched).toEqual(true); }); - it('should mark state as untouched if all children are untouched when nested child is updated', () => { - const inner3State = INITIAL_STATE_FULL.controls.inner3 as FormGroupState; + it('should mark state as untouched if all children are untouched when direct array child is updated', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isTouched: true, + isUntouched: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isTouched: true, + isUntouched: false, + controls: [ + { + ...inner5State.controls[0], + isTouched: true, + isUntouched: false, + }, + ], + }, + }, + }; + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER5_ID)); + expect(resultState.isTouched).toEqual(false); + expect(resultState.isUntouched).toEqual(true); + }); + + it('should mark state as untouched if all children are untouched when nested child in group is updated', () => { + const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; const state = { ...INITIAL_STATE_FULL, isTouched: true, @@ -190,6 +290,33 @@ describe('form group markAsUntouchedReducer', () => { expect(resultState.isUntouched).toEqual(true); }); + it('should mark state as untouched if all children are untouched when nested child in array is updated', () => { + const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; + const state = { + ...INITIAL_STATE_FULL, + isTouched: true, + isUntouched: false, + controls: { + ...INITIAL_STATE_FULL.controls, + inner5: { + ...inner5State, + isTouched: true, + isUntouched: false, + controls: [ + { + ...inner5State.controls[0], + isTouched: true, + isUntouched: false, + }, + ], + }, + }, + }; + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER5_0_ID)); + expect(resultState.isTouched).toEqual(false); + expect(resultState.isUntouched).toEqual(true); + }); + it('should forward actions to children', () => { const state = { ...INITIAL_STATE_FULL, diff --git a/src/group/reducer/remove-control.spec.ts b/src/group/reducer/remove-control.spec.ts index 40532ec4..f2fbde16 100644 --- a/src/group/reducer/remove-control.spec.ts +++ b/src/group/reducer/remove-control.spec.ts @@ -4,26 +4,82 @@ import { removeControlReducer } from './remove-control'; describe('form group removeControlReducer', () => { const FORM_CONTROL_ID = 'test ID'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); it('should remove child state', () => { const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner2'); const resultState = removeControlReducer(INITIAL_STATE_FULL, action); - expect(resultState.value).toEqual({ inner: '', inner3: { inner4: '' } }); + expect(resultState.value).toEqual({ inner: '', inner3: { inner4: '' }, inner5: [''] }); expect(resultState.controls.inner2).toBeUndefined(); }); it('should remove child state for group children', () => { const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner3'); const resultState = removeControlReducer(INITIAL_STATE_FULL, action); - expect(resultState.value).toEqual({ inner: '', inner2: '' }); + expect(resultState.value).toEqual({ inner: '', inner2: '', inner5: [''] }); expect(resultState.controls.inner3).toBeUndefined(); }); + it('should remove child state for array children', () => { + const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner5'); + const resultState = removeControlReducer(INITIAL_STATE_FULL, action); + expect(resultState.value).toEqual({ inner: '', inner2: '', inner3: { inner4: '' } }); + expect(resultState.controls.inner5).toBeUndefined(); + }); + + it('should remove child errors for removed child', () => { + interface FormValue { inner?: number; } + const id = 'ID'; + const errors = { required: true }; + let state = createFormGroupState(id, { inner: 5 }); + state = { + ...state, + errors: { + _inner: errors, + }, + controls: { + inner: { + ...state.controls.inner, + errors, + }, + }, + }; + const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner'); + const resultState = removeControlReducer(state, action); + expect(resultState.value).toEqual({}); + expect(resultState.errors).toEqual({}); + expect(resultState.controls.inner).toBeUndefined(); + }); + + it('should remove child errors for removed child and keep own errors', () => { + interface FormValue { inner?: number; } + const id = 'ID'; + const errors = { required: true }; + let state = createFormGroupState(id, { inner: 5 }); + state = { + ...state, + errors: { + _inner: errors, + ...errors, + }, + controls: { + inner: { + ...state.controls.inner, + errors, + }, + }, + }; + const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner'); + const resultState = removeControlReducer(state, action); + expect(resultState.value).toEqual({}); + expect(resultState.errors).toEqual(errors); + expect(resultState.controls.inner).toBeUndefined(); + }); + it('should throw if trying to remove non-existing control', () => { const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner2'); expect(() => removeControlReducer(INITIAL_STATE, action)).toThrowError(); diff --git a/src/group/reducer/set-errors.spec.ts b/src/group/reducer/set-errors.spec.ts index 2f4bdca6..fa187274 100644 --- a/src/group/reducer/set-errors.spec.ts +++ b/src/group/reducer/set-errors.spec.ts @@ -7,9 +7,11 @@ describe('form group setErrorsReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' } }; + const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); @@ -69,7 +71,15 @@ describe('form group setErrorsReducer', () => { expect(resultState.isInvalid).toEqual(true); }); - it('should aggregate nested child errors', () => { + it('should aggregate child errors for array children', () => { + const errors = { required: true }; + const resultState = setErrorsReducer(INITIAL_STATE_FULL, new SetErrorsAction(FORM_CONTROL_INNER5_ID, errors)); + expect(resultState.errors).toEqual({ _inner5: errors }); + expect(resultState.isValid).toEqual(false); + expect(resultState.isInvalid).toEqual(true); + }); + + it('should aggregate nested child errors for group', () => { const errors = { required: true }; const resultState = setErrorsReducer(INITIAL_STATE_FULL, new SetErrorsAction(FORM_CONTROL_INNER4_ID, errors)); expect(resultState.errors).toEqual({ _inner3: { _inner4: errors } }); @@ -77,6 +87,14 @@ describe('form group setErrorsReducer', () => { expect(resultState.isInvalid).toEqual(true); }); + it('should aggregate nested child errors for array', () => { + const errors = { required: true }; + const resultState = setErrorsReducer(INITIAL_STATE_FULL, new SetErrorsAction(FORM_CONTROL_INNER5_0_ID, errors)); + expect(resultState.errors).toEqual({ _inner5: { _0: errors } }); + expect(resultState.isValid).toEqual(false); + expect(resultState.isInvalid).toEqual(true); + }); + it('should aggregate multiple child errors', () => { const errors1 = { required: true }; const errors2 = { min: 0 }; diff --git a/src/group/reducer/set-value.spec.ts b/src/group/reducer/set-value.spec.ts index 97c7f77e..1b3f7907 100644 --- a/src/group/reducer/set-value.spec.ts +++ b/src/group/reducer/set-value.spec.ts @@ -1,5 +1,5 @@ -import { FormGroupState, createFormGroupState } from '../../state'; import { SetValueAction } from '../../actions'; +import { cast, createFormGroupState } from '../../state'; import { setValueReducer } from './set-value'; describe('form group setValueReducer', () => { @@ -7,7 +7,9 @@ describe('form group setValueReducer', () => { const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } + const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; + const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; + interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); @@ -50,6 +52,13 @@ describe('form group setValueReducer', () => { expect(resultState.controls.inner3.value).toEqual(value.inner3); }); + it('should create child states on demand for array children', () => { + const value = { inner: 'A', inner5: ['C'] }; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + expect(resultState.controls.inner5.value).toEqual(value.inner5); + }); + it('should create child states on demand for null children', () => { const value = { inner: 'A', inner2: null as any as string }; const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); @@ -98,6 +107,16 @@ describe('form group setValueReducer', () => { expect(resultState.value.inner3).toEqual(value); }); + it('should aggregate child values for array children', () => { + let resultState = setValueReducer( + INITIAL_STATE, + new SetValueAction(FORM_CONTROL_ID, { inner: 'A', inner5: ['C'] }), + ); + const value = ['D']; + resultState = setValueReducer(resultState, new SetValueAction(FORM_CONTROL_INNER5_ID, value) as any); + expect(resultState.value.inner5).toEqual(value); + }); + it('should mark state as dirty if group child value is updated', () => { let resultState = setValueReducer( INITIAL_STATE, @@ -109,7 +128,18 @@ describe('form group setValueReducer', () => { expect(resultState.controls.inner3.isDirty).toEqual(true); }); - it('should aggregate nested child values', () => { + it('should mark state as dirty if array child value is updated', () => { + let resultState = setValueReducer( + INITIAL_STATE, + new SetValueAction(FORM_CONTROL_ID, { inner: 'A', inner5: ['C'] }), + ); + const value = ['D']; + resultState = setValueReducer(resultState, new SetValueAction(FORM_CONTROL_INNER5_ID, value) as any); + expect(resultState.isDirty).toEqual(true); + expect(resultState.controls.inner5.isDirty).toEqual(true); + }); + + it('should aggregate nested child values in groups', () => { let resultState = setValueReducer( INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, { inner: 'A', inner3: { inner4: 'C' } }), @@ -119,7 +149,17 @@ describe('form group setValueReducer', () => { expect(resultState.value.inner3!.inner4).toEqual(value); }); - it('should mark state as dirty if nested child value is updated', () => { + it('should aggregate nested child values in arrays', () => { + let resultState = setValueReducer( + INITIAL_STATE, + new SetValueAction(FORM_CONTROL_ID, { inner: 'A', inner5: ['C'] }), + ); + const value = 'D'; + resultState = setValueReducer(resultState, new SetValueAction(FORM_CONTROL_INNER5_0_ID, value) as any); + expect(resultState.value.inner5![0]).toEqual(value); + }); + + it('should mark state as dirty if nested child value in group is updated', () => { let resultState = setValueReducer( INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, { inner: 'A', inner3: { inner4: 'C' } }), @@ -127,7 +167,18 @@ describe('form group setValueReducer', () => { const value = 'D'; resultState = setValueReducer(resultState, new SetValueAction(FORM_CONTROL_INNER4_ID, value) as any); expect(resultState.isDirty).toEqual(true); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isDirty).toEqual(true); + expect(cast(resultState.controls.inner3)!.controls.inner4.isDirty).toEqual(true); + }); + + it('should mark state as dirty if nested child value in array is updated', () => { + let resultState = setValueReducer( + INITIAL_STATE, + new SetValueAction(FORM_CONTROL_ID, { inner: 'A', inner5: ['C'] }), + ); + const value = 'D'; + resultState = setValueReducer(resultState, new SetValueAction(FORM_CONTROL_INNER5_0_ID, value) as any); + expect(resultState.isDirty).toEqual(true); + expect(cast(resultState.controls.inner5)!.controls[0].isDirty).toEqual(true); }); it('should remove child errors on demand when value is empty', () => { @@ -137,6 +188,9 @@ describe('form group setValueReducer', () => { let state = createFormGroupState(id, { inner: 5 }); state = { ...state, + errors: { + _inner: errors, + }, controls: { inner: { ...state.controls.inner, @@ -157,7 +211,10 @@ describe('form group setValueReducer', () => { let state = createFormGroupState(id, { inner: 5 }); state = { ...state, - errors, + errors: { + _inner: errors, + ...errors, + }, controls: { inner: { ...state.controls.inner, diff --git a/src/group/reducer/set-value.ts b/src/group/reducer/set-value.ts index cadcd48d..6a5cc7a4 100644 --- a/src/group/reducer/set-value.ts +++ b/src/group/reducer/set-value.ts @@ -1,6 +1,6 @@ -import { FormGroupState, FormGroupControls, KeyValue } from '../../state'; import { Actions, SetValueAction } from '../../actions'; -import { computeGroupState, callChildReducer, childReducer, createChildState } from './util'; +import { createChildState, FormGroupControls, FormGroupState, KeyValue } from '../../state'; +import { callChildReducer, childReducer, computeGroupState } from './util'; export function setValueReducer( state: FormGroupState, diff --git a/src/group/reducer/util.ts b/src/group/reducer/util.ts index ec4ecd75..d004633d 100644 --- a/src/group/reducer/util.ts +++ b/src/group/reducer/util.ts @@ -1,15 +1,8 @@ import { Actions } from '../../actions'; -import { - AbstractControlState, - FormGroupState, - FormGroupControls, - KeyValue, - ValidationErrors, - createFormControlState, - createFormGroupState, -} from '../../state'; -import { isEmpty } from '../../util'; +import { formArrayReducerInternal } from '../../array/reducer'; import { formControlReducerInternal } from '../../control/reducer'; +import { AbstractControlState, FormGroupControls, FormGroupState, KeyValue, ValidationErrors } from '../../state'; +import { isEmpty } from '../../util'; import { formGroupReducerInternal } from '../reducer'; export function getFormGroupValue( @@ -36,7 +29,7 @@ export function getFormGroupErrors( .filter(key => !key.startsWith('_')) .reduce((res, key) => Object.assign(res, { [key]: originalErrors[key] }), {}); - const newErrors = Object.keys(controls).reduce((res, key) => { + const newErrors = Object.keys(controls).reduce((res, key: any) => { const controlErrors = controls[key].errors; hasChanged = hasChanged || originalErrors['_' + key] !== controlErrors; if (!isEmpty(controlErrors)) { @@ -83,6 +76,10 @@ export function computeGroupState( }; } +export function isArrayState(state: AbstractControlState): boolean { + return state.hasOwnProperty('controls') && Array.isArray((state as any).controls); +} + export function isGroupState(state: AbstractControlState): boolean { return state.hasOwnProperty('controls'); } @@ -91,6 +88,10 @@ export function callChildReducer( state: AbstractControlState, action: Actions, ): AbstractControlState { + if (isArrayState(state)) { + return formArrayReducerInternal(state as any, action as any); + } + if (isGroupState(state)) { return formGroupReducerInternal(state as any, action); } @@ -135,11 +136,3 @@ export function childReducer(state: FormGroupState { - if (childValue !== null && typeof childValue === 'object') { - return createFormGroupState(id, childValue); - } - - return createFormControlState(id, childValue); -} diff --git a/src/state.spec.ts b/src/state.spec.ts index a71345f3..c467c64d 100644 --- a/src/state.spec.ts +++ b/src/state.spec.ts @@ -134,6 +134,10 @@ describe('state', () => { expect(INITIAL_STATE.value).toBe(INITIAL_VALUE); }); + it('should set the correct value for empty arrays', () => { + expect(createFormArrayState(FORM_CONTROL_ID, []).value).toEqual([]); + }); + it('should mark control as valid', () => { expect(INITIAL_STATE.isValid).toBe(true); expect(INITIAL_STATE.isInvalid).toBe(false); @@ -202,5 +206,11 @@ describe('state', () => { expect(controls2).toBeDefined(); expect(Array.isArray(controls2)).toBe(false); }); + + it('should create empty children array for empty value array', () => { + const initialValue = [] as string[]; + const initialState = createFormArrayState(FORM_CONTROL_ID, initialValue); + expect(initialState.controls).toEqual([]); + }); }); }); diff --git a/src/state.ts b/src/state.ts index 4d8ebdd2..50406f33 100644 --- a/src/state.ts +++ b/src/state.ts @@ -38,18 +38,36 @@ export class FormArrayState extends AbstractControlState { export function cast( state: AbstractControlState, ): FormControlState; -export function cast( - state: AbstractControlState, +export function cast( + state: AbstractControlState, ): FormArrayState; +export function cast( + state: AbstractControlState, +): FormArrayState | undefined; export function cast( state: AbstractControlState, ): FormGroupState; +export function cast( + state: AbstractControlState, +): FormGroupState | undefined; export function cast( state: AbstractControlState, ) { return state as any; } +export function createChildState(id: string, childValue: any): AbstractControlState { + if (childValue !== null && Array.isArray(childValue)) { + return createFormArrayState(id, childValue); + } + + if (childValue !== null && typeof childValue === 'object') { + return createFormGroupState(id, childValue); + } + + return createFormControlState(id, childValue); +} + export function createFormControlState( id: NgrxFormControlId, value: TValue, @@ -78,20 +96,8 @@ export function createFormGroupState( id: NgrxFormControlId, initialValue: TValue, ): FormGroupState { - function createState(key: string, value: any) { - if (value !== null && Array.isArray(value)) { - return createFormArrayState(`${id}.${key}`, value); - } - - if (value !== null && typeof value === 'object') { - return createFormGroupState(`${id}.${key}`, value); - } - - return createFormControlState(`${id}.${key}`, value); - } - const controls = Object.keys(initialValue) - .map((key: keyof TValue) => [key, createState(key, initialValue[key])] as [string, AbstractControlState]) + .map((key: keyof TValue) => [key, createChildState(`${id}.${key}`, initialValue[key])] as [string, AbstractControlState]) .reduce((res, [controlId, state]) => Object.assign(res, { [controlId]: state }), {} as FormGroupControls); return { @@ -117,20 +123,8 @@ export function createFormArrayState( id: NgrxFormControlId, initialValue: TValue[], ): FormArrayState { - function createState(index: number, value: any) { - if (value !== null && Array.isArray(value)) { - return createFormArrayState(`${id}.${index}`, value); - } - - if (value !== null && typeof value === 'object') { - return createFormGroupState(`${id}.${index}`, value); - } - - return createFormControlState(`${id}.${index}`, value); - } - const controls = initialValue - .map((value, i) => createState(i, value) as AbstractControlState); + .map((value, i) => createChildState(`${id}.${i}`, value) as AbstractControlState); return { id, From a738e0010d4d3f64d1106863b9dd013d08eb0051 Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sat, 28 Oct 2017 21:47:16 +0200 Subject: [PATCH 03/11] feat: add all remaining form array reducers + refactor tests to have less duplication --- src/array/reducer.spec.ts | 2 +- src/array/reducer.ts | 14 + src/array/reducer/disable.spec.ts | 58 ++- src/array/reducer/enable.spec.ts | 134 ++----- src/array/reducer/mark-as-dirty.spec.ts | 51 +-- src/array/reducer/mark-as-pristine.spec.ts | 182 ++-------- src/array/reducer/mark-as-submitted.spec.ts | 79 +++++ src/array/reducer/mark-as-submitted.ts | 30 ++ src/array/reducer/mark-as-touched.spec.ts | 79 +++++ src/array/reducer/mark-as-touched.ts | 30 ++ src/array/reducer/mark-as-unsubmitted.spec.ts | 75 ++++ src/array/reducer/mark-as-unsubmitted.ts | 28 ++ src/array/reducer/mark-as-untouched.spec.ts | 75 ++++ src/array/reducer/mark-as-untouched.ts | 28 ++ src/array/reducer/set-errors.spec.ts | 122 +++++++ src/array/reducer/set-errors.ts | 46 +++ .../reducer/set-user-defined-property.spec.ts | 25 ++ .../reducer/set-user-defined-property.ts | 27 ++ src/array/reducer/set-value.spec.ts | 160 +++++++++ src/array/reducer/set-value.ts | 37 ++ src/array/reducer/test-util.ts | 59 ++++ src/array/reducer/util.ts | 18 +- src/group/reducer/add-control.spec.ts | 16 +- src/group/reducer/disable.spec.ts | 197 ++--------- src/group/reducer/enable.spec.ts | 227 ++---------- src/group/reducer/mark-as-dirty.spec.ts | 106 +----- src/group/reducer/mark-as-pristine.spec.ts | 330 +++--------------- src/group/reducer/mark-as-submitted.spec.ts | 112 +----- src/group/reducer/mark-as-touched.spec.ts | 112 +----- src/group/reducer/mark-as-unsubmitted.spec.ts | 329 +++-------------- src/group/reducer/mark-as-untouched.spec.ts | 328 +++-------------- src/group/reducer/remove-control.spec.ts | 16 +- src/group/reducer/set-errors.spec.ts | 25 +- .../reducer/set-user-defined-property.spec.ts | 7 +- src/group/reducer/set-value.spec.ts | 24 +- src/group/reducer/test-util.ts | 71 ++++ src/group/reducer/util.ts | 18 +- src/state.ts | 8 + 38 files changed, 1361 insertions(+), 1924 deletions(-) create mode 100644 src/array/reducer/mark-as-submitted.spec.ts create mode 100644 src/array/reducer/mark-as-submitted.ts create mode 100644 src/array/reducer/mark-as-touched.spec.ts create mode 100644 src/array/reducer/mark-as-touched.ts create mode 100644 src/array/reducer/mark-as-unsubmitted.spec.ts create mode 100644 src/array/reducer/mark-as-unsubmitted.ts create mode 100644 src/array/reducer/mark-as-untouched.spec.ts create mode 100644 src/array/reducer/mark-as-untouched.ts create mode 100644 src/array/reducer/set-errors.spec.ts create mode 100644 src/array/reducer/set-errors.ts create mode 100644 src/array/reducer/set-user-defined-property.spec.ts create mode 100644 src/array/reducer/set-user-defined-property.ts create mode 100644 src/array/reducer/set-value.spec.ts create mode 100644 src/array/reducer/set-value.ts create mode 100644 src/array/reducer/test-util.ts create mode 100644 src/group/reducer/test-util.ts diff --git a/src/array/reducer.spec.ts b/src/array/reducer.spec.ts index 053c1ec9..1cb1ee0e 100644 --- a/src/array/reducer.spec.ts +++ b/src/array/reducer.spec.ts @@ -16,7 +16,7 @@ import { import { cast, createFormArrayState } from '../state'; import { formArrayReducerInternal } from './reducer'; -describe('form group reducer', () => { +describe('form array reducer', () => { const FORM_CONTROL_ID = 'test ID'; const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; type FormArrayValue = string[]; diff --git a/src/array/reducer.ts b/src/array/reducer.ts index daf06cd4..1b7313e4 100644 --- a/src/array/reducer.ts +++ b/src/array/reducer.ts @@ -6,6 +6,13 @@ import { disableReducer } from './reducer/disable'; import { enableReducer } from './reducer/enable'; import { markAsDirtyReducer } from './reducer/mark-as-dirty'; import { markAsPristineReducer } from './reducer/mark-as-pristine'; +import { markAsSubmittedReducer } from './reducer/mark-as-submitted'; +import { markAsTouchedReducer } from './reducer/mark-as-touched'; +import { markAsUnsubmittedReducer } from './reducer/mark-as-unsubmitted'; +import { markAsUntouchedReducer } from './reducer/mark-as-untouched'; +import { setErrorsReducer } from './reducer/set-errors'; +import { setUserDefinedPropertyReducer } from './reducer/set-user-defined-property'; +import { setValueReducer } from './reducer/set-value'; import { childReducer } from './reducer/util'; export function formArrayReducerInternal(state: FormArrayState, action: Actions) { @@ -15,10 +22,17 @@ export function formArrayReducerInternal(state: FormArrayState, return childReducer(state, action); } + state = setValueReducer(state, action); + state = setErrorsReducer(state, action); state = enableReducer(state, action); state = disableReducer(state, action); state = markAsDirtyReducer(state, action); state = markAsPristineReducer(state, action); + state = markAsTouchedReducer(state, action); + state = markAsUntouchedReducer(state, action); + state = markAsSubmittedReducer(state, action); + state = markAsUnsubmittedReducer(state, action); + state = setUserDefinedPropertyReducer(state, action); return state; } diff --git a/src/array/reducer/disable.spec.ts b/src/array/reducer/disable.spec.ts index 275404d1..afc03ce9 100644 --- a/src/array/reducer/disable.spec.ts +++ b/src/array/reducer/disable.spec.ts @@ -1,18 +1,17 @@ import { DisableAction } from '../../actions'; -import { createFormArrayState } from '../../state'; +import { setPropertiesRecursively } from '../../group/reducer/test-util'; +import { cast } from '../../state'; import { disableReducer } from './disable'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_1_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, +} from './test-util'; -describe('form group disableReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; - const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; - const INITIAL_FORM_ARRAY_VALUE = ['', '']; - const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }]; - const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [['']]; - const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); - const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); - const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); - +describe(`form array ${disableReducer.name}`, () => { it('should update state if enabled', () => { const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_ID)); expect(resultState.isEnabled).toEqual(false); @@ -55,43 +54,32 @@ describe('form group disableReducer', () => { }); it('should disable if all children are disabled when control child is disabled', () => { - const state = { - ...INITIAL_STATE, - controls: [ - INITIAL_STATE.controls[0], - { - ...INITIAL_STATE.controls[1], - isEnabled: false, - isDisabled: true, - }, - ], - }; - const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_0_ID)); + let state = cast(setPropertiesRecursively(INITIAL_STATE, [['isEnabled', false], ['isDisabled', true]], FORM_CONTROL_0_ID)); + state = cast(setPropertiesRecursively(state, [['isEnabled', true], ['isDisabled', false]], FORM_CONTROL_1_ID)); + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_0_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - it('should not disable if not all children are disabled when direct control child is disabled', () => { + it('should not disable if not all children are disabled when control child is disabled', () => { const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_0_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); }); - it('should disable if all children are disabled when direct group child is disabled', () => { - const resultState = disableReducer(INITIAL_STATE_NESTED_GROUP, new DisableAction(FORM_CONTROL_0_ID)); + it('should disable if all children are disabled when group child is disabled', () => { + let state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isEnabled', false], ['isDisabled', true]], FORM_CONTROL_0_ID)); + state = cast(setPropertiesRecursively(state, [['isEnabled', true], ['isDisabled', false]], FORM_CONTROL_1_ID)); + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_0_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - it('should disable if all children are disabled when direct array child is disabled', () => { - const resultState = disableReducer(INITIAL_STATE_NESTED_ARRAY, new DisableAction(FORM_CONTROL_0_ID)); + it('should disable if all children are disabled when array child is disabled', () => { + let state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isEnabled', false], ['isDisabled', true]], FORM_CONTROL_0_ID)); + state = cast(setPropertiesRecursively(state, [['isEnabled', true], ['isDisabled', false]], FORM_CONTROL_1_ID)); + const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_0_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - - it('should forward actions to children', () => { - const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_0_ID)); - expect(resultState.controls[0].isEnabled).toEqual(false); - expect(resultState.controls[0].isDisabled).toEqual(true); - }); }); diff --git a/src/array/reducer/enable.spec.ts b/src/array/reducer/enable.spec.ts index a8e7cbfb..284f9ce9 100644 --- a/src/array/reducer/enable.spec.ts +++ b/src/array/reducer/enable.spec.ts @@ -1,36 +1,19 @@ import { EnableAction } from '../../actions'; -import { createFormArrayState } from '../../state'; +import { cast } from '../../state'; import { enableReducer } from './enable'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_1_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, + setPropertiesRecursively, +} from './test-util'; -describe('form group enableReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; - const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; - const INITIAL_FORM_ARRAY_VALUE = ['', '']; - const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }, { inner2: '' }]; - const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [[''], ['']]; - const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); - const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); - const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); - +describe(`form array ${enableReducer.name}`, () => { it('should enable itself and all children recursively', () => { - const state = { - ...INITIAL_STATE, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...INITIAL_STATE.controls[0], - isEnabled: false, - isDisabled: true, - }, - { - ...INITIAL_STATE.controls[1], - isEnabled: false, - isDisabled: true, - }, - ], - }; + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isEnabled', false], ['isDisabled', true]])); const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_ID)); expect(resultState).toEqual(INITIAL_STATE); }); @@ -56,40 +39,14 @@ describe('form group enableReducer', () => { expect(resultState).toEqual(INITIAL_STATE); }); - it('should enable if direct control child gets enabled', () => { - const state = { - ...INITIAL_STATE, - controls: [ - INITIAL_STATE.controls[0], - { - ...INITIAL_STATE.controls[1], - isEnabled: false, - isDisabled: true, - }, - ], - }; - const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_1_ID)); + it('should enable if control child gets enabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isEnabled', false], ['isDisabled', true]], FORM_CONTROL_1_ID)); + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_0_ID)); expect(resultState).toEqual(INITIAL_STATE); }); - it('should enable without enabling any other children if direct control child gets enabled', () => { - const state = { - ...INITIAL_STATE, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...INITIAL_STATE.controls[0], - isEnabled: false, - isDisabled: true, - }, - { - ...INITIAL_STATE.controls[1], - isEnabled: false, - isDisabled: true, - }, - ], - }; + it('should enable without enabling any other children if control child gets enabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isEnabled', false], ['isDisabled', true]])); const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_0_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); @@ -97,24 +54,8 @@ describe('form group enableReducer', () => { expect(resultState.controls[1].isDisabled).toBe(true); }); - it('should enable without enabling any other children if direct group child gets enabled', () => { - const state = { - ...INITIAL_STATE_NESTED_GROUP, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...INITIAL_STATE_NESTED_GROUP.controls[0], - isEnabled: false, - isDisabled: true, - }, - { - ...INITIAL_STATE_NESTED_GROUP.controls[1], - isEnabled: false, - isDisabled: true, - }, - ], - }; + it('should enable without enabling any other children if group child gets enabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isEnabled', false], ['isDisabled', true]])); const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_0_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); @@ -122,45 +63,12 @@ describe('form group enableReducer', () => { expect(resultState.controls[1].isDisabled).toBe(true); }); - it('should enable without enabling any other children if direct array child gets enabled', () => { - const state = { - ...INITIAL_STATE_NESTED_ARRAY, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...INITIAL_STATE_NESTED_ARRAY.controls[0], - isEnabled: false, - isDisabled: true, - }, - { - ...INITIAL_STATE_NESTED_ARRAY.controls[1], - isEnabled: false, - isDisabled: true, - }, - ], - }; + it('should enable without enabling any other children if array child gets enabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isEnabled', false], ['isDisabled', true]])); const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_0_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); expect(resultState.controls[1].isEnabled).toBe(false); expect(resultState.controls[1].isDisabled).toBe(true); }); - - it('should forward actions to children', () => { - const state = { - ...INITIAL_STATE, - controls: [ - INITIAL_STATE.controls[0], - { - ...INITIAL_STATE.controls[1], - isEnabled: false, - isDisabled: true, - }, - ], - }; - const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_1_ID)); - expect(resultState.controls[1].isEnabled).toEqual(true); - expect(resultState.controls[1].isDisabled).toEqual(false); - }); }); diff --git a/src/array/reducer/mark-as-dirty.spec.ts b/src/array/reducer/mark-as-dirty.spec.ts index feb4c0b7..c06ca428 100644 --- a/src/array/reducer/mark-as-dirty.spec.ts +++ b/src/array/reducer/mark-as-dirty.spec.ts @@ -1,34 +1,17 @@ import { MarkAsDirtyAction } from '../../actions'; -import { createFormArrayState } from '../../state'; +import { cast } from '../../state'; import { markAsDirtyReducer } from './mark-as-dirty'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsDirtyReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; - const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; - const INITIAL_FORM_ARRAY_VALUE = ['', '']; - const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }]; - const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [['']]; - const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); - const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); - const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); - const INITIAL_STATE_DIRTY = { - ...INITIAL_STATE, - isDirty: true, - isPristine: false, - controls: [ - { - ...INITIAL_STATE.controls[0], - isDirty: true, - isPristine: false, - }, - { - ...INITIAL_STATE.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; +describe(`form array ${markAsDirtyReducer.name}`, () => { + const INITIAL_STATE_DIRTY = cast(setPropertiesRecursively(INITIAL_STATE, [['isDirty', true], ['isPristine', false]])); it('should mark itself and all children recursively as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_ID)); @@ -76,27 +59,21 @@ describe('form group markAsDirtyReducer', () => { expect(resultState.controls[0].isPristine).toEqual(false); }); - it('should mark state as dirty if direct control child is marked as dirty', () => { + it('should mark state as dirty if control child is marked as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - it('should mark state as dirty if direct group child is marked as dirty', () => { + it('should mark state as dirty if group child is marked as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_NESTED_GROUP, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - it('should mark state as dirty if direct array child is marked as dirty', () => { + it('should mark state as dirty if array child is marked as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_NESTED_ARRAY, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - - it('should forward actions to children', () => { - const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_0_ID)); - expect(resultState.controls[0].isDirty).toEqual(true); - expect(resultState.controls[0].isPristine).toEqual(false); - }); }); diff --git a/src/array/reducer/mark-as-pristine.spec.ts b/src/array/reducer/mark-as-pristine.spec.ts index 9e39153f..cb48e34a 100644 --- a/src/array/reducer/mark-as-pristine.spec.ts +++ b/src/array/reducer/mark-as-pristine.spec.ts @@ -1,18 +1,17 @@ import { MarkAsPristineAction } from '../../actions'; -import { cast, createFormArrayState } from '../../state'; +import { cast } from '../../state'; import { markAsPristineReducer } from './mark-as-pristine'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_1_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsPristineReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; - const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; - const INITIAL_FORM_ARRAY_VALUE = ['', '']; - const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }, { inner2: '' }]; - const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [[''], ['']]; - const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); - const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); - const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); - +describe(`form array ${markAsPristineReducer.name}`, () => { it('should update state if dirty', () => { const state = { ...INITIAL_STATE, isDirty: true, isPristine: false }; const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); @@ -25,171 +24,52 @@ describe('form group markAsPristineReducer', () => { expect(resultState).toBe(INITIAL_STATE); }); - it('should mark direct control children as pristine', () => { - const state = { - ...INITIAL_STATE, - isDirty: true, - isPristine: false, - controls: [ - INITIAL_STATE.controls[0], - { - ...INITIAL_STATE.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; + it('should mark control children as pristine', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); expect(resultState.controls[0].isDirty).toEqual(false); expect(resultState.controls[0].isPristine).toEqual(true); }); - it('should mark direct group children as pristine', () => { - const state = { - ...INITIAL_STATE_NESTED_GROUP, - isDirty: true, - isPristine: false, - controls: [ - INITIAL_STATE_NESTED_GROUP.controls[0], - { - ...INITIAL_STATE_NESTED_GROUP.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; + it('should mark group children as pristine', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); expect(resultState.controls[0].isDirty).toEqual(false); expect(resultState.controls[0].isPristine).toEqual(true); }); - it('should mark direct array children as pristine', () => { - const state = { - ...INITIAL_STATE_NESTED_ARRAY, - isDirty: true, - isPristine: false, - controls: [ - INITIAL_STATE_NESTED_ARRAY.controls[0], - { - ...INITIAL_STATE_NESTED_ARRAY.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; + it('should mark array children as pristine', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); expect(resultState.controls[0].isDirty).toEqual(false); expect(resultState.controls[0].isPristine).toEqual(true); }); - it('should mark state as pristine if all children are pristine when direct control child is updated', () => { - const state = { - ...INITIAL_STATE, - isDirty: true, - isPristine: false, - controls: [ - INITIAL_STATE.controls[0], - { - ...INITIAL_STATE.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; + it('should mark state as pristine if all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isDirty', true], ['isPristine', false]], FORM_CONTROL_0_ID)); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_1_ID)); expect(resultState.isDirty).toEqual(false); expect(resultState.isPristine).toEqual(true); }); - it('should not mark state as pristine if not all children are pristine when direct control child is updated', () => { - const state = { - ...INITIAL_STATE, - isDirty: true, - isPristine: false, - controls: [ - { - ...INITIAL_STATE.controls[0], - isDirty: true, - isPristine: false, - }, - { - ...INITIAL_STATE.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; + it('should not mark state as pristine if not all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - it('should mark state as pristine if all children are pristine when direct group child is updated', () => { - const state = { - ...INITIAL_STATE_NESTED_GROUP, - isDirty: true, - isPristine: false, - controls: [ - { - ...INITIAL_STATE_NESTED_GROUP.controls[0], - isDirty: true, - isPristine: false, - }, - { - ...INITIAL_STATE_NESTED_GROUP.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); - expect(resultState.isDirty).toEqual(true); - expect(resultState.isPristine).toEqual(false); - }); - - it('should mark state as pristine if all children are pristine when direct array child is updated', () => { - const state = { - ...INITIAL_STATE_NESTED_ARRAY, - isDirty: true, - isPristine: false, - controls: [ - { - ...INITIAL_STATE_NESTED_ARRAY.controls[0], - isDirty: true, - isPristine: false, - }, - { - ...INITIAL_STATE_NESTED_ARRAY.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); - expect(resultState.isDirty).toEqual(true); - expect(resultState.isPristine).toEqual(false); + it('should mark state as pristine if all children are pristine when group child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isDirty', true], ['isPristine', false]], FORM_CONTROL_0_ID)); + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_1_ID)); + expect(resultState.isDirty).toEqual(false); + expect(resultState.isPristine).toEqual(true); }); - it('should forward actions to children', () => { - const state = { - ...INITIAL_STATE, - isDirty: true, - isPristine: false, - controls: [ - { - ...INITIAL_STATE.controls[0], - isDirty: true, - isPristine: false, - }, - { - ...INITIAL_STATE.controls[1], - isDirty: true, - isPristine: false, - }, - ], - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_0_ID)); - expect(resultState.controls[0].isDirty).toEqual(false); - expect(resultState.controls[0].isPristine).toEqual(true); + it('should mark state as pristine if all children are pristine when array child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isDirty', true], ['isPristine', false]], FORM_CONTROL_0_ID)); + const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_1_ID)); + expect(resultState.isDirty).toEqual(false); + expect(resultState.isPristine).toEqual(true); }); }); diff --git a/src/array/reducer/mark-as-submitted.spec.ts b/src/array/reducer/mark-as-submitted.spec.ts new file mode 100644 index 00000000..6f363a34 --- /dev/null +++ b/src/array/reducer/mark-as-submitted.spec.ts @@ -0,0 +1,79 @@ +import { MarkAsSubmittedAction } from '../../actions'; +import { cast } from '../../state'; +import { markAsSubmittedReducer } from './mark-as-submitted'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, + setPropertiesRecursively, +} from './test-util'; + +describe(`form array ${markAsSubmittedReducer.name}`, () => { + const INITIAL_STATE_SUBMITTED = cast(setPropertiesRecursively(INITIAL_STATE, [['isSubmitted', true], ['isUnsubmitted', false]])); + + it('should mark itself and all children recursively as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE_SUBMITTED); + }); + + it('should not update state if all children are marked as submitted recursively', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_SUBMITTED, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE_SUBMITTED); + }); + + it('should mark children as submitted if the group itself is already marked as submitted', () => { + const state = { + ...INITIAL_STATE, + isSubmitted: true, + isUnsubmitted: false, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isSubmitted: true, + isUnsubmitted: false, + }, + ], + }; + const resultState = markAsSubmittedReducer(state, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE_SUBMITTED); + }); + + it('should mark control children as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isSubmitted).toEqual(true); + expect(resultState.controls[0].isUnsubmitted).toEqual(false); + }); + + it('should mark group children as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_NESTED_GROUP, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isSubmitted).toEqual(true); + expect(resultState.controls[0].isUnsubmitted).toEqual(false); + }); + + it('should mark array children as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_NESTED_ARRAY, new MarkAsSubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isSubmitted).toEqual(true); + expect(resultState.controls[0].isUnsubmitted).toEqual(false); + }); + + it('should mark state as submitted if control child is marked as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_0_ID)); + expect(resultState.isSubmitted).toEqual(true); + expect(resultState.isUnsubmitted).toEqual(false); + }); + + it('should mark state as submitted if group child is marked as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_NESTED_GROUP, new MarkAsSubmittedAction(FORM_CONTROL_0_ID)); + expect(resultState.isSubmitted).toEqual(true); + expect(resultState.isUnsubmitted).toEqual(false); + }); + + it('should mark state as submitted if array child is marked as submitted', () => { + const resultState = markAsSubmittedReducer(INITIAL_STATE_NESTED_ARRAY, new MarkAsSubmittedAction(FORM_CONTROL_0_ID)); + expect(resultState.isSubmitted).toEqual(true); + expect(resultState.isUnsubmitted).toEqual(false); + }); +}); diff --git a/src/array/reducer/mark-as-submitted.ts b/src/array/reducer/mark-as-submitted.ts new file mode 100644 index 00000000..8d8ca4ee --- /dev/null +++ b/src/array/reducer/mark-as-submitted.ts @@ -0,0 +1,30 @@ +import { FormArrayState, KeyValue } from '../../state'; +import { Actions, MarkAsSubmittedAction } from '../../actions'; +import { computeArrayState, dispatchActionPerChild, childReducer } from './util'; + +export function markAsSubmittedReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== MarkAsSubmittedAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + const controls = dispatchActionPerChild(state.controls, controlId => new MarkAsSubmittedAction(controlId)); + + if (controls === state.controls) { + return state; + } + + return computeArrayState( + state.id, + controls, + state.value, + state.errors, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/mark-as-touched.spec.ts b/src/array/reducer/mark-as-touched.spec.ts new file mode 100644 index 00000000..3196c8ec --- /dev/null +++ b/src/array/reducer/mark-as-touched.spec.ts @@ -0,0 +1,79 @@ +import { MarkAsTouchedAction } from '../../actions'; +import { cast } from '../../state'; +import { markAsTouchedReducer } from './mark-as-touched'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, + setPropertiesRecursively, +} from './test-util'; + +describe(`form array ${markAsTouchedReducer.name}`, () => { + const INITIAL_STATE_TOUCHED = cast(setPropertiesRecursively(INITIAL_STATE, [['isTouched', true], ['isUntouched', false]])); + + it('should mark itself and all children recursively as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE_TOUCHED); + }); + + it('should not update state if all children are marked as touched recursively', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_TOUCHED, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE_TOUCHED); + }); + + it('should mark children as touched if the group itself is already marked as touched', () => { + const state = { + ...INITIAL_STATE, + isTouched: true, + isUntouched: false, + controls: [ + INITIAL_STATE.controls[0], + { + ...INITIAL_STATE.controls[1], + isTouched: true, + isUntouched: false, + }, + ], + }; + const resultState = markAsTouchedReducer(state, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState).toEqual(INITIAL_STATE_TOUCHED); + }); + + it('should mark control children as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isTouched).toEqual(true); + expect(resultState.controls[0].isUntouched).toEqual(false); + }); + + it('should mark group children as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_NESTED_GROUP, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isTouched).toEqual(true); + expect(resultState.controls[0].isUntouched).toEqual(false); + }); + + it('should mark array children as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_NESTED_ARRAY, new MarkAsTouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isTouched).toEqual(true); + expect(resultState.controls[0].isUntouched).toEqual(false); + }); + + it('should mark state as touched if control child is marked as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_0_ID)); + expect(resultState.isTouched).toEqual(true); + expect(resultState.isUntouched).toEqual(false); + }); + + it('should mark state as touched if group child is marked as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_NESTED_GROUP, new MarkAsTouchedAction(FORM_CONTROL_0_ID)); + expect(resultState.isTouched).toEqual(true); + expect(resultState.isUntouched).toEqual(false); + }); + + it('should mark state as touched if array child is marked as touched', () => { + const resultState = markAsTouchedReducer(INITIAL_STATE_NESTED_ARRAY, new MarkAsTouchedAction(FORM_CONTROL_0_ID)); + expect(resultState.isTouched).toEqual(true); + expect(resultState.isUntouched).toEqual(false); + }); +}); diff --git a/src/array/reducer/mark-as-touched.ts b/src/array/reducer/mark-as-touched.ts new file mode 100644 index 00000000..f00b0b0d --- /dev/null +++ b/src/array/reducer/mark-as-touched.ts @@ -0,0 +1,30 @@ +import { Actions, MarkAsTouchedAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; + +export function markAsTouchedReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== MarkAsTouchedAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + const controls = dispatchActionPerChild(state.controls, controlId => new MarkAsTouchedAction(controlId)); + + if (controls === state.controls) { + return state; + } + + return computeArrayState( + state.id, + controls, + state.value, + state.errors, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/mark-as-unsubmitted.spec.ts b/src/array/reducer/mark-as-unsubmitted.spec.ts new file mode 100644 index 00000000..4ba30130 --- /dev/null +++ b/src/array/reducer/mark-as-unsubmitted.spec.ts @@ -0,0 +1,75 @@ +import { MarkAsUnsubmittedAction } from '../../actions'; +import { cast } from '../../state'; +import { markAsUnsubmittedReducer } from './mark-as-unsubmitted'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_1_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, + setPropertiesRecursively, +} from './test-util'; + +describe(`form array ${markAsUnsubmittedReducer.name}`, () => { + it('should update state if submitted', () => { + const state = { ...INITIAL_STATE, isSubmitted: true, isUnsubmitted: false }; + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(resultState.isSubmitted).toEqual(false); + expect(resultState.isUnsubmitted).toEqual(true); + }); + + it('should not update state if unsubmitted', () => { + const resultState = markAsUnsubmittedReducer(INITIAL_STATE, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE); + }); + + it('should mark control children as unsubmitted', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isSubmitted', true], ['isUnsubmitted', false]])); + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isSubmitted).toEqual(false); + expect(resultState.controls[0].isUnsubmitted).toEqual(true); + }); + + it('should mark group children as unsubmitted', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isSubmitted', true], ['isUnsubmitted', false]])); + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isSubmitted).toEqual(false); + expect(resultState.controls[0].isUnsubmitted).toEqual(true); + }); + + it('should mark array children as unsubmitted', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isSubmitted', true], ['isUnsubmitted', false]])); + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isSubmitted).toEqual(false); + expect(resultState.controls[0].isUnsubmitted).toEqual(true); + }); + + it('should mark state as unsubmitted if all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isSubmitted', true], ['isUnsubmitted', false]], FORM_CONTROL_0_ID)); + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_1_ID)); + expect(resultState.isSubmitted).toEqual(false); + expect(resultState.isUnsubmitted).toEqual(true); + }); + + it('should not mark state as unsubmitted if not all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isSubmitted', true], ['isUnsubmitted', false]])); + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_0_ID)); + expect(resultState.isSubmitted).toEqual(true); + expect(resultState.isUnsubmitted).toEqual(false); + }); + + it('should mark state as unsubmitted if all children are pristine when group child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isSubmitted', true], ['isUnsubmitted', false]], FORM_CONTROL_0_ID)); + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_1_ID)); + expect(resultState.isSubmitted).toEqual(false); + expect(resultState.isUnsubmitted).toEqual(true); + }); + + it('should mark state as unsubmitted if all children are pristine when array child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isSubmitted', true], ['isUnsubmitted', false]], FORM_CONTROL_0_ID)); + const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_1_ID)); + expect(resultState.isSubmitted).toEqual(false); + expect(resultState.isUnsubmitted).toEqual(true); + }); +}); diff --git a/src/array/reducer/mark-as-unsubmitted.ts b/src/array/reducer/mark-as-unsubmitted.ts new file mode 100644 index 00000000..ce542ec2 --- /dev/null +++ b/src/array/reducer/mark-as-unsubmitted.ts @@ -0,0 +1,28 @@ +import { Actions, MarkAsUnsubmittedAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; + +export function markAsUnsubmittedReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== MarkAsUnsubmittedAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + if (state.isUnsubmitted) { + return state; + } + + return computeArrayState( + state.id, + dispatchActionPerChild(state.controls, controlId => new MarkAsUnsubmittedAction(controlId)), + state.value, + state.errors, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/mark-as-untouched.spec.ts b/src/array/reducer/mark-as-untouched.spec.ts new file mode 100644 index 00000000..d947cf5b --- /dev/null +++ b/src/array/reducer/mark-as-untouched.spec.ts @@ -0,0 +1,75 @@ +import { MarkAsUntouchedAction } from '../../actions'; +import { cast } from '../../state'; +import { markAsUntouchedReducer } from './mark-as-untouched'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_1_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, + setPropertiesRecursively, +} from './test-util'; + +describe(`form array ${markAsUntouchedReducer.name}`, () => { + it('should update state if touched', () => { + const state = { ...INITIAL_STATE, isTouched: true, isUntouched: false }; + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(resultState.isTouched).toEqual(false); + expect(resultState.isUntouched).toEqual(true); + }); + + it('should not update state if untouched', () => { + const resultState = markAsUntouchedReducer(INITIAL_STATE, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE); + }); + + it('should mark control children as untouched', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isTouched', true], ['isUntouched', false]])); + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isTouched).toEqual(false); + expect(resultState.controls[0].isUntouched).toEqual(true); + }); + + it('should mark group children as untouched', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isTouched', true], ['isUntouched', false]])); + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isTouched).toEqual(false); + expect(resultState.controls[0].isUntouched).toEqual(true); + }); + + it('should mark array children as untouched', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isTouched', true], ['isUntouched', false]])); + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); + expect(resultState.controls[0].isTouched).toEqual(false); + expect(resultState.controls[0].isUntouched).toEqual(true); + }); + + it('should mark state as untouched if all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isTouched', true], ['isUntouched', false]], FORM_CONTROL_0_ID)); + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_1_ID)); + expect(resultState.isTouched).toEqual(false); + expect(resultState.isUntouched).toEqual(true); + }); + + it('should not mark state as untouched if not all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE, [['isTouched', true], ['isUntouched', false]])); + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_0_ID)); + expect(resultState.isTouched).toEqual(true); + expect(resultState.isUntouched).toEqual(false); + }); + + it('should mark state as untouched if all children are pristine when group child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_GROUP, [['isTouched', true], ['isUntouched', false]], FORM_CONTROL_0_ID)); + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_1_ID)); + expect(resultState.isTouched).toEqual(false); + expect(resultState.isUntouched).toEqual(true); + }); + + it('should mark state as untouched if all children are pristine when array child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_NESTED_ARRAY, [['isTouched', true], ['isUntouched', false]], FORM_CONTROL_0_ID)); + const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_1_ID)); + expect(resultState.isTouched).toEqual(false); + expect(resultState.isUntouched).toEqual(true); + }); +}); diff --git a/src/array/reducer/mark-as-untouched.ts b/src/array/reducer/mark-as-untouched.ts new file mode 100644 index 00000000..8f3464f0 --- /dev/null +++ b/src/array/reducer/mark-as-untouched.ts @@ -0,0 +1,28 @@ +import { Actions, MarkAsUntouchedAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; + +export function markAsUntouchedReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== MarkAsUntouchedAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + if (state.isUntouched) { + return state; + } + + return computeArrayState( + state.id, + dispatchActionPerChild(state.controls, controlId => new MarkAsUntouchedAction(controlId)), + state.value, + state.errors, + state.userDefinedProperties, + ); +} diff --git a/src/array/reducer/set-errors.spec.ts b/src/array/reducer/set-errors.spec.ts new file mode 100644 index 00000000..fa52f6cc --- /dev/null +++ b/src/array/reducer/set-errors.spec.ts @@ -0,0 +1,122 @@ +import { SetErrorsAction } from '../../actions'; +import { setErrorsReducer } from './set-errors'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_1_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, +} from './test-util'; + +describe(`form array ${setErrorsReducer.name}`, () => { + it('should update state if there are errors', () => { + const errors = { required: true }; + const resultState = setErrorsReducer(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_ID, errors)); + expect(resultState.errors).toEqual(errors); + expect(resultState.isValid).toBe(false); + expect(resultState.isInvalid).toBe(true); + }); + + it('should update state if there are no errors', () => { + const errors = { required: true }; + const state = { ...INITIAL_STATE, isValid: false, isInvalid: true, errors }; + const resultState = setErrorsReducer(state, new SetErrorsAction(FORM_CONTROL_ID, {})); + expect(resultState.errors).toEqual({}); + expect(resultState.isValid).toBe(true); + expect(resultState.isInvalid).toBe(false); + }); + + it('should not update state if errors are equal', () => { + const state = { ...INITIAL_STATE, isValid: false, isInvalid: true, errors: { required: true } }; + const resultState = setErrorsReducer(state, new SetErrorsAction(FORM_CONTROL_ID, { required: true })); + expect(resultState).toBe(state); + }); + + it('should not update state if control is disabled', () => { + const errors = { required: true }; + const state = { ...INITIAL_STATE, isEnabled: false, isDisabled: true }; + const resultState = setErrorsReducer(state, new SetErrorsAction(FORM_CONTROL_ID, errors)); + expect(resultState).toBe(state); + }); + + it('should not update state if errors are equal and empty', () => { + const resultState = setErrorsReducer(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_ID, {})); + expect(resultState).toBe(INITIAL_STATE); + }); + + it('should throw if trying to set invalid error value', () => { + expect(() => setErrorsReducer(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_ID, null as any))).toThrowError(); + expect(() => setErrorsReducer(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_ID, { _inner: true }))).toThrowError(); + }); + + it('should aggregate child errors', () => { + const errors = { required: true }; + const resultState = setErrorsReducer(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_0_ID, errors)); + expect(resultState.errors).toEqual({ _0: errors }); + expect(resultState.isValid).toEqual(false); + expect(resultState.isInvalid).toEqual(true); + }); + + it('should aggregate child errors for group children', () => { + const errors = { required: true }; + const resultState = setErrorsReducer(INITIAL_STATE_NESTED_GROUP, new SetErrorsAction(FORM_CONTROL_0_ID, errors)); + expect(resultState.errors).toEqual({ _0: errors }); + expect(resultState.isValid).toEqual(false); + expect(resultState.isInvalid).toEqual(true); + }); + + it('should aggregate child errors for array children', () => { + const errors = { required: true }; + const resultState = setErrorsReducer(INITIAL_STATE_NESTED_ARRAY, new SetErrorsAction(FORM_CONTROL_0_ID, errors)); + expect(resultState.errors).toEqual({ _0: errors }); + expect(resultState.isValid).toEqual(false); + expect(resultState.isInvalid).toEqual(true); + }); + + it('should aggregate multiple child errors', () => { + const errors1 = { required: true }; + const errors2 = { min: 0 }; + let resultState = setErrorsReducer(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_0_ID, errors1)); + resultState = setErrorsReducer(resultState, new SetErrorsAction(FORM_CONTROL_1_ID, errors2)); + expect(resultState.errors).toEqual({ _0: errors1, _1: errors2 }); + expect(resultState.isValid).toEqual(false); + expect(resultState.isInvalid).toEqual(true); + }); + + it('should track child errors and own errors when own errors are changed', () => { + const errors1 = { required: true }; + const errors2 = { min: 0 }; + const state = { + ...INITIAL_STATE, + errors: { + _0: errors2, + }, + isValid: false, + isInvalid: true, + controls: [ + { + ...INITIAL_STATE.controls[0], + isValid: false, + isInvalid: true, + errors: errors2, + }, + ], + }; + const resultState = setErrorsReducer(state, new SetErrorsAction(FORM_CONTROL_ID, errors1)); + expect(resultState.errors).toEqual({ ...errors1, _0: errors2 }); + }); + + it('should track own errors and child errors when child errors are changed', () => { + const errors1 = { required: true }; + const state = { + ...INITIAL_STATE, + isValid: false, + isInvalid: true, + errors: errors1, + }; + const errors2 = { min: 0 }; + const resultState = setErrorsReducer(state, new SetErrorsAction(FORM_CONTROL_0_ID, errors2)); + expect(resultState.errors).toEqual({ ...errors1, _0: errors2 }); + }); +}); diff --git a/src/array/reducer/set-errors.ts b/src/array/reducer/set-errors.ts new file mode 100644 index 00000000..dae6a8af --- /dev/null +++ b/src/array/reducer/set-errors.ts @@ -0,0 +1,46 @@ +import { Actions, SetErrorsAction } from '../../actions'; +import { FormArrayState } from '../../state'; +import { deepEquals } from '../../util'; +import { childReducer, computeArrayState } from './util'; + +export function setErrorsReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== SetErrorsAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + if (!action.payload.errors) { + throw new Error(`Control errors must be an object; got ${JSON.stringify(action.payload.errors)}`); // `; + } + + if (Object.keys(action.payload.errors).some(key => key.startsWith('_'))) { + throw new Error(`Control errors must not use underscore as a prefix; got ${JSON.stringify(action.payload.errors)}`); // `; + } + + if (state.errors === action.payload.errors) { + return state; + } + + if (deepEquals(state.errors, action.payload.errors)) { + return state; + } + + if (state.isDisabled) { + return state; + } + + const childErrors = + Object.keys(state.errors) + .filter(key => key.startsWith('_')) + .reduce((res, key) => Object.assign(res, { [key]: state.errors[key] }), {}); + + const newErrors = Object.assign(childErrors, action.payload.errors); + + return computeArrayState(state.id, state.controls, state.value, newErrors, state.userDefinedProperties); +} diff --git a/src/array/reducer/set-user-defined-property.spec.ts b/src/array/reducer/set-user-defined-property.spec.ts new file mode 100644 index 00000000..5c3e4431 --- /dev/null +++ b/src/array/reducer/set-user-defined-property.spec.ts @@ -0,0 +1,25 @@ +import { SetUserDefinedPropertyAction } from '../../actions'; +import { setUserDefinedPropertyReducer } from './set-user-defined-property'; +import { FORM_CONTROL_ID, INITIAL_STATE } from './test-util'; + +describe(`form group ${setUserDefinedPropertyReducer.name}`, () => { + it('should skip any actionof the wrong type', () => + expect(setUserDefinedPropertyReducer(INITIAL_STATE, { type: '' } as any)).toBe(INITIAL_STATE)); + + it('should update state user defined properties if different', () => { + const prop = 'prop'; + const value = 12; + const resultState = setUserDefinedPropertyReducer(INITIAL_STATE, new SetUserDefinedPropertyAction(FORM_CONTROL_ID, prop, value)); + expect(resultState.userDefinedProperties).toEqual({ + [prop]: value, + }); + }); + + it('should not update state user defined properties if same', () => { + const prop = 'prop'; + const value = 12; + const state = { ...INITIAL_STATE, userDefinedProperties: { [prop]: value } }; + const resultState = setUserDefinedPropertyReducer(state, new SetUserDefinedPropertyAction(FORM_CONTROL_ID, prop, value)); + expect(resultState).toBe(state); + }); +}); diff --git a/src/array/reducer/set-user-defined-property.ts b/src/array/reducer/set-user-defined-property.ts new file mode 100644 index 00000000..4e51af99 --- /dev/null +++ b/src/array/reducer/set-user-defined-property.ts @@ -0,0 +1,27 @@ +import { Actions, SetUserDefinedPropertyAction } from '../../actions'; +import { FormArrayState } from '../../state'; + +export function setUserDefinedPropertyReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== SetUserDefinedPropertyAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return state; + } + + if (state.userDefinedProperties[action.payload.name] === action.payload.value) { + return state; + } + + return { + ...state, + userDefinedProperties: { + ...state.userDefinedProperties, + [action.payload.name]: action.payload.value, + }, + }; +} diff --git a/src/array/reducer/set-value.spec.ts b/src/array/reducer/set-value.spec.ts new file mode 100644 index 00000000..c985ada8 --- /dev/null +++ b/src/array/reducer/set-value.spec.ts @@ -0,0 +1,160 @@ +import { SetValueAction } from '../../actions'; +import { setValueReducer } from './set-value'; +import { + FORM_CONTROL_0_ID, + FORM_CONTROL_ID, + INITIAL_STATE, + INITIAL_STATE_NESTED_ARRAY, + INITIAL_STATE_NESTED_GROUP, +} from './test-util'; + +describe(`form array ${setValueReducer.name}`, () => { + it('should update state value if different', () => { + const value = ['A', '']; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + }); + + it('should not update state value if same', () => { + const value = ['', '']; + const state = { ...INITIAL_STATE, value }; + const resultState = setValueReducer(state, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState).toBe(state); + }); + + it('should mark state as dirty if value is different', () => { + const value = ['A', '']; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.isDirty).toEqual(true); + }); + + it('should update child state value', () => { + const value = ['A', '']; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.controls[0].value).toEqual(value[0]); + }); + + it('should create child states on demand', () => { + const value = ['', '', '']; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + expect(resultState.controls[2].value).toEqual(value[2]); + }); + + it('should create child states on demand for group children', () => { + const value = [{ inner: '' }, { inner: '' }, { inner: '' }]; + const resultState = setValueReducer(INITIAL_STATE_NESTED_GROUP, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + expect(resultState.controls[2].value).toEqual(value[2]); + }); + + it('should create child states on demand for array children', () => { + const value = [[''], [''], ['']]; + const resultState = setValueReducer(INITIAL_STATE_NESTED_ARRAY, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + expect(resultState.controls[2].value).toEqual(value[2]); + }); + + it('should create child states on demand for null children', () => { + const value = ['', '', null]; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + expect(resultState.controls[2].value).toEqual(value[2]); + }); + + it('should remove child states on demand', () => { + const value = ['']; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + expect(resultState.controls[1]).toBeUndefined(); + }); + + it('should remove child states on demand when value is empty', () => { + const value: string[] = []; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); + expect(resultState.value).toEqual(value); + expect(resultState.controls[0]).toBeUndefined(); + }); + + it('should aggregate child values', () => { + const value = 'A'; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_0_ID, value) as any); + expect(resultState.value).toEqual([value, '']); + }); + + it('should mark state as dirty if child value is updated', () => { + const value = 'A'; + const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_0_ID, value) as any); + expect(resultState.isDirty).toEqual(true); + expect(resultState.controls[0].isDirty).toEqual(true); + }); + + it('should aggregate child values for group children', () => { + const value = { inner: 'A' }; + const resultState = setValueReducer(INITIAL_STATE_NESTED_GROUP, new SetValueAction(FORM_CONTROL_0_ID, value) as any); + expect(resultState.value).toEqual([value, { inner: '' }]); + }); + + it('should mark state as dirty if group child value is updated', () => { + const value = { inner: 'A' }; + const resultState = setValueReducer(INITIAL_STATE_NESTED_GROUP, new SetValueAction(FORM_CONTROL_0_ID, value) as any); + expect(resultState.isDirty).toEqual(true); + expect(resultState.controls[0].isDirty).toEqual(true); + }); + + it('should aggregate child values for array children', () => { + const value = ['A']; + const resultState = setValueReducer(INITIAL_STATE_NESTED_ARRAY, new SetValueAction(FORM_CONTROL_0_ID, value) as any); + expect(resultState.value).toEqual([value, ['']]); + }); + + it('should mark state as dirty if array child value is updated', () => { + const value = ['A']; + const resultState = setValueReducer(INITIAL_STATE_NESTED_ARRAY, new SetValueAction(FORM_CONTROL_0_ID, value) as any); + expect(resultState.isDirty).toEqual(true); + expect(resultState.controls[0].isDirty).toEqual(true); + }); + + it('should remove child errors on demand when value is empty', () => { + const errors = { required: true }; + const state = { + ...INITIAL_STATE, + errors: { + _0: errors, + }, + controls: [ + { + ...INITIAL_STATE.controls[0], + errors, + }, + INITIAL_STATE.controls[1], + ], + }; + const resultState = setValueReducer(state, new SetValueAction(FORM_CONTROL_ID, [])); + expect(resultState.value).toEqual([]); + expect(resultState.errors).toEqual({}); + expect(resultState.controls[0]).toBeUndefined(); + }); + + it('should remove child errors and keep own errors on demand when value is empty', () => { + const errors = { required: true }; + const state = { + ...INITIAL_STATE, + errors: { + _0: errors, + ...errors, + }, + controls: [ + { + ...INITIAL_STATE.controls[0], + errors, + }, + INITIAL_STATE.controls[1], + ], + }; + const resultState = setValueReducer(state, new SetValueAction(FORM_CONTROL_ID, [])); + expect(resultState.value).toEqual([]); + expect(resultState.errors).toEqual(errors); + expect(resultState.controls[0]).toBeUndefined(); + }); +}); diff --git a/src/array/reducer/set-value.ts b/src/array/reducer/set-value.ts new file mode 100644 index 00000000..7377ba3b --- /dev/null +++ b/src/array/reducer/set-value.ts @@ -0,0 +1,37 @@ +import { Actions, SetValueAction } from '../../actions'; +import { createChildState, FormArrayState } from '../../state'; +import { callChildReducer, childReducer, computeArrayState } from './util'; + +export function setValueReducer( + state: FormArrayState, + action: Actions, +): FormArrayState { + if (action.type !== SetValueAction.TYPE) { + return state; + } + + if (action.controlId !== state.id) { + return childReducer(state, action); + } + + if (state.value === action.payload.value) { + return state; + } + + if (action.payload.value instanceof Date) { + throw new Error('Date values are not supported. Please used serialized strings instead.'); + } + + const value = action.payload.value; + + const controls = value + .map((v, i) => { + if (!state.controls[i]) { + return createChildState(`${state.id}.${i}`, v); + } + + return callChildReducer(state.controls[i], new SetValueAction(state.controls[i].id, v)); + }); + + return computeArrayState(state.id, controls, value, state.errors, state.userDefinedProperties); +} diff --git a/src/array/reducer/test-util.ts b/src/array/reducer/test-util.ts new file mode 100644 index 00000000..5faf5366 --- /dev/null +++ b/src/array/reducer/test-util.ts @@ -0,0 +1,59 @@ +import { AbstractControlState, createFormArrayState, FormArrayState, FormControlState, FormGroupState, isArrayState, isGroupState } from '../../state'; + +export const FORM_CONTROL_ID = 'test ID'; +export const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; +export const FORM_CONTROL_1_ID = FORM_CONTROL_ID + '.1'; +export const INITIAL_FORM_ARRAY_VALUE = ['', '']; +export const INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP = [{ inner: '' }, { inner: '' }]; +export const INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY = [[''], ['']]; +export const INITIAL_STATE = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE); +export const INITIAL_STATE_NESTED_GROUP = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_GROUP); +export const INITIAL_STATE_NESTED_ARRAY = createFormArrayState(FORM_CONTROL_ID, INITIAL_FORM_ARRAY_VALUE_NESTED_ARRAY); + +export const setPropertyRecursively = ( + state: AbstractControlState, + property: keyof AbstractControlState, + value: any, + ...excludeIds: string[], +): AbstractControlState => { + if (excludeIds.indexOf(state.id) >= 0) { + return state; + } + + state = { + ...state, + [property]: value, + }; + + if (isArrayState(state)) { + const controls = state.controls.map(s => setPropertyRecursively(s, property, value, ...excludeIds)); + return { + ...state, + controls, + } as any; + } + + if (isGroupState(state)) { + let controls = state.controls; + controls = Object.keys(controls).reduce((res, key) => { + const s = setPropertyRecursively(controls[key], property, value, ...excludeIds); + res[key] = s; + return res; + }, {} as any); + + return { + ...state, + controls, + } as any; + } + + return state; +}; + +export const setPropertiesRecursively = ( + state: AbstractControlState, + properties: Array<[keyof AbstractControlState, any]>, + ...excludeIds: string[], +): AbstractControlState => { + return properties.reduce((s, [p, v]) => setPropertyRecursively(s, p, v, ...excludeIds), state); +}; diff --git a/src/array/reducer/util.ts b/src/array/reducer/util.ts index 2dd0c3b2..d30143a1 100644 --- a/src/array/reducer/util.ts +++ b/src/array/reducer/util.ts @@ -1,7 +1,7 @@ import { Actions } from '../../actions'; import { formControlReducerInternal } from '../../control/reducer'; import { formGroupReducerInternal } from '../../group/reducer'; -import { AbstractControlState, FormArrayState, FormGroupControls, KeyValue, ValidationErrors } from '../../state'; +import { AbstractControlState, FormArrayState, isArrayState, isGroupState, KeyValue, ValidationErrors } from '../../state'; import { isEmpty } from '../../util'; import { formArrayReducerInternal } from '../reducer'; @@ -75,24 +75,16 @@ export function computeArrayState( }; } -export function isArrayState(state: AbstractControlState): boolean { - return state.hasOwnProperty('controls') && Array.isArray((state as any).controls); -} - -export function isGroupState(state: AbstractControlState): boolean { - return state.hasOwnProperty('controls'); -} - export function callChildReducer( state: AbstractControlState, action: Actions, ): AbstractControlState { if (isArrayState(state)) { - return formArrayReducerInternal(state as any, action as any); + return formArrayReducerInternal(state, action as any); } if (isGroupState(state)) { - return formGroupReducerInternal(state as any, action); + return formGroupReducerInternal(state, action); } return formControlReducerInternal(state as any, action); @@ -114,7 +106,7 @@ export function dispatchActionPerChild( function callChildReducers( controls: Array>, - action: Actions, + action: Actions, ): Array> { let hasChanged = false; const newControls = controls @@ -126,7 +118,7 @@ function callChildReducers( return hasChanged ? newControls : controls; } -export function childReducer(state: FormArrayState, action: Actions) { +export function childReducer(state: FormArrayState, action: Actions) { const controls = callChildReducers(state.controls, action); if (state.controls === controls) { diff --git a/src/group/reducer/add-control.spec.ts b/src/group/reducer/add-control.spec.ts index a190077b..81c50903 100644 --- a/src/group/reducer/add-control.spec.ts +++ b/src/group/reducer/add-control.spec.ts @@ -1,14 +1,10 @@ import { AddControlAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { addControlReducer } from './add-control'; +import { FORM_CONTROL_ID, FormGroupValue, INITIAL_STATE } from './test-util'; -describe('form group addControlReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - - it('should create child state', () => { +describe(`form group ${addControlReducer.name}`, () => { + it('should create child state for control child', () => { const value = 'B'; const action = new AddControlAction(FORM_CONTROL_ID, 'inner2', value); const resultState = addControlReducer(INITIAL_STATE, action); @@ -16,7 +12,7 @@ describe('form group addControlReducer', () => { expect(resultState.controls.inner2.value).toEqual(value); }); - it('should create child state for group children', () => { + it('should create child state for group child', () => { const value = { inner4: 'D' }; const action = new AddControlAction(FORM_CONTROL_ID, 'inner3', value); const resultState = addControlReducer(INITIAL_STATE, action); @@ -26,7 +22,7 @@ describe('form group addControlReducer', () => { expect(Array.isArray(cast(resultState.controls.inner3)!.controls)).toBe(false); }); - it('should create child state for array children', () => { + it('should create child state for array child', () => { const value = ['A']; const action = new AddControlAction(FORM_CONTROL_ID, 'inner5', value); const resultState = addControlReducer(INITIAL_STATE, action); diff --git a/src/group/reducer/disable.spec.ts b/src/group/reducer/disable.spec.ts index 375bd53c..8eee4580 100644 --- a/src/group/reducer/disable.spec.ts +++ b/src/group/reducer/disable.spec.ts @@ -1,20 +1,17 @@ import { DisableAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { disableReducer } from './disable'; - -describe('form group disableReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; + +describe(`form group ${disableReducer.name}`, () => { it('should update state if enabled', () => { const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_ID)); expect(resultState.isEnabled).toEqual(false); @@ -36,199 +33,47 @@ describe('form group disableReducer', () => { expect(resultState.errors).toEqual({}); }); - it('should disable direct control children', () => { + it('should disable control children', () => { const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_ID)); expect(resultState.controls.inner.isEnabled).toBe(false); expect(resultState.controls.inner.isDisabled).toBe(true); }); - it('should disable direct group children', () => { + it('should disable group children', () => { const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isEnabled).toBe(false); expect(resultState.controls.inner3.isDisabled).toBe(true); }); - it('should disable direct array children', () => { + it('should disable array children', () => { const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_ID)); expect(resultState.controls.inner5.isEnabled).toBe(false); expect(resultState.controls.inner5.isDisabled).toBe(true); }); - it('should disable nested children in group', () => { - const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner3)!.controls.inner4.isEnabled).toBe(false); - expect(cast(resultState.controls.inner3)!.controls.inner4.isDisabled).toBe(true); - }); - - it('should disable nested children in array', () => { - const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner5)!.controls[0].isEnabled).toBe(false); - expect(cast(resultState.controls.inner5)!.controls[0].isDisabled).toBe(true); - }); - - it('should disable if all children are disabled when direct control child is disabled', () => { + it('should disable if all children are disabled when control child is disabled', () => { const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_INNER_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - it('should not disable if not all children are disabled when direct control child is disabled', () => { + it('should not disable if not all children are disabled when child is disabled', () => { const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); }); - it('should disable if all children are disabled when direct group child is disabled', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isEnabled: false, - isDisabled: true, - }, - inner5: { - ...INITIAL_STATE_FULL.controls.inner5, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...cast(INITIAL_STATE_FULL.controls.inner5)!.controls[0], - isEnabled: false, - isDisabled: true, - }, - ], - }, - }, - }; + it('should disable if all children are disabled when group child is disabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isEnabled', false], ['isDisabled', false]], FORM_CONTROL_INNER3_ID)); const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - it('should disable if all children are disabled when direct array child is disabled', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isEnabled: false, - isDisabled: true, - }, - inner3: { - ...INITIAL_STATE_FULL.controls.inner3, - isEnabled: false, - isDisabled: true, - controls: { - inner4: { - ...cast(INITIAL_STATE_FULL.controls.inner3)!.controls.inner4, - isEnabled: false, - isDisabled: true, - }, - }, - }, - }, - }; + it('should disable if all children are disabled when array child is disabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isEnabled', false], ['isDisabled', false]], FORM_CONTROL_INNER5_ID)); const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isEnabled).toBe(false); expect(resultState.isDisabled).toBe(true); }); - - it('should disable if all children are disabled when nested child in group is disabled', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isEnabled: false, - isDisabled: true, - }, - inner5: { - ...INITIAL_STATE_FULL.controls.inner5, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...cast(INITIAL_STATE_FULL.controls.inner5)!.controls[0], - isEnabled: false, - isDisabled: true, - }, - ], - }, - }, - }; - const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isEnabled).toBe(false); - expect(resultState.isDisabled).toBe(true); - }); - - it('should not disable if not all children are disabled when nested child in group is disabled', () => { - const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isEnabled).toBe(true); - expect(resultState.isDisabled).toBe(false); - }); - - it('should disable if all children are disabled when nested child in array is disabled', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isEnabled: false, - isDisabled: true, - }, - inner3: { - ...INITIAL_STATE_FULL.controls.inner3, - isEnabled: false, - isDisabled: true, - controls: { - inner4: { - ...cast(INITIAL_STATE_FULL.controls.inner3)!.controls.inner4, - isEnabled: false, - isDisabled: true, - }, - }, - }, - }, - }; - const resultState = disableReducer(state, new DisableAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isEnabled).toBe(false); - expect(resultState.isDisabled).toBe(true); - }); - - it('should not disable if not all children are disabled when nested child in array is disabled', () => { - const resultState = disableReducer(INITIAL_STATE_FULL, new DisableAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isEnabled).toBe(true); - expect(resultState.isDisabled).toBe(false); - }); - - it('should forward actions to children', () => { - const resultState = disableReducer(INITIAL_STATE, new DisableAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isEnabled).toEqual(false); - expect(resultState.controls.inner.isDisabled).toEqual(true); - }); }); diff --git a/src/group/reducer/enable.spec.ts b/src/group/reducer/enable.spec.ts index fbf26a5d..f9285338 100644 --- a/src/group/reducer/enable.spec.ts +++ b/src/group/reducer/enable.spec.ts @@ -1,69 +1,25 @@ import { EnableAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { enableReducer } from './enable'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; -describe('form group enableReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER2_ID = FORM_CONTROL_ID + '.inner2'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - +describe(`form group ${enableReducer.name}`, () => { it('should enable itself and all children recursively', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isEnabled: false, - isDisabled: true, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner3: { - ...inner3State, - isEnabled: false, - isDisabled: true, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isEnabled: false, - isDisabled: true, - }, - }, - }, - inner5: { - ...inner5State, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...inner5State.controls[0], - isEnabled: false, - isDisabled: true, - }, - ], - }, - }, - }; + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isEnabled', false], ['isDisabled', true]])); const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_ID)); expect(resultState).toEqual(INITIAL_STATE_FULL); }); it('should not update state if all children are enabled recursively', () => { - const resultState = enableReducer(INITIAL_STATE, new EnableAction(FORM_CONTROL_ID)); - expect(resultState).toBe(INITIAL_STATE); + const resultState = enableReducer(INITIAL_STATE_FULL, new EnableAction(FORM_CONTROL_ID)); + expect(resultState).toBe(INITIAL_STATE_FULL); }); it('should enable children if the group itself is already enabled', () => { @@ -82,167 +38,30 @@ describe('form group enableReducer', () => { expect(resultState).toEqual(INITIAL_STATE_FULL); }); - it('should enable if direct control child gets enabled', () => { - const state = { - ...INITIAL_STATE, - isEnabled: false, - isDisabled: true, - controls: { - ...INITIAL_STATE.controls, - inner: { - ...INITIAL_STATE.controls.inner, - isEnabled: false, - isDisabled: true, - }, - }, - }; + it('should enable without enabling any other children if control child gets enabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isEnabled', false], ['isDisabled', true]])); const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER_ID)); - expect(resultState).toEqual(INITIAL_STATE); - }); - - it('should enable without enabling any other children if direct group child gets enabled', () => { - const state = { - ...INITIAL_STATE_FULL, - isEnabled: false, - isDisabled: true, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isEnabled: false, - isDisabled: true, - }, - }, - }; - const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER2_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); - expect(resultState.controls.inner.isEnabled).toBe(false); - expect(resultState.controls.inner.isDisabled).toBe(true); + expect(resultState.controls.inner2.isEnabled).toBe(false); + expect(resultState.controls.inner2.isDisabled).toBe(true); }); - it('should enable without enabling any other children if direct array child gets enabled', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isEnabled: false, - isDisabled: true, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner5: { - ...inner5State, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...inner5State.controls[0], - isEnabled: false, - isDisabled: true, - }, - ], - }, - }, - }; - const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER5_ID)); - expect(resultState.isEnabled).toBe(true); - expect(resultState.isDisabled).toBe(false); - expect(resultState.controls.inner.isEnabled).toBe(false); - expect(resultState.controls.inner.isDisabled).toBe(true); - }); - - it('should enable without enabling any other children if nested child in group gets enabled', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isEnabled: false, - isDisabled: true, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner3: { - ...inner3State, - isEnabled: false, - isDisabled: true, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isEnabled: false, - isDisabled: true, - }, - }, - }, - }, - }; - const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER4_ID)); + it('should enable without enabling any other children if group child gets enabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isEnabled', false], ['isDisabled', true]])); + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); expect(resultState.controls.inner.isEnabled).toBe(false); expect(resultState.controls.inner.isDisabled).toBe(true); }); - it('should enable without enabling any other children if nested child in array gets enabled', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isEnabled: false, - isDisabled: true, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - inner5: { - ...inner5State, - isEnabled: false, - isDisabled: true, - controls: [ - { - ...inner5State.controls[0], - isEnabled: false, - isDisabled: true, - }, - ], - }, - }, - }; - const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER5_0_ID)); + it('should enable without enabling any other children if array child gets enabled', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isEnabled', false], ['isDisabled', true]])); + const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isEnabled).toBe(true); expect(resultState.isDisabled).toBe(false); expect(resultState.controls.inner.isEnabled).toBe(false); expect(resultState.controls.inner.isDisabled).toBe(true); }); - - it('should forward actions to children', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isEnabled: false, - isDisabled: true, - }, - }, - }; - const resultState = enableReducer(state, new EnableAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isEnabled).toEqual(true); - expect(resultState.controls.inner.isDisabled).toEqual(false); - }); }); diff --git a/src/group/reducer/mark-as-dirty.spec.ts b/src/group/reducer/mark-as-dirty.spec.ts index c8e15562..e45df7f8 100644 --- a/src/group/reducer/mark-as-dirty.spec.ts +++ b/src/group/reducer/mark-as-dirty.spec.ts @@ -1,64 +1,18 @@ import { MarkAsDirtyAction } from '../../actions'; -import { cast, createFormGroupState, FormGroupState } from '../../state'; +import { cast, FormGroupState } from '../../state'; import { markAsDirtyReducer } from './mark-as-dirty'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsDirtyReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - const INITIAL_STATE_FULL_INNER3 = cast(INITIAL_STATE_FULL.controls.inner3)!; - const INITIAL_STATE_FULL_INNER5 = cast(INITIAL_STATE_FULL.controls.inner5)!; - const INITIAL_STATE_FULL_DIRTY = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isDirty: true, - isPristine: false, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isDirty: true, - isPristine: false, - }, - inner3: { - ...INITIAL_STATE_FULL_INNER3, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL_INNER3.controls, - inner4: { - ...INITIAL_STATE_FULL_INNER3.controls.inner4, - isDirty: true, - isPristine: false, - }, - }, - }, - inner5: { - ...INITIAL_STATE_FULL_INNER5, - isDirty: true, - isPristine: false, - controls: [ - { - ...INITIAL_STATE_FULL_INNER5.controls[0], - isDirty: true, - isPristine: false, - }, - ], - }, - }, - }; +describe(`form group ${markAsDirtyReducer.name}`, () => { + const INITIAL_STATE_FULL_DIRTY = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isDirty', true], ['isPristine', false]])); it('should mark itself and all children recursively as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_ID)); @@ -88,63 +42,39 @@ describe('form group markAsDirtyReducer', () => { expect(resultState).toEqual(INITIAL_STATE_FULL_DIRTY); }); - it('should mark direct control children as dirty', () => { + it('should mark control children as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_ID)); expect(resultState.controls.inner.isDirty).toEqual(true); expect(resultState.controls.inner.isPristine).toEqual(false); }); - it('should mark direct group children as dirty', () => { + it('should mark group children as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isDirty).toEqual(true); expect(resultState.controls.inner3.isPristine).toEqual(false); }); - it('should mark direct array children as dirty', () => { + it('should mark array children as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_ID)); expect(resultState.controls.inner5.isDirty).toEqual(true); expect(resultState.controls.inner5.isPristine).toEqual(false); }); - it('should mark nested children as dirty', () => { - const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_ID)); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isDirty).toBe(true); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4.isPristine).toBe(false); - }); - - it('should mark state as dirty if direct control child is marked as dirty', () => { + it('should mark state as dirty if control child is marked as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_INNER_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - it('should mark state as dirty if direct group child is marked as dirty', () => { + it('should mark state as dirty if group child is marked as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - it('should mark state as dirty if direct array child is marked as dirty', () => { + it('should mark state as dirty if array child is marked as dirty', () => { const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - - it('should mark state as dirty if nested child in group is marked as dirty', () => { - const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isDirty).toEqual(true); - expect(resultState.isPristine).toEqual(false); - }); - - it('should mark state as dirty if nested child in array is marked as dirty', () => { - const resultState = markAsDirtyReducer(INITIAL_STATE_FULL, new MarkAsDirtyAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isDirty).toEqual(true); - expect(resultState.isPristine).toEqual(false); - }); - - it('should forward actions to children', () => { - const resultState = markAsDirtyReducer(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isDirty).toEqual(true); - expect(resultState.controls.inner.isPristine).toEqual(false); - }); }); diff --git a/src/group/reducer/mark-as-pristine.spec.ts b/src/group/reducer/mark-as-pristine.spec.ts index f9e25c5a..98ac2342 100644 --- a/src/group/reducer/mark-as-pristine.spec.ts +++ b/src/group/reducer/mark-as-pristine.spec.ts @@ -1,20 +1,18 @@ import { MarkAsPristineAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { markAsPristineReducer } from './mark-as-pristine'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsPristineReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - +describe(`form group ${markAsPristineReducer.name}`, () => { it('should update state if dirty', () => { const state = { ...INITIAL_STATE, isDirty: true, isPristine: false }; const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); @@ -27,310 +25,70 @@ describe('form group markAsPristineReducer', () => { expect(resultState).toBe(INITIAL_STATE); }); - it('should mark direct control children as pristine', () => { - const state = { - ...INITIAL_STATE, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE.controls, - inner: { - ...INITIAL_STATE.controls.inner, - isDirty: true, - isPristine: false, - }, - }, - }; + it('should mark control children as pristine', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); expect(resultState.controls.inner.isDirty).toEqual(false); expect(resultState.controls.inner.isPristine).toEqual(true); }); - it('should mark direct group children as pristine', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isDirty: true, - isPristine: false, - controls: { - inner4: { - ...inner3State.controls.inner4, - isDirty: true, - isPristine: false, - }, - }, - }, - }, - }; + it('should mark group children as pristine', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isDirty).toEqual(false); expect(resultState.controls.inner3.isPristine).toEqual(true); }); - it('should mark direct array children as pristine', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isDirty: true, - isPristine: false, - controls: [ - { - ...inner5State.controls[0], - isDirty: true, - isPristine: false, - }, - ], - }, - }, - }; + it('should mark array children as pristine', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); expect(resultState.controls.inner5.isDirty).toEqual(false); expect(resultState.controls.inner5.isPristine).toEqual(true); }); - it('should mark nested children in group as pristine', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isDirty: true, - isPristine: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isDirty: true, - isPristine: false, - }, - }, - }, - }, - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner3)!.controls.inner4.isDirty).toBe(false); - expect(cast(resultState.controls.inner3)!.controls.inner4.isPristine).toBe(true); - }); - - it('should mark nested children in array as pristine', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isDirty: true, - isPristine: false, - controls: [ - { - ...inner5State.controls[0], - isDirty: true, - isPristine: false, - }, - ], - }, - }, - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner5)!.controls[0].isDirty).toBe(false); - expect(cast(resultState.controls.inner5)!.controls[0].isPristine).toBe(true); - }); - - it('should mark state as pristine if all children are pristine when direct control child is updated', () => { - const state = { - ...INITIAL_STATE, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE.controls, - inner: { - ...INITIAL_STATE.controls.inner, - isDirty: true, - isPristine: false, - }, - }, - }; + it('should mark state as pristine if all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isDirty', true], ['isPristine', false]], + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + )); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER_ID)); expect(resultState.isDirty).toEqual(false); expect(resultState.isPristine).toEqual(true); }); - it('should not mark state as pristine if not all children are pristine when direct control child is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isDirty: true, - isPristine: false, - }, - inner3: { - ...inner3State, - isDirty: true, - isPristine: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isDirty: true, - isPristine: false, - }, - }, - }, - }, - }; + it('should not mark state as pristine if not all children are pristine when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isDirty', true], ['isPristine', false]])); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER_ID)); expect(resultState.isDirty).toEqual(true); expect(resultState.isPristine).toEqual(false); }); - it('should mark state as pristine if all children are pristine when direct group child is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isDirty: true, - isPristine: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isDirty: true, - isPristine: false, - }, - }, - }, - }, - }; + it('should mark state as pristine if all children are pristine when group child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isDirty', true], ['isPristine', false]], + FORM_CONTROL_INNER_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER5_ID, + )); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isDirty).toEqual(false); expect(resultState.isPristine).toEqual(true); }); - it('should mark state as pristine if all children are pristine when direct array child is updated', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isDirty: true, - isPristine: false, - controls: [ - { - ...inner5State.controls[0], - isDirty: true, - isPristine: false, - }, - ], - }, - }, - }; + it('should mark state as pristine if all children are pristine when array child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isDirty', true], ['isPristine', false]], + FORM_CONTROL_INNER_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + )); const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isDirty).toEqual(false); expect(resultState.isPristine).toEqual(true); }); - - it('should mark state as pristine if all children are pristine when nested child in group is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isDirty: true, - isPristine: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isDirty: true, - isPristine: false, - }, - }, - }, - }, - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isDirty).toEqual(false); - expect(resultState.isPristine).toEqual(true); - }); - - it('should mark state as pristine if all children are pristine when nested child in array is updated', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isDirty: true, - isPristine: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isDirty: true, - isPristine: false, - controls: [ - { - ...inner5State.controls[0], - isDirty: true, - isPristine: false, - }, - ], - }, - }, - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isDirty).toEqual(false); - expect(resultState.isPristine).toEqual(true); - }); - - it('should forward actions to children', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isDirty: true, - isPristine: false, - }, - }, - }; - const resultState = markAsPristineReducer(state, new MarkAsPristineAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isDirty).toEqual(false); - expect(resultState.controls.inner.isPristine).toEqual(true); - }); }); diff --git a/src/group/reducer/mark-as-submitted.spec.ts b/src/group/reducer/mark-as-submitted.spec.ts index 740d8b46..b9893148 100644 --- a/src/group/reducer/mark-as-submitted.spec.ts +++ b/src/group/reducer/mark-as-submitted.spec.ts @@ -1,64 +1,18 @@ import { MarkAsSubmittedAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { markAsSubmittedReducer } from './mark-as-submitted'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsSubmittedReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - const INITIAL_STATE_FULL_INNER3 = cast(INITIAL_STATE_FULL.controls.inner3)!; - const INITIAL_STATE_FULL_INNER5 = cast(INITIAL_STATE_FULL.controls.inner5)!; - const INITIAL_STATE_FULL_SUBMITTED = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isSubmitted: true, - isUnsubmitted: false, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isSubmitted: true, - isUnsubmitted: false, - }, - inner3: { - ...INITIAL_STATE_FULL_INNER3, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL_INNER3.controls, - inner4: { - ...INITIAL_STATE_FULL_INNER3.controls.inner4, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }, - inner5: { - ...INITIAL_STATE_FULL_INNER5, - isSubmitted: true, - isUnsubmitted: false, - controls: [ - { - ...INITIAL_STATE_FULL_INNER5.controls[0], - isSubmitted: true, - isUnsubmitted: false, - }, - ], - }, - }, - }; +describe(`form group ${markAsSubmittedReducer.name}`, () => { + const INITIAL_STATE_FULL_SUBMITTED = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isSubmitted', true], ['isUnsubmitted', false]])); it('should mark itself and all children recursively as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); @@ -88,69 +42,39 @@ describe('form group markAsSubmittedReducer', () => { expect(resultState).toEqual(INITIAL_STATE_FULL_SUBMITTED); }); - it('should mark direct control children as submitted', () => { + it('should mark control children as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner.isSubmitted).toEqual(true); expect(resultState.controls.inner.isUnsubmitted).toEqual(false); }); - it('should mark direct group children as submitted', () => { + it('should mark group children as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isSubmitted).toEqual(true); expect(resultState.controls.inner3.isUnsubmitted).toEqual(false); }); - it('should mark direct array children as submitted', () => { + it('should mark array children as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner5.isSubmitted).toEqual(true); expect(resultState.controls.inner5.isUnsubmitted).toEqual(false); }); - it('should mark nested children in group as submitted', () => { - const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner3)!.controls.inner4.isSubmitted).toBe(true); - expect(cast(resultState.controls.inner3)!.controls.inner4.isUnsubmitted).toBe(false); - }); - - it('should mark nested children in array as submitted', () => { - const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner5)!.controls[0].isSubmitted).toBe(true); - expect(cast(resultState.controls.inner5)!.controls[0].isUnsubmitted).toBe(false); - }); - - it('should mark state as submitted if direct control child is marked as submitted', () => { + it('should mark state as submitted if control child is marked as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_INNER_ID)); expect(resultState.isSubmitted).toEqual(true); expect(resultState.isUnsubmitted).toEqual(false); }); - it('should mark state as submitted if direct group child is marked as submitted', () => { + it('should mark state as submitted if group child is marked as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isSubmitted).toEqual(true); expect(resultState.isUnsubmitted).toEqual(false); }); - it('should mark state as submitted if direct array child is marked as submitted', () => { + it('should mark state as submitted if array child is marked as submitted', () => { const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isSubmitted).toEqual(true); expect(resultState.isUnsubmitted).toEqual(false); }); - - it('should mark state as submitted if nested child in group is marked as submitted', () => { - const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isSubmitted).toEqual(true); - expect(resultState.isUnsubmitted).toEqual(false); - }); - - it('should mark state as submitted if nested child in array is marked as submitted', () => { - const resultState = markAsSubmittedReducer(INITIAL_STATE_FULL, new MarkAsSubmittedAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isSubmitted).toEqual(true); - expect(resultState.isUnsubmitted).toEqual(false); - }); - - it('should forward actions to children', () => { - const resultState = markAsSubmittedReducer(INITIAL_STATE, new MarkAsSubmittedAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isSubmitted).toEqual(true); - expect(resultState.controls.inner.isUnsubmitted).toEqual(false); - }); }); diff --git a/src/group/reducer/mark-as-touched.spec.ts b/src/group/reducer/mark-as-touched.spec.ts index 20831fe6..8eaa1abb 100644 --- a/src/group/reducer/mark-as-touched.spec.ts +++ b/src/group/reducer/mark-as-touched.spec.ts @@ -1,64 +1,18 @@ import { MarkAsTouchedAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { markAsTouchedReducer } from './mark-as-touched'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsTouchedReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - const INITIAL_STATE_FULL_INNER3 = cast(INITIAL_STATE_FULL.controls.inner3)!; - const INITIAL_STATE_FULL_INNER5 = cast(INITIAL_STATE_FULL.controls.inner5)!; - const INITIAL_STATE_FULL_TOUCHED = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isTouched: true, - isUntouched: false, - }, - inner2: { - ...INITIAL_STATE_FULL.controls.inner2, - isTouched: true, - isUntouched: false, - }, - inner3: { - ...INITIAL_STATE_FULL_INNER3, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL_INNER3.controls, - inner4: { - ...INITIAL_STATE_FULL_INNER3.controls.inner4, - isTouched: true, - isUntouched: false, - }, - }, - }, - inner5: { - ...INITIAL_STATE_FULL_INNER5, - isTouched: true, - isUntouched: false, - controls: [ - { - ...INITIAL_STATE_FULL_INNER5.controls[0], - isTouched: true, - isUntouched: false, - }, - ], - }, - }, - }; +describe(`form group ${markAsTouchedReducer.name}`, () => { + const INITIAL_STATE_FULL_TOUCHED = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isTouched', true], ['isUntouched', false]])); it('should mark itself and all children recursively as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); @@ -88,69 +42,39 @@ describe('form group markAsTouchedReducer', () => { expect(resultState).toEqual(INITIAL_STATE_FULL_TOUCHED); }); - it('should mark direct control children as touched', () => { + it('should mark control children as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner.isTouched).toEqual(true); expect(resultState.controls.inner.isUntouched).toEqual(false); }); - it('should mark direct group children as touched', () => { + it('should mark group children as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isTouched).toEqual(true); expect(resultState.controls.inner3.isUntouched).toEqual(false); }); - it('should mark direct array children as touched', () => { + it('should mark array children as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner5.isTouched).toEqual(true); expect(resultState.controls.inner5.isUntouched).toEqual(false); }); - it('should mark nested children in group as touched', () => { - const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner3)!.controls.inner4.isTouched).toBe(true); - expect(cast(resultState.controls.inner3)!.controls.inner4.isUntouched).toBe(false); - }); - - it('should mark nested children in array as touched', () => { - const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner5)!.controls[0].isTouched).toBe(true); - expect(cast(resultState.controls.inner5)!.controls[0].isUntouched).toBe(false); - }); - - it('should mark state as touched if direct control child is marked as touched', () => { + it('should mark state as touched if control child is marked as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_INNER_ID)); expect(resultState.isTouched).toEqual(true); expect(resultState.isUntouched).toEqual(false); }); - it('should mark state as touched if direct group child is marked as touched', () => { + it('should mark state as touched if group child is marked as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isTouched).toEqual(true); expect(resultState.isUntouched).toEqual(false); }); - it('should mark state as touched if direct array child is marked as touched', () => { + it('should mark state as touched if array child is marked as touched', () => { const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isTouched).toEqual(true); expect(resultState.isUntouched).toEqual(false); }); - - it('should mark state as touched if nested child in group is marked as touched', () => { - const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isTouched).toEqual(true); - expect(resultState.isUntouched).toEqual(false); - }); - - it('should mark state as touched if nested child in array is marked as touched', () => { - const resultState = markAsTouchedReducer(INITIAL_STATE_FULL, new MarkAsTouchedAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isTouched).toEqual(true); - expect(resultState.isUntouched).toEqual(false); - }); - - it('should forward actions to children', () => { - const resultState = markAsTouchedReducer(INITIAL_STATE, new MarkAsTouchedAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isTouched).toEqual(true); - expect(resultState.controls.inner.isUntouched).toEqual(false); - }); }); diff --git a/src/group/reducer/mark-as-unsubmitted.spec.ts b/src/group/reducer/mark-as-unsubmitted.spec.ts index 02e797bd..9cf2dbb9 100644 --- a/src/group/reducer/mark-as-unsubmitted.spec.ts +++ b/src/group/reducer/mark-as-unsubmitted.spec.ts @@ -1,20 +1,18 @@ import { MarkAsUnsubmittedAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { markAsUnsubmittedReducer } from './mark-as-unsubmitted'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsUnsubmittedReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - +describe(`form group ${markAsUnsubmittedReducer.name}`, () => { it('should update state if submitted', () => { const state = { ...INITIAL_STATE, isSubmitted: true, isUnsubmitted: false }; const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); @@ -27,309 +25,70 @@ describe('form group markAsUnsubmittedReducer', () => { expect(resultState).toBe(INITIAL_STATE); }); - it('should mark direct control children as unsubmitted', () => { - const state = { - ...INITIAL_STATE, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE.controls, - inner: { - ...INITIAL_STATE.controls.inner, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }; + it('should mark control children as unsubmitted', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isSubmitted', true], ['isUnsubmitted', false]])); const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner.isSubmitted).toEqual(false); expect(resultState.controls.inner.isUnsubmitted).toEqual(true); }); - it('should mark direct group children as unsubmitted', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isSubmitted: true, - isUnsubmitted: false, - controls: { - inner4: { - ...inner3State.controls.inner4, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }, - }, - }; + it('should mark group children as unsubmitted', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isSubmitted', true], ['isUnsubmitted', false]])); const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isSubmitted).toEqual(false); expect(resultState.controls.inner3.isUnsubmitted).toEqual(true); }); - it('should mark direct array children as unsubmitted', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isSubmitted: true, - isUnsubmitted: false, - controls: [ - { - ...inner5State.controls[0], - isSubmitted: true, - isUnsubmitted: false, - }, - ], - }, - }, - }; + it('should mark array children as unsubmitted', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isSubmitted', true], ['isUnsubmitted', false]])); const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isSubmitted).toEqual(false); expect(resultState.controls.inner3.isUnsubmitted).toEqual(true); }); - it('should mark nested children in group as unsubmitted', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }, - }, - }; - const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner3)!.controls.inner4.isSubmitted).toBe(false); - expect(cast(resultState.controls.inner3)!.controls.inner4.isUnsubmitted).toBe(true); - }); - - it('should mark nested children in array as unsubmitted', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isSubmitted: true, - isUnsubmitted: false, - controls: [ - { - ...inner5State.controls[0], - isSubmitted: true, - isUnsubmitted: false, - }, - ], - }, - }, - }; - const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner5)!.controls[0].isSubmitted).toBe(false); - expect(cast(resultState.controls.inner5)!.controls[0].isUnsubmitted).toBe(true); - }); - - it('should mark state as unsubmitted if all children are unsubmitted when direct control child is updated', () => { - const state = { - ...INITIAL_STATE, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE.controls, - inner: { - ...INITIAL_STATE.controls.inner, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }; + it('should mark state as unsubmitted if all children are unsubmitted when control child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isSubmitted', true], ['isUnsubmitted', false]], + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + )); const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER_ID)); expect(resultState.isSubmitted).toEqual(false); expect(resultState.isUnsubmitted).toEqual(true); }); - it('should not mark state as unsubmitted if not all children are unsubmitted when direct control child is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isSubmitted: true, - isUnsubmitted: false, - }, - inner3: { - ...INITIAL_STATE_FULL.controls.inner3, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }, - }, - }; + it('should not mark state as unsubmitted if not all children are unsubmitted when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isSubmitted', true], ['isUnsubmitted', false]])); const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER_ID)); expect(resultState.isSubmitted).toEqual(true); expect(resultState.isUnsubmitted).toEqual(false); }); - it('should mark state as unsubmitted if all children are unsubmitted when direct group child is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isSubmitted: true, - isUnsubmitted: false, - controls: { - inner4: { - ...inner3State.controls.inner4, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }, - }, - }; + it('should mark state as unsubmitted if all children are unsubmitted when group child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isSubmitted', true], ['isUnsubmitted', false]], + FORM_CONTROL_INNER_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER5_ID, + )); const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isSubmitted).toEqual(false); expect(resultState.isUnsubmitted).toEqual(true); }); - it('should mark state as unsubmitted if all children are unsubmitted when direct array child is updated', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isSubmitted: true, - isUnsubmitted: false, - controls: [ - { - ...inner5State.controls[0], - isSubmitted: true, - isUnsubmitted: false, - }, - ], - }, - }, - }; + it('should mark state as unsubmitted if all children are unsubmitted when array child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isSubmitted', true], ['isUnsubmitted', false]], + FORM_CONTROL_INNER_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + )); const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isSubmitted).toEqual(false); expect(resultState.isUnsubmitted).toEqual(true); }); - - it('should mark state as unsubmitted if all children are unsubmitted when nested child in group is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }, - }, - }; - const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isSubmitted).toEqual(false); - expect(resultState.isUnsubmitted).toEqual(true); - }); - - it('should mark state as unsubmitted if all children are unsubmitted when nested child in array is updated', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isSubmitted: true, - isUnsubmitted: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isSubmitted: true, - isUnsubmitted: false, - controls: [ - { - ...inner5State.controls[0], - isSubmitted: true, - isUnsubmitted: false, - }, - ], - }, - }, - }; - const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isSubmitted).toEqual(false); - expect(resultState.isUnsubmitted).toEqual(true); - }); - - it('should forward actions to children', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isSubmitted: true, - isUnsubmitted: false, - }, - }, - }; - const resultState = markAsUnsubmittedReducer(state, new MarkAsUnsubmittedAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isSubmitted).toEqual(false); - expect(resultState.controls.inner.isUnsubmitted).toEqual(true); - }); }); diff --git a/src/group/reducer/mark-as-untouched.spec.ts b/src/group/reducer/mark-as-untouched.spec.ts index 82aed083..d0feb351 100644 --- a/src/group/reducer/mark-as-untouched.spec.ts +++ b/src/group/reducer/mark-as-untouched.spec.ts @@ -1,20 +1,18 @@ import { MarkAsUntouchedAction } from '../../actions'; import { cast, createFormGroupState } from '../../state'; import { markAsUntouchedReducer } from './mark-as-untouched'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, + setPropertiesRecursively, +} from './test-util'; -describe('form group markAsUntouchedReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - +describe(`form group ${markAsUntouchedReducer.name}`, () => { it('should update state if touched', () => { const state = { ...INITIAL_STATE, isTouched: true, isUntouched: false }; const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); @@ -27,310 +25,70 @@ describe('form group markAsUntouchedReducer', () => { expect(resultState).toBe(INITIAL_STATE); }); - it('should mark direct control children as untouched', () => { - const state = { - ...INITIAL_STATE, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE.controls, - inner: { - ...INITIAL_STATE.controls.inner, - isTouched: true, - isUntouched: false, - }, - }, - }; + it('should mark control children as untouched', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isTouched', true], ['isUntouched', false]])); const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner.isTouched).toEqual(false); expect(resultState.controls.inner.isUntouched).toEqual(true); }); - it('should mark direct group children as untouched', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isTouched: true, - isUntouched: false, - controls: { - inner4: { - ...inner3State.controls.inner4, - isTouched: true, - isUntouched: false, - }, - }, - }, - }, - }; + it('should mark group children as untouched', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isTouched', true], ['isUntouched', false]])); const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner3.isTouched).toEqual(false); expect(resultState.controls.inner3.isUntouched).toEqual(true); }); - it('should mark direct array children as untouched', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isTouched: true, - isUntouched: false, - controls: [ - { - ...inner5State.controls[0], - isTouched: true, - isUntouched: false, - }, - ], - }, - }, - }; + it('should mark array children as untouched', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isTouched', true], ['isUntouched', false]])); const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); expect(resultState.controls.inner5.isTouched).toEqual(false); expect(resultState.controls.inner5.isUntouched).toEqual(true); }); - it('should mark nested children in group as untouched', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isTouched: true, - isUntouched: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isTouched: true, - isUntouched: false, - }, - }, - }, - }, - }; - const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner3)!.controls.inner4.isTouched).toBe(false); - expect(cast(resultState.controls.inner3)!.controls.inner4.isUntouched).toBe(true); - }); - - it('should mark nested children in array as untouched', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isTouched: true, - isUntouched: false, - controls: [ - { - ...inner5State.controls[0], - isTouched: true, - isUntouched: false, - }, - ], - }, - }, - }; - const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_ID)); - expect(cast(resultState.controls.inner5)!.controls[0].isTouched).toBe(false); - expect(cast(resultState.controls.inner5)!.controls[0].isUntouched).toBe(true); - }); - - it('should mark state as untouched if all children are untouched when direct control child is updated', () => { - const state = { - ...INITIAL_STATE, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE.controls, - inner: { - ...INITIAL_STATE.controls.inner, - isTouched: true, - isUntouched: false, - }, - }, - }; + it('should mark state as untouched if all children are untouched when control child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isTouched', true], ['isUntouched', false]], + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER5_ID, + )); const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER_ID)); expect(resultState.isTouched).toEqual(false); expect(resultState.isUntouched).toEqual(true); }); - it('should not mark state as untouched if not all children are untouched when direct control child is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isTouched: true, - isUntouched: false, - }, - inner3: { - ...INITIAL_STATE_FULL.controls.inner3, - isTouched: true, - isUntouched: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isTouched: true, - isUntouched: false, - }, - }, - }, - }, - }; + it('should not mark state as untouched if not all children are untouched when control child is updated', () => { + const state = cast(setPropertiesRecursively(INITIAL_STATE_FULL, [['isTouched', true], ['isUntouched', false]])); const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER_ID)); expect(resultState.isTouched).toEqual(true); expect(resultState.isUntouched).toEqual(false); }); - it('should mark state as untouched if all children are untouched when direct group child is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isTouched: true, - isUntouched: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isTouched: true, - isUntouched: false, - }, - }, - }, - }, - }; + it('should mark state as untouched if all children are untouched when group child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isTouched', true], ['isUntouched', false]], + FORM_CONTROL_INNER_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER5_ID, + )); const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER3_ID)); expect(resultState.isTouched).toEqual(false); expect(resultState.isUntouched).toEqual(true); }); - it('should mark state as untouched if all children are untouched when direct array child is updated', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isTouched: true, - isUntouched: false, - controls: [ - { - ...inner5State.controls[0], - isTouched: true, - isUntouched: false, - }, - ], - }, - }, - }; + it('should mark state as untouched if all children are untouched when array child is updated', () => { + const state = cast(setPropertiesRecursively( + INITIAL_STATE_FULL, + [['isTouched', true], ['isUntouched', false]], + FORM_CONTROL_INNER_ID, + FORM_CONTROL_INNER2_ID, + FORM_CONTROL_INNER3_ID, + )); const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER5_ID)); expect(resultState.isTouched).toEqual(false); expect(resultState.isUntouched).toEqual(true); }); - - it('should mark state as untouched if all children are untouched when nested child in group is updated', () => { - const inner3State = cast(INITIAL_STATE_FULL.controls.inner3)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner3: { - ...inner3State, - isTouched: true, - isUntouched: false, - controls: { - ...inner3State.controls, - inner4: { - ...inner3State.controls.inner4, - isTouched: true, - isUntouched: false, - }, - }, - }, - }, - }; - const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER4_ID)); - expect(resultState.isTouched).toEqual(false); - expect(resultState.isUntouched).toEqual(true); - }); - - it('should mark state as untouched if all children are untouched when nested child in array is updated', () => { - const inner5State = cast(INITIAL_STATE_FULL.controls.inner5)!; - const state = { - ...INITIAL_STATE_FULL, - isTouched: true, - isUntouched: false, - controls: { - ...INITIAL_STATE_FULL.controls, - inner5: { - ...inner5State, - isTouched: true, - isUntouched: false, - controls: [ - { - ...inner5State.controls[0], - isTouched: true, - isUntouched: false, - }, - ], - }, - }, - }; - const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER5_0_ID)); - expect(resultState.isTouched).toEqual(false); - expect(resultState.isUntouched).toEqual(true); - }); - - it('should forward actions to children', () => { - const state = { - ...INITIAL_STATE_FULL, - controls: { - ...INITIAL_STATE_FULL.controls, - inner: { - ...INITIAL_STATE_FULL.controls.inner, - isTouched: true, - isUntouched: false, - }, - }, - }; - const resultState = markAsUntouchedReducer(state, new MarkAsUntouchedAction(FORM_CONTROL_INNER_ID)); - expect(resultState.controls.inner.isTouched).toEqual(false); - expect(resultState.controls.inner.isUntouched).toEqual(true); - }); }); diff --git a/src/group/reducer/remove-control.spec.ts b/src/group/reducer/remove-control.spec.ts index f2fbde16..dea2f12e 100644 --- a/src/group/reducer/remove-control.spec.ts +++ b/src/group/reducer/remove-control.spec.ts @@ -1,15 +1,9 @@ -import { createFormGroupState } from '../../state'; import { RemoveControlAction } from '../../actions'; +import { createFormGroupState } from '../../state'; import { removeControlReducer } from './remove-control'; +import { FORM_CONTROL_ID, FormGroupValue, INITIAL_STATE, INITIAL_STATE_FULL } from './test-util'; -describe('form group removeControlReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - +describe(`form group ${removeControlReducer.name}`, () => { it('should remove child state', () => { const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner2'); const resultState = removeControlReducer(INITIAL_STATE_FULL, action); @@ -48,7 +42,7 @@ describe('form group removeControlReducer', () => { }, }, }; - const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner'); + const action = new RemoveControlAction(id, 'inner'); const resultState = removeControlReducer(state, action); expect(resultState.value).toEqual({}); expect(resultState.errors).toEqual({}); @@ -73,7 +67,7 @@ describe('form group removeControlReducer', () => { }, }, }; - const action = new RemoveControlAction(FORM_CONTROL_ID, 'inner'); + const action = new RemoveControlAction(id, 'inner'); const resultState = removeControlReducer(state, action); expect(resultState.value).toEqual({}); expect(resultState.errors).toEqual(errors); diff --git a/src/group/reducer/set-errors.spec.ts b/src/group/reducer/set-errors.spec.ts index fa187274..31ea3b80 100644 --- a/src/group/reducer/set-errors.spec.ts +++ b/src/group/reducer/set-errors.spec.ts @@ -1,20 +1,17 @@ -import { createFormGroupState } from '../../state'; import { SetErrorsAction } from '../../actions'; import { setErrorsReducer } from './set-errors'; +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER4_ID, + FORM_CONTROL_INNER5_0_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + INITIAL_STATE, + INITIAL_STATE_FULL, +} from './test-util'; -describe('form group setErrorsReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); - +describe(`form group ${setErrorsReducer.name}`, () => { it('should update state if there are errors', () => { const errors = { required: true }; const resultState = setErrorsReducer(INITIAL_STATE, new SetErrorsAction(FORM_CONTROL_ID, errors)); diff --git a/src/group/reducer/set-user-defined-property.spec.ts b/src/group/reducer/set-user-defined-property.spec.ts index cb118684..5c3e4431 100644 --- a/src/group/reducer/set-user-defined-property.spec.ts +++ b/src/group/reducer/set-user-defined-property.spec.ts @@ -1,13 +1,8 @@ -import { createFormGroupState } from '../../state'; import { SetUserDefinedPropertyAction } from '../../actions'; import { setUserDefinedPropertyReducer } from './set-user-defined-property'; +import { FORM_CONTROL_ID, INITIAL_STATE } from './test-util'; describe(`form group ${setUserDefinedPropertyReducer.name}`, () => { - const FORM_CONTROL_ID = 'test ID'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - it('should skip any actionof the wrong type', () => expect(setUserDefinedPropertyReducer(INITIAL_STATE, { type: '' } as any)).toBe(INITIAL_STATE)); diff --git a/src/group/reducer/set-value.spec.ts b/src/group/reducer/set-value.spec.ts index 1b3f7907..49e1a83c 100644 --- a/src/group/reducer/set-value.spec.ts +++ b/src/group/reducer/set-value.spec.ts @@ -1,18 +1,18 @@ import { SetValueAction } from '../../actions'; import { cast, createFormGroupState } from '../../state'; import { setValueReducer } from './set-value'; - -describe('form group setValueReducer', () => { - const FORM_CONTROL_ID = 'test ID'; - const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; - const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; - const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; - const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; - const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; - interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; - const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); - +import { + FORM_CONTROL_ID, + FORM_CONTROL_INNER3_ID, + FORM_CONTROL_INNER4_ID, + FORM_CONTROL_INNER5_0_ID, + FORM_CONTROL_INNER5_ID, + FORM_CONTROL_INNER_ID, + FormGroupValue, + INITIAL_STATE, +} from './test-util'; + +describe(`form group ${setValueReducer.name}`, () => { it('should update state value if different', () => { const value = { inner: 'A' }; const resultState = setValueReducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, value)); diff --git a/src/group/reducer/test-util.ts b/src/group/reducer/test-util.ts new file mode 100644 index 00000000..3ae5eed9 --- /dev/null +++ b/src/group/reducer/test-util.ts @@ -0,0 +1,71 @@ +import { + AbstractControlState, + createFormGroupState, + FormArrayState, + FormControlState, + FormGroupControls, + FormGroupState, + isArrayState, + isGroupState, +} from '../../state'; + +export const FORM_CONTROL_ID = 'test ID'; +export const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; +export const FORM_CONTROL_INNER2_ID = FORM_CONTROL_ID + '.inner2'; +export const FORM_CONTROL_INNER3_ID = FORM_CONTROL_ID + '.inner3'; +export const FORM_CONTROL_INNER4_ID = FORM_CONTROL_INNER3_ID + '.inner4'; +export const FORM_CONTROL_INNER5_ID = FORM_CONTROL_ID + '.inner5'; +export const FORM_CONTROL_INNER5_0_ID = FORM_CONTROL_ID + '.inner5.0'; +export interface FormGroupValue { inner: string; inner2?: string; inner3?: { inner4: string }; inner5?: string[]; } +export const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '' }; +export const INITIAL_FORM_CONTROL_VALUE_FULL: FormGroupValue = { inner: '', inner2: '', inner3: { inner4: '' }, inner5: [''] }; +export const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); +export const INITIAL_STATE_FULL = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE_FULL); + +export const setPropertyRecursively = ( + state: AbstractControlState, + property: keyof AbstractControlState, + value: any, + ...excludeIds: string[], +): AbstractControlState => { + if (excludeIds.indexOf(state.id) >= 0) { + return state; + } + + state = { + ...state, + [property]: value, + }; + + if (isArrayState(state)) { + const controls = state.controls.map(s => setPropertyRecursively(s, property, value, ...excludeIds)); + return { + ...state, + controls, + } as any; + } + + if (isGroupState(state)) { + const controls = state.controls; + const newControls = Object.keys(controls).reduce((res, key) => { + const s = setPropertyRecursively(controls[key], property, value, ...excludeIds); + res[key] = s; + return res; + }, {} as FormGroupControls); + + return { + ...state, + controls: newControls, + } as any; + } + + return state; +}; + +export const setPropertiesRecursively = ( + state: AbstractControlState, + properties: Array<[keyof AbstractControlState, any]>, + ...excludeIds: string[], +): AbstractControlState => { + return properties.reduce((s, [p, v]) => setPropertyRecursively(s, p, v, ...excludeIds), state); +}; diff --git a/src/group/reducer/util.ts b/src/group/reducer/util.ts index d004633d..2ec0fe38 100644 --- a/src/group/reducer/util.ts +++ b/src/group/reducer/util.ts @@ -1,7 +1,15 @@ import { Actions } from '../../actions'; import { formArrayReducerInternal } from '../../array/reducer'; import { formControlReducerInternal } from '../../control/reducer'; -import { AbstractControlState, FormGroupControls, FormGroupState, KeyValue, ValidationErrors } from '../../state'; +import { + AbstractControlState, + FormGroupControls, + FormGroupState, + isArrayState, + isGroupState, + KeyValue, + ValidationErrors, +} from '../../state'; import { isEmpty } from '../../util'; import { formGroupReducerInternal } from '../reducer'; @@ -76,14 +84,6 @@ export function computeGroupState( }; } -export function isArrayState(state: AbstractControlState): boolean { - return state.hasOwnProperty('controls') && Array.isArray((state as any).controls); -} - -export function isGroupState(state: AbstractControlState): boolean { - return state.hasOwnProperty('controls'); -} - export function callChildReducer( state: AbstractControlState, action: Actions, diff --git a/src/state.ts b/src/state.ts index 50406f33..5e7df759 100644 --- a/src/state.ts +++ b/src/state.ts @@ -35,6 +35,14 @@ export class FormArrayState extends AbstractControlState { readonly controls: Array>; } +export function isArrayState(state: AbstractControlState): state is FormArrayState { + return state.hasOwnProperty('controls') && Array.isArray((state as any).controls); +} + +export function isGroupState(state: AbstractControlState): state is FormGroupState { + return state.hasOwnProperty('controls'); +} + export function cast( state: AbstractControlState, ): FormControlState; From 8bb34dd94db9970741165a585ddc6ab6760375b3 Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sat, 28 Oct 2017 21:48:12 +0200 Subject: [PATCH 04/11] build: make test output more succinct --- karma.conf.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/karma.conf.js b/karma.conf.js index 6e3b608e..db9bdcf7 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -60,6 +60,16 @@ module.exports = function (config) { // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['spec'], + specReporter: { + maxLogLines: 10, + suppressErrorSummary: true, + suppressFailed: false, + suppressPassed: true, + suppressSkipped: true, + showSpecTiming: false, + failFast: false + }, + // web server port port: 9876, From f30d01453d445a0388fb96cc6faa388c2897e39f Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sat, 28 Oct 2017 22:08:09 +0200 Subject: [PATCH 05/11] refactor: fix tslint errors --- src/array/reducer/mark-as-submitted.ts | 4 ++-- src/array/reducer/test-util.ts | 2 +- src/group/reducer/mark-as-dirty.spec.ts | 2 +- src/group/reducer/mark-as-untouched.spec.ts | 2 +- src/group/reducer/test-util.ts | 11 +---------- src/ngrx-forms.ts | 1 + src/update-functions.ts | 3 ++- 7 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/array/reducer/mark-as-submitted.ts b/src/array/reducer/mark-as-submitted.ts index 8d8ca4ee..c22639a4 100644 --- a/src/array/reducer/mark-as-submitted.ts +++ b/src/array/reducer/mark-as-submitted.ts @@ -1,6 +1,6 @@ -import { FormArrayState, KeyValue } from '../../state'; import { Actions, MarkAsSubmittedAction } from '../../actions'; -import { computeArrayState, dispatchActionPerChild, childReducer } from './util'; +import { FormArrayState } from '../../state'; +import { childReducer, computeArrayState, dispatchActionPerChild } from './util'; export function markAsSubmittedReducer( state: FormArrayState, diff --git a/src/array/reducer/test-util.ts b/src/array/reducer/test-util.ts index 5faf5366..e93caae5 100644 --- a/src/array/reducer/test-util.ts +++ b/src/array/reducer/test-util.ts @@ -1,4 +1,4 @@ -import { AbstractControlState, createFormArrayState, FormArrayState, FormControlState, FormGroupState, isArrayState, isGroupState } from '../../state'; +import { AbstractControlState, createFormArrayState, isArrayState, isGroupState } from '../../state'; export const FORM_CONTROL_ID = 'test ID'; export const FORM_CONTROL_0_ID = FORM_CONTROL_ID + '.0'; diff --git a/src/group/reducer/mark-as-dirty.spec.ts b/src/group/reducer/mark-as-dirty.spec.ts index e45df7f8..2d05f7fa 100644 --- a/src/group/reducer/mark-as-dirty.spec.ts +++ b/src/group/reducer/mark-as-dirty.spec.ts @@ -1,5 +1,5 @@ import { MarkAsDirtyAction } from '../../actions'; -import { cast, FormGroupState } from '../../state'; +import { cast } from '../../state'; import { markAsDirtyReducer } from './mark-as-dirty'; import { FORM_CONTROL_ID, diff --git a/src/group/reducer/mark-as-untouched.spec.ts b/src/group/reducer/mark-as-untouched.spec.ts index d0feb351..62bf5f88 100644 --- a/src/group/reducer/mark-as-untouched.spec.ts +++ b/src/group/reducer/mark-as-untouched.spec.ts @@ -1,5 +1,5 @@ import { MarkAsUntouchedAction } from '../../actions'; -import { cast, createFormGroupState } from '../../state'; +import { cast } from '../../state'; import { markAsUntouchedReducer } from './mark-as-untouched'; import { FORM_CONTROL_ID, diff --git a/src/group/reducer/test-util.ts b/src/group/reducer/test-util.ts index 3ae5eed9..48e7528a 100644 --- a/src/group/reducer/test-util.ts +++ b/src/group/reducer/test-util.ts @@ -1,13 +1,4 @@ -import { - AbstractControlState, - createFormGroupState, - FormArrayState, - FormControlState, - FormGroupControls, - FormGroupState, - isArrayState, - isGroupState, -} from '../../state'; +import { AbstractControlState, createFormGroupState, FormGroupControls, isArrayState, isGroupState } from '../../state'; export const FORM_CONTROL_ID = 'test ID'; export const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; diff --git a/src/ngrx-forms.ts b/src/ngrx-forms.ts index 13a40a09..8c0f705b 100644 --- a/src/ngrx-forms.ts +++ b/src/ngrx-forms.ts @@ -2,6 +2,7 @@ export * from './actions'; export * from './state'; export { formControlReducer } from './control/reducer'; export { formGroupReducer } from './group/reducer'; +export { formArrayReducer } from './array/reducer'; export { NgrxFormControlDirective } from './control/directive'; export { NgrxValueConverter, NgrxValueConverters } from './control/value-converter'; export { NgrxFormDirective } from './group/directive'; diff --git a/src/update-functions.ts b/src/update-functions.ts index 32a53875..c4cb8dbd 100644 --- a/src/update-functions.ts +++ b/src/update-functions.ts @@ -19,13 +19,14 @@ import { } from './actions'; import { formControlReducer } from './control/reducer'; import { formGroupReducer } from './group/reducer'; -import { computeGroupState, isGroupState } from './group/reducer/util'; +import { computeGroupState } from './group/reducer/util'; import { AbstractControlState, FormControlState, FormControlValueTypes, FormGroupControls, FormGroupState, + isGroupState, KeyValue, ValidationErrors, } from './state'; From eb76d63a10cb6456b1e370b82d924d7c6c6d4b54 Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sat, 28 Oct 2017 22:08:35 +0200 Subject: [PATCH 06/11] docs: add user guide entry for form arrays --- docs/FORM_ARRAYS.md | 74 +++++++++++++++++++++++++++++++++++++++++++++ docs/FORM_GROUPS.md | 2 +- docs/USER_GUIDE.md | 2 ++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 docs/FORM_ARRAYS.md diff --git a/docs/FORM_ARRAYS.md b/docs/FORM_ARRAYS.md new file mode 100644 index 00000000..77577522 --- /dev/null +++ b/docs/FORM_ARRAYS.md @@ -0,0 +1,74 @@ +## Form Arrays + +Form arrays are collections of controls. They are represented as plain state arrays. The state of an array is determined almost fully by its child controls (with the exception of `errors` which an array can have by itself). Array states have the following shape: + +```typescript +export class FormArrayState extends AbstractControlState { + readonly controls: Array>; +} +``` + +As you can see most properties are shared with controls via the common base interface `AbstractControlState`. The following table explains each property in the context of an array. + +|Property|Negated|Description| +|-|-|-| +|`id`||The unique ID of the array.| +|`value`||The aggregated value of the array. The value is computed by aggregating the values of all children and is itself an array.| +|`isValid`|`isInvalid`|The `isValid` flag is `true` if the array does not have any errors itself and none of its children have any errors.| +|`errors`||The errors of the array. This property is computed by merging the errors of the control with the errors of all children where the child errors are a property of the `errors` object prefixed with an underscore (e.g. `{ arrayError: true, _0: { childError: true } }`). If neither the array nor any children have errors the property is set to `{}`.| +|`isEnabled`|`isDisabled`|The `isEnabled` flag is `true` if and only if at least one child control is enabled.| +|`isDirty`|`isPristine`|The `isDirty` flag is `true` if and only if at least one child control is marked as dirty.| +|`isTouched`|`isUntouched`|The `isTouched` flag is `true` if and only if at least one child control is marked as touched.| +|`isSubmitted`|`isUnsubmitted`|The `isSubmitted` flag is set to `true` if the containing group is submitted.| +|`controls`||This property contains all child controls of the array. As you may have noticed the type of each child control is `AbstractControlState` which sometimes forces you to cast the state explicitly. It is not possible to improve this typing until [conditional mapped types](https://github.com/Microsoft/TypeScript/issues/12424) are added to TypeScript.| +|`userDefinedProperties`||`userDefinedProperties` work the same for arrays as they do for controls.| + +Array states are completely independent of the DOM. They are updated by intercepting all actions that change their children (i.e. the array's reducer is the parent reducer of all its child reducers and forwards any actions to all children; if any children change it recomputes the state of the array). An array state can be created via `createFormArrayState`. This function takes an initial value and automatically creates all child controls recursively. + +#### Dynamic Form Arrays + +Sometimes you will have to render a variable number of fields in your form. Form arrays support adding and removing controls dynamically. This is done by setting the value of the form array via `setValue` which will automatically update the form array based on the value you provide. + +Below you can find an example of how this would look. Assume that we have an action that provides a variable set of objects which each should be mapped to an array with two form controls. + +```typescript +import { Action } from '@ngrx/store'; +import { FormArrayState, setValue, updateGroup, cast } from 'ngrx-forms'; + +interface DynamicObject { + someNumber: number; + someCheckbox: boolean; +} + +interface DynamicObjectFormValue { + someNumber: number; + someCheckbox: boolean; +} + +interface SetDynamicObjectsAction extends Action { + type: 'SET_DYNAMIC_OBJECTS'; + objects: DynamicObject[]; +} + +interface AppState { + someOtherState: string; + someOtherNumber: number; + dynamicFormArray: FormArrayState; +} + +export function appReducer(state: AppState, action: Action): AppState { + switch (action.type) { + case 'SET_DYNAMIC_OBJECTS': { + const newFormValue: DynamicObjectFormValue[] = (action as SetDynamicObjectsAction).objects; + + // the `setValue` will add and remove controls as required; existing controls that are still + // present get their value updated but are otherwise kept in the same state as before + const dynamicFormArray = cast(setValue(newFormValue, state.dynamicFormArray)); + return { ...state, dynamicFormArray }; + } + + default: + return state; + } +} +``` diff --git a/docs/FORM_GROUPS.md b/docs/FORM_GROUPS.md index d396729c..c218dcf9 100644 --- a/docs/FORM_GROUPS.md +++ b/docs/FORM_GROUPS.md @@ -1,6 +1,6 @@ ## Form Groups -Groups are collections of controls. Just like controls groups are represented as plain state objects. The state of a group is determined almost fully by its child controls (with the exception of `errors` which a group can have by itself). Group states have the following shape: +Groups are collections of named controls. Just like controls groups are represented as plain state objects. The state of a group is determined almost fully by its child controls (with the exception of `errors` which a group can have by itself). Group states have the following shape: ```typescript export interface KeyValue { [key: string]: any; } diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 6722d202..e21ac0c7 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -9,6 +9,8 @@ The following sections will explain and showcase the different features of `ngrx * [Value Conversion](FORM_CONTROLS.md#value-conversion) * [Form Groups](FORM_GROUPS.md#form-groups) * [Dynamic Form Groups](FORM_GROUPS.md#dynamic-form-groups) +* [Form Arrays](FORM_ARRAYS.md#form-arrays) + * [Dynamic Form Arrays](FORM_ARRAYS.md#dynamic-form-arrays) * [Updating the State](UPDATING_THE_STATE.md#updating-the-state) * [Validation](VALIDATION.md#validation) * [Custom Controls](CUSTOM_CONTROLS.md#custom-controls) From c09a835a22d41c7f3471b2c25c5532cfbfafb93d Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sat, 28 Oct 2017 22:54:56 +0200 Subject: [PATCH 07/11] feat: adjust update functions to work for form arrays and add parameter sanity checks to reducers --- src/array/reducer.spec.ts | 4 ++ src/array/reducer.ts | 6 ++- src/control/reducer.spec.ts | 4 ++ src/control/reducer.ts | 6 ++- src/group/reducer.spec.ts | 4 ++ src/group/reducer.ts | 6 ++- src/state.ts | 2 +- src/update-functions.spec.ts | 102 +++++++++++++++++++++++++++++------ src/update-functions.ts | 12 ++++- 9 files changed, 126 insertions(+), 20 deletions(-) diff --git a/src/array/reducer.spec.ts b/src/array/reducer.spec.ts index 1cb1ee0e..45e1e03d 100644 --- a/src/array/reducer.spec.ts +++ b/src/array/reducer.spec.ts @@ -72,6 +72,10 @@ describe('form array reducer', () => { expect(() => formArrayReducerInternal(INITIAL_STATE, new MarkAsDirtyAction(FORM_CONTROL_ID))).not.toThrowError(); }); + it('should throw if state is not an array state', () => { + expect(() => formArrayReducerInternal(INITIAL_STATE.controls[0] as any, new MarkAsDirtyAction(FORM_CONTROL_ID))).toThrowError(); + }); + describe(SetValueAction.name, () => { it('should update state', () => { const resultState = formArrayReducerInternal(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, ['A'])); diff --git a/src/array/reducer.ts b/src/array/reducer.ts index 1b7313e4..1d539eb8 100644 --- a/src/array/reducer.ts +++ b/src/array/reducer.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { Actions, FocusAction, UnfocusAction } from '../actions'; -import { FormArrayState } from '../state'; +import { FormArrayState, isArrayState } from '../state'; import { disableReducer } from './reducer/disable'; import { enableReducer } from './reducer/enable'; import { markAsDirtyReducer } from './reducer/mark-as-dirty'; @@ -16,6 +16,10 @@ import { setValueReducer } from './reducer/set-value'; import { childReducer } from './reducer/util'; export function formArrayReducerInternal(state: FormArrayState, action: Actions) { + if (!isArrayState(state)) { + throw new Error('State must be array state'); + } + switch (action.type) { case FocusAction.TYPE: case UnfocusAction.TYPE: diff --git a/src/control/reducer.spec.ts b/src/control/reducer.spec.ts index 21512836..9ed9cd88 100644 --- a/src/control/reducer.spec.ts +++ b/src/control/reducer.spec.ts @@ -33,6 +33,10 @@ describe('form control reducer', () => { expect(resultState).toBe(INITIAL_STATE); }); + it('should throw if state is not a control state', () => { + expect(() => reducer({ controls: [] } as any, new MarkAsDirtyAction(FORM_CONTROL_ID))).toThrowError(); + }); + describe(SetValueAction.name, () => { it('should update state', () => { const resultState = reducer(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, 'A')); diff --git a/src/control/reducer.ts b/src/control/reducer.ts index 4a13dbf2..14dd8dcc 100644 --- a/src/control/reducer.ts +++ b/src/control/reducer.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { Actions } from '../actions'; -import { FormControlState, FormControlValueTypes } from '../state'; +import { FormControlState, FormControlValueTypes, isArrayState, isGroupState } from '../state'; import { disableReducer } from './reducer/disable'; import { enableReducer } from './reducer/enable'; import { focusReducer } from './reducer/focus'; @@ -20,6 +20,10 @@ export function formControlReducerInternal state: FormControlState, action: Actions, ): FormControlState { + if (isGroupState(state) || isArrayState(state)) { + throw new Error('State must be control state'); + } + if (action.controlId !== state.id) { return state; } diff --git a/src/group/reducer.spec.ts b/src/group/reducer.spec.ts index a987772b..fda5efc0 100644 --- a/src/group/reducer.spec.ts +++ b/src/group/reducer.spec.ts @@ -86,6 +86,10 @@ describe('form group reducer', () => { expect(() => formGroupReducerInternal(state, new SetValueAction(FORM_CONTROL_INNER_ID, new Date()))).toThrowError(); }); + it('should throw if state is not a group state', () => { + expect(() => formGroupReducerInternal(INITIAL_STATE.controls.inner as any, new MarkAsDirtyAction(FORM_CONTROL_ID))).toThrowError(); + }); + describe(SetValueAction.name, () => { it('should update state', () => { const resultState = formGroupReducerInternal(INITIAL_STATE, new SetValueAction(FORM_CONTROL_ID, { inner: 'A' })); diff --git a/src/group/reducer.ts b/src/group/reducer.ts index b05cc40f..9cf263a9 100644 --- a/src/group/reducer.ts +++ b/src/group/reducer.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { Actions, FocusAction, UnfocusAction } from '../actions'; -import { FormGroupState, KeyValue } from '../state'; +import { FormGroupState, KeyValue, isGroupState } from '../state'; import { addControlReducer } from './reducer/add-control'; import { disableReducer } from './reducer/disable'; import { enableReducer } from './reducer/enable'; @@ -18,6 +18,10 @@ import { setValueReducer } from './reducer/set-value'; import { childReducer } from './reducer/util'; export function formGroupReducerInternal(state: FormGroupState, action: Actions) { + if (!isGroupState(state)) { + throw new Error('State must be group state'); + } + switch (action.type) { case FocusAction.TYPE: case UnfocusAction.TYPE: diff --git a/src/state.ts b/src/state.ts index 5e7df759..8048bf1c 100644 --- a/src/state.ts +++ b/src/state.ts @@ -40,7 +40,7 @@ export function isArrayState(state: AbstractControlState): state is FormArr } export function isGroupState(state: AbstractControlState): state is FormGroupState { - return state.hasOwnProperty('controls'); + return state.hasOwnProperty('controls') && !Array.isArray((state as any).controls); } export function cast( diff --git a/src/update-functions.spec.ts b/src/update-functions.spec.ts index db875166..5d9734cc 100644 --- a/src/update-functions.spec.ts +++ b/src/update-functions.spec.ts @@ -24,12 +24,12 @@ describe('update functions', () => { const FORM_CONTROL_ID = 'test ID'; const FORM_CONTROL_INNER_ID = FORM_CONTROL_ID + '.inner'; interface NestedValue { inner4: string; } - interface FormGroupValue { inner: string; inner2?: string; inner3?: NestedValue; } - const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '', inner3: { inner4: '' } }; + interface FormGroupValue { inner: string; inner2?: string; inner3?: NestedValue; inner5: string[]; } + const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); describe(updateGroup.name, () => { - it('should apply the provided functions to direct control children', () => { + it('should apply the provided functions to control children', () => { const expected = { ...INITIAL_STATE.controls.inner, value: 'A' }; const resultState = updateGroup({ inner: () => expected, @@ -45,16 +45,6 @@ describe('update functions', () => { expect(resultState.controls.inner3).toBe(expected); }); - it('should apply the provided functions to nested children', () => { - const expected = { ...(INITIAL_STATE.controls.inner3 as FormGroupState).controls.inner4, value: 'A' }; - const resultState = updateGroup({ - inner3: updateGroup({ - inner4: () => expected, - }), - })(INITIAL_STATE); - expect((resultState.controls.inner3 as FormGroupState).controls.inner4).toBe(expected); - }); - it('should apply multiple provided function objects one after another', () => { const updatedInner1 = { ...INITIAL_STATE.controls.inner, value: 'A' }; const expectedInner1 = { ...INITIAL_STATE.controls.inner, value: 'B' }; @@ -80,7 +70,7 @@ describe('update functions', () => { }); describe(groupUpdateReducer.name, () => { - it('should apply the action and the provided functions to direct control children', () => { + it('should apply the action and the provided functions to control children', () => { const value = 'A'; const resultState = groupUpdateReducer({ inner: s => ({ ...s, value }), @@ -139,15 +129,30 @@ describe('update functions', () => { }); it('should call reducer for groups', () => { - const resultState = setValue({ inner: 'A' })(INITIAL_STATE); + const resultState = setValue({ inner: 'A', inner5: INITIAL_STATE.value.inner5 })(INITIAL_STATE); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + it('should call reducer for arrays', () => { + const resultState = setValue(['A'])(INITIAL_STATE.controls.inner5); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); + it('should call reducer for controls uncurried', () => { const resultState = setValue('A', INITIAL_STATE.controls.inner); expect(resultState).not.toBe(INITIAL_STATE.controls.inner); }); + it('should call reducer for groups uncurried', () => { + const resultState = setValue({ inner: 'A', inner5: INITIAL_STATE.value.inner5 }, INITIAL_STATE); + expect(resultState).not.toBe(INITIAL_STATE); + }); + + it('should call reducer for arrays uncurried', () => { + const resultState = setValue(['A'], INITIAL_STATE.controls.inner5); + expect(resultState).not.toBe(INITIAL_STATE.controls.inner5); + }); + it('should throw if curried and no state', () => { expect(() => setValue('')(undefined as any)).toThrowError(); }); @@ -166,12 +171,30 @@ describe('update functions', () => { expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + it('should call reducer for arrays', () => { + const errors = { required: true }; + const resultState = validate(() => errors)(INITIAL_STATE.controls.inner5); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); + it('should call reducer for controls uncurried', () => { const errors = { required: true }; const resultState = validate(() => errors, INITIAL_STATE.controls.inner); expect(resultState).not.toBe(INITIAL_STATE.controls.inner); }); + it('should call reducer for groups uncurried', () => { + const errors = { required: true }; + const resultState = validate(() => errors, INITIAL_STATE); + expect(resultState).not.toBe(INITIAL_STATE); + }); + + it('should call reducer for arrays uncurried', () => { + const errors = { required: true }; + const resultState = validate(() => errors, INITIAL_STATE.controls.inner5); + expect(resultState).not.toBe(INITIAL_STATE.controls.inner5); + }); + it('should merge errors from multiple validation functions', () => { const errors1 = { required: true }; const errors2 = { min: 1 }; @@ -206,6 +229,12 @@ describe('update functions', () => { const resultState = enable(state); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const state = { ...INITIAL_STATE.controls.inner5, isEnabled: false, isDisabled: true }; + const resultState = enable(state); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(disable.name, () => { @@ -218,6 +247,11 @@ describe('update functions', () => { const resultState = disable(INITIAL_STATE); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const resultState = disable(cast(INITIAL_STATE.controls.inner5)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(markAsDirty.name, () => { @@ -230,6 +264,11 @@ describe('update functions', () => { const resultState = markAsDirty(INITIAL_STATE); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const resultState = markAsDirty(cast(INITIAL_STATE.controls.inner5)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(markAsPristine.name, () => { @@ -244,6 +283,12 @@ describe('update functions', () => { const resultState = markAsPristine(state); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const state = { ...INITIAL_STATE.controls.inner5, isDirty: false, isPristine: true }; + const resultState = markAsPristine(cast(state)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(markAsTouched.name, () => { @@ -256,6 +301,11 @@ describe('update functions', () => { const resultState = markAsTouched(INITIAL_STATE); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const resultState = markAsTouched(cast(INITIAL_STATE.controls.inner5)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(markAsUntouched.name, () => { @@ -270,6 +320,12 @@ describe('update functions', () => { const resultState = markAsUntouched(state); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const state = { ...INITIAL_STATE.controls.inner5, isTouched: false, isUntouched: true }; + const resultState = markAsUntouched(cast(state)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(markAsSubmitted.name, () => { @@ -282,6 +338,11 @@ describe('update functions', () => { const resultState = markAsSubmitted(INITIAL_STATE); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const resultState = markAsSubmitted(cast(INITIAL_STATE.controls.inner5)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(markAsUnsubmitted.name, () => { @@ -296,6 +357,12 @@ describe('update functions', () => { const resultState = markAsUnsubmitted(state); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const state = { ...INITIAL_STATE.controls.inner5, isSubmitted: false, isUnsubmitted: true }; + const resultState = markAsUnsubmitted(cast(state)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); describe(focus.name, () => { @@ -337,5 +404,10 @@ describe('update functions', () => { const resultState = setUserDefinedProperty('prop', 12)(cast(INITIAL_STATE)); expect(resultState).not.toBe(cast(INITIAL_STATE)); }); + + it('should call reducer for arrays', () => { + const resultState = setUserDefinedProperty('prop', 12)(cast(INITIAL_STATE.controls.inner5)); + expect(resultState).not.toBe(cast(INITIAL_STATE.controls.inner5)); + }); }); }); diff --git a/src/update-functions.ts b/src/update-functions.ts index c4cb8dbd..1f9ccfb8 100644 --- a/src/update-functions.ts +++ b/src/update-functions.ts @@ -17,6 +17,7 @@ import { SetValueAction, UnfocusAction, } from './actions'; +import { formArrayReducer } from './array/reducer'; import { formControlReducer } from './control/reducer'; import { formGroupReducer } from './group/reducer'; import { computeGroupState } from './group/reducer/util'; @@ -26,6 +27,7 @@ import { FormControlValueTypes, FormGroupControls, FormGroupState, + isArrayState, isGroupState, KeyValue, ValidationErrors, @@ -79,7 +81,15 @@ export function groupUpdateReducer(...updateFnsArr: Array } function abstractControlReducer(state: AbstractControlState, action: Action): AbstractControlState { - return isGroupState(state) ? formGroupReducer(state as any, action) as any : formControlReducer(state as any, action); + if (isArrayState(state)) { + return formArrayReducer(state, action as any) as any; + } + + if (isGroupState(state)) { + return formGroupReducer(state, action); + } + + return formControlReducer(state as any, action) as any; } function ensureState(state: AbstractControlState) { From d8a65b4a825fde5e3b1819200fac5b4ac75f533b Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sun, 29 Oct 2017 14:16:13 +0100 Subject: [PATCH 08/11] feat: add update array update function + update documentation --- docs/UPDATING_THE_STATE.md | 27 +++++++++------- src/update-functions.spec.ts | 62 +++++++++++++++++++++++++++++++++++- src/update-functions.ts | 37 ++++++++++++++++++--- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/docs/UPDATING_THE_STATE.md b/docs/UPDATING_THE_STATE.md index a231f5d2..1871ba3a 100644 --- a/docs/UPDATING_THE_STATE.md +++ b/docs/UPDATING_THE_STATE.md @@ -4,29 +4,32 @@ All states are internally updated by ngrx-forms through dispatching actions. Whi |Function|Description| |-|-| -|`setValue`|This curried function takes a value and returns a function that takes a state and updates the value of the state. Note that setting the value of the group will also update all children including adding and removing children on the fly for added/removed properties. Has an uncurried overload that takes a state directly as the second parameter.| +|`setValue`|This curried function takes a value and returns a function that takes a state and updates the value of the state. Note that setting the value of a group or array will also update all children including adding and removing children on the fly for added/removed properties/items. Has an uncurried overload that takes a state directly as the second parameter.| |`validate`|This curried function takes either a single validation function or an array of validation functions as a parameter and returns a function that takes a state and updates the errors of the state with the result of the provided validation function applied to the state's value. Has an uncurried overload that takes a state directly as the second parameter.| -|`enable`|This function takes a state and enables it. For groups this also recursively enables all children.| -|`disable`|This function takes a state and disables it. For groups this also recursively disables all children.| -|`markAsDirty`|This function takes a state and marks it as dirty. For groups this also recursively marks all children as dirty.| -|`markAsPristine`|This function takes a state and marks it as pristine. For groups this also recursively marks all children as pristine.| -|`markAsTouched`|This function takes a state and marks it as touched. For groups this also recursively marks all children as touched.| -|`markAsUntouched`|This function takes a state and marks it as untouched. For groups this also recursively marks all children as untouched.| -|`markAsSubmitted`|This function takes a state and marks it as submitted. For groups this also recursively marks all children as submitted.| -|`markAsUnsubmitted`|This function takes a state and marks it as unsubmitted. For groups this also recursively marks all children as unsubmitted.| +|`enable`|This function takes a state and enables it. For groups and arrays this also recursively enables all children.| +|`disable`|This function takes a state and disables it. For groups and arrays this also recursively disables all children.| +|`markAsDirty`|This function takes a state and marks it as dirty. For groups and arrays this also recursively marks all children as dirty.| +|`markAsPristine`|This function takes a state and marks it as pristine. For groups and arrays this also recursively marks all children as pristine.| +|`markAsTouched`|This function takes a state and marks it as touched. For groups and arrays this also recursively marks all children as touched.| +|`markAsUntouched`|This function takes a state and marks it as untouched. For groups and arrays this also recursively marks all children as untouched.| +|`markAsSubmitted`|This function takes a state and marks it as submitted. For groups and arrays this also recursively marks all children as submitted.| +|`markAsUnsubmitted`|This function takes a state and marks it as unsubmitted. For groups and arrays this also recursively marks all children as unsubmitted.| |`focus`|This function takes a control state and makes it focused (which will also `.focus()` the form element).| |`unfocus`|This function takes a control state and makes it unfocused (which will also `.blur()` the form element).| |`addControl`|This curried function takes a name and a value and returns a function that takes a group state and adds a child control with the given name and value to the state.| |`removeControl`|This curried function takes a name and returns a function that takes a group state and removes a child control with the given name from the state.| |`setUserDefinedProperty`|This curried function takes a name and a value and returns a function that takes a state and sets the property with the given name to the given value on the state's user defined properties.| -These are very basic functions that perform simple updates on states. The last two functions below contain the real magic that allows easily updating deeply nested form states. +These are the basic functions that perform simple updates on states. The functions below contain the real magic that allows easily updating deeply nested form states. + +`updateArray`: +This curried function takes an update function and returns a function that takes an array state, applies the provided update function to each element and recomputes the state of the array afterwards. As with all the functions above this function does not change the reference of the array if the update function does not change any children. See the section below for an example of how this function can be used. `updateGroup`: This curried function takes a partial object in the shape of the group's value where each key contains an update function for that child and returns a function that takes a group state, applies all the provided update functions recursively and recomputes the state of the group afterwards. As with all the functions above this function does not change the reference of the group if none of the child update functions change any children. The best example of how this can be used is simple validation: ```typescript -import { updateGroup, validate } from 'ngrx-forms'; +import { updateArray, updateGroup, validate } from 'ngrx-forms'; export interface NestedValue { someNumber: number; @@ -36,6 +39,7 @@ export interface MyFormValue { someTextInput: string; someCheckbox: boolean; nested: NestedValue; + someNumbers: number[]; } function required(value: any) { @@ -51,6 +55,7 @@ const updateMyFormGroup = updateGroup({ nested: updateGroup({ someNumber: validate([required, min]), }), + someNumbers: updateArray(validate(min)), }); ``` diff --git a/src/update-functions.spec.ts b/src/update-functions.spec.ts index 5d9734cc..a48bd40a 100644 --- a/src/update-functions.spec.ts +++ b/src/update-functions.spec.ts @@ -1,5 +1,5 @@ import { MarkAsTouchedAction, SetValueAction } from './actions'; -import { cast, createFormGroupState, FormGroupState } from './state'; +import { cast, createFormArrayState, createFormGroupState, FormGroupState } from './state'; import { addControl, disable, @@ -16,6 +16,7 @@ import { setUserDefinedProperty, setValue, unfocus, + updateArray, updateGroup, validate, } from './update-functions'; @@ -28,6 +29,57 @@ describe('update functions', () => { const INITIAL_FORM_CONTROL_VALUE: FormGroupValue = { inner: '', inner3: { inner4: '' }, inner5: [''] }; const INITIAL_STATE = createFormGroupState(FORM_CONTROL_ID, INITIAL_FORM_CONTROL_VALUE); + describe(updateArray.name, () => { + it('should apply the provided functions to control children', () => { + const state = createFormArrayState(FORM_CONTROL_ID, ['']); + const expected = { ...state.controls[0], value: 'A' }; + const resultState = updateArray(() => expected)(state); + expect(resultState.controls[0]).toBe(expected); + }); + + it('should apply the provided functions to all control children', () => { + const state = createFormArrayState(FORM_CONTROL_ID, ['', '']); + const expected = { ...state.controls[0], value: 'A' }; + const resultState = updateArray(() => expected)(state); + expect(resultState.controls[0]).toBe(expected); + expect(resultState.controls[1]).toBe(expected); + }); + + it('should apply the provided functions to group children', () => { + const state = createFormArrayState(FORM_CONTROL_ID, [{ inner: '' }]); + const expected = { ...state.controls[0], value: { inner: 'A' } }; + const resultState = updateArray(() => expected)(state); + expect(resultState.controls[0]).toBe(expected); + }); + + it('should apply the provided functions to array children', () => { + const state = createFormArrayState(FORM_CONTROL_ID, [['']]); + const expected = { ...state.controls[0], value: ['A'] }; + const resultState = updateArray(() => expected)(state); + expect(resultState.controls[0]).toBe(expected); + }); + + it('should apply multiple provided functions one after another', () => { + const state = createFormArrayState(FORM_CONTROL_ID, ['A', 'B', 'C']); + const expected1 = { ...state.controls[0], value: 'D' }; + const expected2 = { ...state.controls[1], value: 'E' }; + const expected3 = { ...state.controls[2], value: 'F' }; + let resultState = updateArray(s => s.value === 'A' ? expected1 : s.value === 'B' ? expected3 : s)(state); + resultState = updateArray(s => s.value === 'F' ? expected2 : s.value === 'C' ? expected3 : s)(resultState); + expect(resultState.controls[0]).toBe(expected1); + expect(resultState.controls[1]).toBe(expected2); + expect(resultState.controls[2]).toBe(expected3); + }); + + it('should pass the parent array as the second parameter', () => { + const state = createFormArrayState(FORM_CONTROL_ID, ['', '']); + updateArray((c, p) => { + expect(p).toBe(state); + return c; + })(state); + }); + }); + describe(updateGroup.name, () => { it('should apply the provided functions to control children', () => { const expected = { ...INITIAL_STATE.controls.inner, value: 'A' }; @@ -45,6 +97,14 @@ describe('update functions', () => { expect(resultState.controls.inner3).toBe(expected); }); + it('should apply the provided functions to array children', () => { + const expected = { ...INITIAL_STATE.controls.inner5, value: ['A'] }; + const resultState = updateGroup({ + inner5: () => expected, + })(INITIAL_STATE); + expect(resultState.controls.inner5).toBe(expected); + }); + it('should apply multiple provided function objects one after another', () => { const updatedInner1 = { ...INITIAL_STATE.controls.inner, value: 'A' }; const expectedInner1 = { ...INITIAL_STATE.controls.inner, value: 'B' }; diff --git a/src/update-functions.ts b/src/update-functions.ts index 1f9ccfb8..46d945dd 100644 --- a/src/update-functions.ts +++ b/src/update-functions.ts @@ -18,11 +18,13 @@ import { UnfocusAction, } from './actions'; import { formArrayReducer } from './array/reducer'; +import { computeArrayState } from './array/reducer/util'; import { formControlReducer } from './control/reducer'; import { formGroupReducer } from './group/reducer'; import { computeGroupState } from './group/reducer/util'; import { AbstractControlState, + FormArrayState, FormControlState, FormControlValueTypes, FormGroupControls, @@ -38,7 +40,7 @@ export type ProjectFn2 = (t: T, k: K) => T; export type StateUpdateFns = {[controlId in keyof TValue]?: ProjectFn2, FormGroupState> }; -function updateControlsState(updateFns: StateUpdateFns) { +function updateGroupControlsState(updateFns: StateUpdateFns) { return (state: FormGroupState) => { let hasChanged = false; const newControls = Object.keys(state.controls).reduce((res, key) => { @@ -55,24 +57,49 @@ function updateControlsState(updateFns: StateUpdateFns< }; } -function updateGroupSingle(updateFns: StateUpdateFns) { +function updateGroupSingle(updateFns: StateUpdateFns) { return (state: FormGroupState): FormGroupState => { - const newControls = updateControlsState(updateFns)(state); + const newControls = updateGroupControlsState(updateFns)(state); return newControls !== state.controls ? computeGroupState(state.id, newControls, state.value, state.errors, state.userDefinedProperties) : state; }; } -export function updateGroup(...updateFnsArr: Array>) { +export function updateGroup(...updateFnsArr: Array>) { return (state: FormGroupState): FormGroupState => { return updateFnsArr.reduce((s, updateFns) => updateGroupSingle(updateFns)(s), state); }; } +function updateArrayControlsState(updateFn: ProjectFn2, FormArrayState>) { + return (state: FormArrayState) => { + let hasChanged = false; + const newControls = state.controls.map(control => { + const newControl = updateFn(control, state); + hasChanged = hasChanged || newControl !== control; + return newControl; + }); + return hasChanged ? newControls : state.controls; + }; +} + +function updateArraySingle(updateFn: ProjectFn2, FormArrayState>) { + return (state: FormArrayState): FormArrayState => { + const newControls = updateArrayControlsState(updateFn)(state); + return newControls !== state.controls ? computeArrayState(state.id, newControls, state.value, state.errors, state.userDefinedProperties) : state; + }; +} + +export function updateArray(...updateFnArr: Array, FormArrayState>>) { + return (state: FormArrayState): FormArrayState => { + return updateFnArr.reduce((s, updateFn) => updateArraySingle(updateFn)(s), state); + }; +} + export function compose(...fns: Array<(t: T) => T>) { return (t: T) => fns.reduce((res, f) => f(res), t); } -export function groupUpdateReducer(...updateFnsArr: Array>) { +export function groupUpdateReducer(...updateFnsArr: Array>) { return (state: FormGroupState, action: Action) => compose>( s => formGroupReducer(s, action), From 0b57b49900894670acdf44640cf62e39f0babb0b Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sun, 29 Oct 2017 14:43:19 +0100 Subject: [PATCH 09/11] build: add build script as a workaround for what seems like a bug in npm (folder name prefixed with @ not working during packing) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f0476cd..1ffb189b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "pack-lib": "npm pack ./dist", "publish-lib": "npm publish ./dist", "compodoc": "compodoc -p tsconfig.json", - "compodoc-serve": "compodoc -s" + "compodoc-serve": "compodoc -s", + "example-install": "npm run build && move ./dist/@ngrx ./dist/ngrx && npm pack ./dist && cd example-app && npm install ../ngrx-forms-1.1.1.tgz && move ./node_modules/ngrx-forms/ngrx ./node_modules/ngrx-forms/@ngrx && cd .." }, "typings": "./forms.d.ts", "author": "", From afc5d36a6cacde545ea923af7f9232dcf043241b Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sun, 29 Oct 2017 14:43:52 +0100 Subject: [PATCH 10/11] docs: fix invalid code example --- docs/UPDATING_THE_STATE.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/UPDATING_THE_STATE.md b/docs/UPDATING_THE_STATE.md index 1871ba3a..368840a4 100644 --- a/docs/UPDATING_THE_STATE.md +++ b/docs/UPDATING_THE_STATE.md @@ -46,16 +46,18 @@ function required(value: any) { return !!value ? {} : { required: true }; } -function min(value: number, minValue: number) { - return value >= minValue ? {} : { min: [value, minValue] }; +function min(minValue: number) { + return (value: number) => { + return value >= minValue ? {} : { min: [value, minValue] }; + }; } const updateMyFormGroup = updateGroup({ someTextInput: validate(required), nested: updateGroup({ - someNumber: validate([required, min]), + someNumber: validate([required, min(2)]), }), - someNumbers: updateArray(validate(min)), + someNumbers: updateArray(validate(min(3))), }); ``` From 8539ea474c368b8304a6ca790e10cd25f0313083 Mon Sep 17 00:00:00 2001 From: Jonathan Ziller Date: Sun, 29 Oct 2017 14:47:31 +0100 Subject: [PATCH 11/11] docs: fix inconsistencies in code examples --- docs/UPDATING_THE_STATE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/UPDATING_THE_STATE.md b/docs/UPDATING_THE_STATE.md index 368840a4..10ff8519 100644 --- a/docs/UPDATING_THE_STATE.md +++ b/docs/UPDATING_THE_STATE.md @@ -69,8 +69,9 @@ In addition, the `updateGroup` function allows specifying as many update functio const updateMyFormGroup = updateGroup({ someTextInput: validate(required), nested: updateGroup({ - someNumber: validate(required), + someNumber: validate([required, min(2)]), }), + someNumbers: updateArray(validate(min(3))), }, { // note that the parent form state is provided as the second argument to update functions; // type annotations added for clarity but are inferred correctly otherwise @@ -97,8 +98,9 @@ This curried function combines a `formGroupReducer` and the `updateGroup` functi const myFormReducer = groupUpdateReducer({ someTextInput: validate(required), nested: updateGroup({ - someNumber: validate(required), + someNumber: validate([required, min(2)]), }), + someNumbers: updateArray(validate(min(3))), }, { nested: (nested, myForm) => updateGroup({