Skip to content

Commit

Permalink
feat(manager): synchronize field and form level validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Hyperkid123 committed Aug 17, 2020
1 parent e4cc158 commit 03fe94e
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 5 deletions.
145 changes: 145 additions & 0 deletions packages/form-state-manager/src/tests/utils/manager-api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -824,4 +824,149 @@ describe('managerApi', () => {
});
});
});

describe('Combine form and field level validation', () => {
const formLevelValidate = (values) => (values.foo === 'foo' ? { foo: 'form-error' } : undefined);
const fieldLevelValidate = (value) => (value === 'bar' ? 'field-error' : undefined);

it('should fail sync form level, but pass sync field level validation', () => {
const managerApi = createManagerApi({ validate: formLevelValidate });
const { change, registerField, getFieldState } = managerApi();
const getErrorState = () => {
let { valid, invalid, validating, errors } = managerApi();
let {
meta: { error: fieldError, valid: fieldValid, validating: fieldValidating, invalid: fieldInvalid }
} = getFieldState('foo');

return { valid, invalid, validating, errors, fieldError, fieldValid, fieldValidating, fieldInvalid };
};

registerField({ name: 'foo', validate: fieldLevelValidate, render: jest.fn() });

change('foo', 'foo');
let expectedResult = getErrorState();
expect(expectedResult).toEqual({
valid: false,
invalid: true,
validating: false,
errors: { foo: 'form-error' },
fieldError: 'form-error',
fieldValid: false,
fieldInvalid: true,
fieldValidating: false
});
});

it('should initialy fail sync form level, but pass on second run validation', () => {
const managerApi = createManagerApi({ validate: formLevelValidate });
const { change, registerField, getFieldState } = managerApi();
const getErrorState = () => {
let { valid, invalid, validating, errors } = managerApi();
let {
meta: { error: fieldError, valid: fieldValid, validating: fieldValidating, invalid: fieldInvalid }
} = getFieldState('foo');

return { valid, invalid, validating, errors, fieldError, fieldValid, fieldValidating, fieldInvalid };
};

registerField({ name: 'foo', validate: fieldLevelValidate, render: jest.fn() });

change('foo', 'foo');

let expectedResult = getErrorState();
expect(expectedResult).toEqual({
valid: false,
invalid: true,
validating: false,
errors: { foo: 'form-error' },
fieldError: 'form-error',
fieldValid: false,
fieldInvalid: true,
fieldValidating: false
});

change('foo', 'ok');

expectedResult = getErrorState();
expect(expectedResult).toEqual({
valid: true,
invalid: false,
validating: false,
errors: {},
fieldError: undefined,
fieldValid: true,
fieldInvalid: false,
fieldValidating: false
});
});

it('should pass sync form level, but fail sync field level validation', () => {
const managerApi = createManagerApi({ validate: formLevelValidate });
const { change, registerField, getFieldState } = managerApi();
const getErrorState = () => {
let { valid, invalid, validating, errors } = managerApi();
let {
meta: { error: fieldError, valid: fieldValid, validating: fieldValidating, invalid: fieldInvalid }
} = getFieldState('foo');

return { valid, invalid, validating, errors, fieldError, fieldValid, fieldValidating, fieldInvalid };
};

registerField({ name: 'foo', validate: fieldLevelValidate, render: jest.fn() });

change('foo', 'bar');
let expectedResult = getErrorState();
expect(expectedResult).toEqual({
valid: false,
invalid: true,
validating: false,
errors: { foo: 'field-error' },
fieldError: 'field-error',
fieldValid: false,
fieldInvalid: true,
fieldValidating: false
});
});

it('should fail first sync field level validation, but pass on second round', () => {
const managerApi = createManagerApi({ validate: formLevelValidate });
const { change, registerField, getFieldState } = managerApi();
const getErrorState = () => {
let { valid, invalid, validating, errors } = managerApi();
let {
meta: { error: fieldError, valid: fieldValid, validating: fieldValidating, invalid: fieldInvalid }
} = getFieldState('foo');

return { valid, invalid, validating, errors, fieldError, fieldValid, fieldValidating, fieldInvalid };
};

registerField({ name: 'foo', validate: fieldLevelValidate, render: jest.fn() });

change('foo', 'bar');
let expectedResult = getErrorState();
expect(expectedResult).toEqual({
valid: false,
invalid: true,
validating: false,
errors: { foo: 'field-error' },
fieldError: 'field-error',
fieldValid: false,
fieldInvalid: true,
fieldValidating: false
});

change('foo', 'ok');
expectedResult = getErrorState();
expect(expectedResult).toEqual({
valid: true,
invalid: false,
validating: false,
errors: {},
fieldError: undefined,
fieldValid: true,
fieldInvalid: false,
fieldValidating: false
});
});
});
});
8 changes: 7 additions & 1 deletion packages/form-state-manager/src/types/manager-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export interface FieldState {
name: string;
}

export interface ExtendedFieldState extends FieldState {
change: (value: any) => any;
blur: () => void;
focus: () => void;
}

export type UpdateFieldState = (name: string, mutateState: (prevState: FieldState) => FieldState) => void;

export type Callback = () => void;
Expand All @@ -20,7 +26,7 @@ export type UnregisterField = (field: Omit<FieldConfig, 'render'>) => void;
export type GetState = () => ManagerState;
export type OnSubmit = (values: AnyObject) => void;
export type GetFieldValue = (name: string) => any;
export type GetFieldState = (name: string) => AnyObject | undefined;
export type GetFieldState = (name: string) => ExtendedFieldState | undefined;
export type Focus = (name: string) => void;
export type Blur = (name: string) => void;
export type UpdateValid = (valid: boolean) => void;
Expand Down
33 changes: 30 additions & 3 deletions packages/form-state-manager/src/utils/manager-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import CreateManagerApi, {
AsyncWatcherRecord,
FieldState,
Callback,
SubscriberConfig
SubscriberConfig,
ExtendedFieldState
} from '../types/manager-api';
import AnyObject from '../types/any-object';
import FieldConfig from '../types/field-config';
Expand Down Expand Up @@ -215,6 +216,7 @@ const createManagerApi: CreateManagerApi = ({ onSubmit, clearOnUnmount, initiali

function validateForm(validate: FormValidator) {
const result = formLevelValidator(validate, state.values, managerApi);
const currentInvalidFields = Object.keys(state.errors);
if (isPromise(result)) {
const asyncResult = result as Promise<FormLevelError>;
return asyncResult
Expand All @@ -224,6 +226,7 @@ const createManagerApi: CreateManagerApi = ({ onSubmit, clearOnUnmount, initiali
state.valid = true;
state.invalid = false;
state.error = undefined;
revalidateFields(currentInvalidFields);
}
})
.catch((errors) => {
Expand All @@ -235,6 +238,9 @@ const createManagerApi: CreateManagerApi = ({ onSubmit, clearOnUnmount, initiali

const syncError = result as FormLevelError | undefined;
if (syncError) {
Object.keys(syncError).forEach((name) => {
handleFieldError(name, false, syncError[name]);
});
state.errors = syncError;
state.valid = false;
state.invalid = true;
Expand All @@ -243,9 +249,19 @@ const createManagerApi: CreateManagerApi = ({ onSubmit, clearOnUnmount, initiali
state.valid = true;
state.invalid = false;
state.error = undefined;
/**
* Fields have to be revalidated on field level to synchronize the form and field errors
*/
revalidateFields(currentInvalidFields);
}
}

function revalidateFields(fields: string[]) {
fields.forEach((name) => {
validateField(name, state.values[name]);
});
}

function change(name: string, value?: any): void {
state.values[name] = value;
state.visited[name] = true;
Expand Down Expand Up @@ -330,7 +346,7 @@ const createManagerApi: CreateManagerApi = ({ onSubmit, clearOnUnmount, initiali
return state.values[name];
}

function getFieldState(name: string): AnyObject | undefined {
function getFieldState(name: string): ExtendedFieldState | undefined {
if (state.fieldListeners[name]) {
return {
...state.fieldListeners[name].state,
Expand Down Expand Up @@ -358,7 +374,18 @@ const createManagerApi: CreateManagerApi = ({ onSubmit, clearOnUnmount, initiali
}

function updateError(name: string, error: string | undefined = undefined): void {
state.errors[name] = error;
if (error) {
state.errors[name] = error;
state.valid = false;
state.invalid = true;
} else {
delete state.errors[name];
}

if (Object.keys(state.errors).length === 0) {
state.valid = true;
state.invalid = false;
}
}

function registerAsyncValidator(validator: Promise<unknown>) {
Expand Down
2 changes: 1 addition & 1 deletion packages/form-state-manager/src/utils/use-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const useSubscription = ({
}
};

return [formOptions().getFieldValue(name), onChange, () => focus(name), () => blur(name), state?.meta];
return [formOptions().getFieldValue(name), onChange, () => focus(name), () => blur(name), state!.meta];
};

export default useSubscription;

0 comments on commit 03fe94e

Please sign in to comment.