Skip to content

Commit

Permalink
fix createFormGroupState producing results inconsistent with how gr…
Browse files Browse the repository at this point in the history
…oup states are recomputed from their children after an update
  • Loading branch information
MrWolfZ committed Jan 31, 2018
1 parent 173f818 commit 1c62d8c
Show file tree
Hide file tree
Showing 22 changed files with 185 additions and 136 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

* fix missing union case in typing of `updateArray` update function that causes a compile error if used inside an `updateGroup` ([fa7dccc](https://github.com/MrWolfZ/ngrx-forms/commit/fa7dccc))
* fix `updateGroup` throwing an error if an empty update object was provided in curried as well as uncurried version ([bee4d54](https://github.com/MrWolfZ/ngrx-forms/commit/bee4d54))
* fix `createFormGroupState` producing results inconsistent with how group states are recomputed from their children after an update

<a name="2.1.2"></a>
### 2.1.2
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/add-control.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Actions, AddGroupControlAction } from '../../actions';
import { createChildState, FormGroupState, KeyValue } from '../../state';
import { childReducer, computeGroupState } from './util';
import { computeGroupState, createChildState, FormGroupState, KeyValue } from '../../state';
import { childReducer } from './util';

export function addControlReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/clear-async-error.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Actions, ClearAsyncErrorAction } from '../../actions';
import { FormGroupState, KeyValue } from '../../state';
import { childReducer, computeGroupState } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { childReducer } from './util';

export function clearAsyncErrorReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/disable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, DisableAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function disableReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/enable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, EnableAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function enableReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/mark-as-dirty.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, MarkAsDirtyAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function markAsDirtyReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/mark-as-pristine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, MarkAsPristineAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function markAsPristineReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/mark-as-submitted.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, MarkAsSubmittedAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function markAsSubmittedReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/mark-as-touched.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, MarkAsTouchedAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function markAsTouchedReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/mark-as-unsubmitted.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, MarkAsUnsubmittedAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function markAsUnsubmittedReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/mark-as-untouched.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, MarkAsUntouchedAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function markAsUntouchedReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/remove-control.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, RemoveGroupControlAction } from '../../actions';
import { computeGroupState, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { childReducer } from './util';

export function removeControlReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/reset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, ResetAction } from '../../actions';
import { computeGroupState, dispatchActionPerChild, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { dispatchActionPerChild, childReducer } from './util';

export function resetReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/set-async-error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Actions, SetAsyncErrorAction } from '../../actions';
import { FormGroupState, KeyValue } from '../../state';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { deepEquals } from '../../util';
import { childReducer, computeGroupState } from './util';
import { childReducer } from './util';

export function setAsyncErrorReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/set-errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, SetErrorsAction } from '../../actions';
import { computeGroupState, childReducer } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { childReducer } from './util';
import { deepEquals } from '../../util';

export function setErrorsReducer<TValue extends KeyValue>(
Expand Down
2 changes: 1 addition & 1 deletion src/group/reducer/set-user-defined-property.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FormGroupState, KeyValue } from '../../state';
import { Actions, SetUserDefinedPropertyAction } from '../../actions';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { childReducer } from './util';

export function setUserDefinedPropertyReducer<TValue extends KeyValue>(
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/set-value.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Actions, SetValueAction } from '../../actions';
import { createChildState, FormGroupControls, FormGroupState, KeyValue } from '../../state';
import { callChildReducer, childReducer, computeGroupState } from './util';
import { computeGroupState, createChildState, FormGroupControls, FormGroupState, KeyValue } from '../../state';
import { callChildReducer, childReducer } from './util';

export function setValueReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
4 changes: 2 additions & 2 deletions src/group/reducer/start-async-validation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Actions, StartAsyncValidationAction } from '../../actions';
import { FormGroupState, KeyValue } from '../../state';
import { childReducer, computeGroupState } from './util';
import { computeGroupState, FormGroupState, KeyValue } from '../../state';
import { childReducer } from './util';

export function startAsyncValidationReducer<TValue extends KeyValue>(
state: FormGroupState<TValue>,
Expand Down
81 changes: 1 addition & 80 deletions src/group/reducer/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,89 +8,10 @@ import {
isArrayState,
isGroupState,
KeyValue,
ValidationErrors,
computeGroupState,
} from '../../state';
import { isEmpty } from '../../util';
import { formGroupReducerInternal } from '../reducer';

export function getFormGroupValue<TValue extends { [key: string]: any }>(
controls: FormGroupControls<TValue>,
originalValue: TValue,
): TValue {
let hasChanged = Object.keys(originalValue).length !== Object.keys(controls).length;
const newValue = Object.keys(controls).reduce((res, key) => {
hasChanged = hasChanged || originalValue[key] !== controls[key].value;
res[key] = controls[key].value;
return res;
}, {} as TValue);

return hasChanged ? newValue : originalValue;
}

export function getFormGroupErrors<TValue extends object>(
controls: FormGroupControls<TValue>,
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 = Object.keys(controls).reduce((res, key: any) => {
const controlErrors = controls[key].errors;
if (!isEmpty(controlErrors)) {
hasChanged = hasChanged || originalErrors['_' + key] !== controlErrors;
res['_' + key] = controls[key].errors;
} else {
hasChanged = hasChanged || originalErrors.hasOwnProperty('_' + key);
}

return res;
}, groupErrors as ValidationErrors);

hasChanged = hasChanged || Object.keys(originalErrors).length !== Object.keys(newErrors).length;

return hasChanged ? newErrors : originalErrors;
}

export function computeGroupState<TValue extends KeyValue>(
id: string,
controls: FormGroupControls<TValue>,
value: TValue,
errors: ValidationErrors,
pendingValidations: string[],
userDefinedProperties: KeyValue,
): FormGroupState<TValue> {
value = getFormGroupValue<TValue>(controls, value);
errors = getFormGroupErrors(controls, errors);
const isValid = isEmpty(errors);
const isDirty = Object.keys(controls).some(key => controls[key].isDirty);
const isEnabled = Object.keys(controls).some(key => controls[key].isEnabled);
const isTouched = Object.keys(controls).some(key => controls[key].isTouched);
const isSubmitted = Object.keys(controls).some(key => controls[key].isSubmitted);
const isValidationPending = pendingValidations.length > 0 || Object.keys(controls).some(key => controls[key].isValidationPending);
return {
id,
value,
errors,
pendingValidations,
isValidationPending,
isValid,
isInvalid: !isValid,
isEnabled,
isDisabled: !isEnabled,
isDirty,
isPristine: !isDirty,
isTouched,
isUntouched: !isTouched,
isSubmitted,
isUnsubmitted: !isSubmitted,
userDefinedProperties,
controls,
};
}

export function callChildReducer(
state: AbstractControlState<any>,
action: Actions<any>,
Expand Down
70 changes: 68 additions & 2 deletions src/state.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { cast, createFormArrayState, createFormControlState, createFormGroupState, isGroupState, isArrayState } from './state';
import { SetValueAction, SetUserDefinedPropertyAction } from './actions';
import {
cast,
computeGroupState,
createFormArrayState,
createFormControlState,
createFormGroupState,
isGroupState,
isArrayState,
} from './state';
import { formGroupReducer } from './group/reducer';
import { formArrayReducer } from './array/reducer';

describe('state', () => {
const FORM_CONTROL_ID = 'test ID';
Expand Down Expand Up @@ -58,7 +69,7 @@ describe('state', () => {
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_VALUE = { control: CONTROL_VALUE, group: GROUP_VALUE, array: ARRAY_VALUE };
const INITIAL_STATE = createFormGroupState<typeof INITIAL_VALUE>(FORM_CONTROL_ID, INITIAL_VALUE);

it('should set the correct id', () => {
Expand Down Expand Up @@ -120,6 +131,61 @@ describe('state', () => {
expect(controls).toBeDefined();
expect(Array.isArray(controls)).toBe(true);
});

it('should produce the same state as is computed after an action is applied', () => {
const state = computeGroupState(
INITIAL_STATE.id,
INITIAL_STATE.controls,
INITIAL_STATE.value,
INITIAL_STATE.errors,
INITIAL_STATE.pendingValidations,
INITIAL_STATE.userDefinedProperties,
);

expect(state.id).toBe(INITIAL_STATE.id);
expect(state.value).toEqual(INITIAL_STATE.value);
expect(state.errors).toEqual(INITIAL_STATE.errors);
expect(state.pendingValidations).toEqual(INITIAL_STATE.pendingValidations);
expect(state.isValid).toEqual(INITIAL_STATE.isValid);
expect(state.isInvalid).toEqual(INITIAL_STATE.isInvalid);
expect(state.isEnabled).toEqual(INITIAL_STATE.isEnabled);
expect(state.isDisabled).toEqual(INITIAL_STATE.isDisabled);
expect(state.isDirty).toEqual(INITIAL_STATE.isDirty);
expect(state.isPristine).toEqual(INITIAL_STATE.isPristine);
expect(state.isTouched).toEqual(INITIAL_STATE.isTouched);
expect(state.isUntouched).toEqual(INITIAL_STATE.isUntouched);
expect(state.isSubmitted).toEqual(INITIAL_STATE.isSubmitted);
expect(state.isUnsubmitted).toEqual(INITIAL_STATE.isUnsubmitted);
expect(state.userDefinedProperties).toEqual(INITIAL_STATE.userDefinedProperties);
});

it('should produce the same state as is computed after an action is applied for empty group', () => {
const initialState = createFormGroupState(FORM_CONTROL_ID, {});
const state = computeGroupState(
initialState.id,
initialState.controls,
initialState.value,
initialState.errors,
initialState.pendingValidations,
initialState.userDefinedProperties,
);

expect(state.id).toBe(initialState.id);
expect(state.value).toEqual(initialState.value);
expect(state.errors).toEqual(initialState.errors);
expect(state.pendingValidations).toEqual(initialState.pendingValidations);
expect(state.isValid).toEqual(initialState.isValid);
expect(state.isInvalid).toEqual(initialState.isInvalid);
expect(state.isEnabled).toEqual(initialState.isEnabled);
expect(state.isDisabled).toEqual(initialState.isDisabled);
expect(state.isDirty).toEqual(initialState.isDirty);
expect(state.isPristine).toEqual(initialState.isPristine);
expect(state.isTouched).toEqual(initialState.isTouched);
expect(state.isUntouched).toEqual(initialState.isUntouched);
expect(state.isSubmitted).toEqual(initialState.isSubmitted);
expect(state.isUnsubmitted).toEqual(initialState.isUnsubmitted);
expect(state.userDefinedProperties).toEqual(initialState.userDefinedProperties);
});
});

describe('array', () => {
Expand Down
Loading

0 comments on commit 1c62d8c

Please sign in to comment.