Skip to content

Commit

Permalink
Merge pull request #278 from andyrichardson/235-update-validation
Browse files Browse the repository at this point in the history
Add setFieldState call to useField on validation change
  • Loading branch information
andyrichardson committed Jul 30, 2020
2 parents 16cdfad + 8610486 commit 846a4a9
Show file tree
Hide file tree
Showing 6 changed files with 423 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/__snapshots__/useForm.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Object {
"isValid": true,
"isValidating": false,
"mountField": [Function],
"setFieldState": [Function],
"setFieldValue": [Function],
"unmountField": [Function],
"validateField": [Function],
Expand Down
22 changes: 19 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { SetFieldValueArgs, BlurFieldArgs, UnmountFieldArgs } from './useForm';
import {
SetFieldValueArgs,
BlurFieldArgs,
UnmountFieldArgs,
SetFieldStateArgs,
} from './useForm';

export type FormError = Error | string;

Expand All @@ -11,14 +16,25 @@ export type FieldsState<T = any> = Record<

export interface FormState<T extends Record<string, any> = any> {
fields: FieldsState<T>;
/** All mounted fields are valid. */
isValid: boolean;
/** Async validation currently active on a mounted fields. */
isValidating: boolean;
mountField: (k: FieldConfig<T>) => void;
unmountField: (k: UnmountFieldArgs<T>) => void;
/** Set value for a field. */
setFieldValue: (a: SetFieldValueArgs<T>) => void;
/** Trigger blur event for a mounted field. */
blurField: (a: BlurFieldArgs<T>) => void;
/** Force trigger validation on a mounted field. */
validateField: (a: { name: keyof T & string }) => void;
/** Force trigger validation */
validateFields: () => void;

/** Internal: Manually mount field. */
mountField: (k: FieldConfig<T>) => void;
/** Internal: Manually unmount field. */
unmountField: (k: UnmountFieldArgs<T>) => void;
/** Internal: Manually set field state. */
setFieldState: (a: SetFieldStateArgs<T>) => void;
}

export interface FieldState<T = string | boolean | number | string[]> {
Expand Down
121 changes: 120 additions & 1 deletion src/useField.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { mount } from 'enzyme';
import { mount, ReactWrapper } from 'enzyme';
import { useField, UseFieldResponse } from './useField';
import { FielderContext } from './context';
import { FieldConfig } from './types';
Expand All @@ -10,6 +10,7 @@ let context = {
unmountField: jest.fn(),
blurField: jest.fn(),
setFieldValue: jest.fn(),
setFieldState: jest.fn(),
};
let response: UseFieldResponse;
let args: FieldConfig<any>;
Expand Down Expand Up @@ -37,6 +38,7 @@ beforeEach(() => {
unmountField: jest.fn(),
blurField: jest.fn(),
setFieldValue: jest.fn(),
setFieldState: jest.fn(),
};
});

Expand Down Expand Up @@ -96,6 +98,24 @@ describe('on mount', () => {
...args,
});
});

it('does not call setFieldState with validation args', () => {
args = {
name: 'someField',
initialError: 'aaa',
initialValid: true,
initialValue: 'hello',
initialTouched: true,
validate: jest.fn(),
validateOnBlur: false,
validateOnChange: false,
validateOnUpdate: true,
};

mount(<Fixture />);

expect(context.setFieldState).toBeCalledTimes(0);
});
});

describe('on unmount', () => {
Expand Down Expand Up @@ -245,3 +265,102 @@ describe('on change', () => {
});
});
});

describe('on validation arg change', () => {
let wrapper: ReactWrapper<any>;

beforeEach(() => {
args = {
name: 'someField',
validateOnBlur: false,
validateOnChange: true,
validateOnUpdate: false,
validate: jest.fn(),
};
wrapper = mount(<Fixture />);
});

describe('on validate change', () => {
it('calls setFieldState with new function', () => {
const oldState = { ...args };

args = {
...args,
validate: jest.fn(),
};

wrapper.setProps({});

expect(context.setFieldState).toBeCalledTimes(1);
expect(context.setFieldState.mock.calls[0][0].state(oldState)).toEqual(
expect.objectContaining({
...oldState,
_validate: args.validate,
})
);
});
});

describe('on validateOnBlur change', () => {
it('calls setFieldState with new value', () => {
const oldState = { ...args };

args = {
...args,
validateOnBlur: true,
};

wrapper.setProps({});

expect(context.setFieldState).toBeCalledTimes(1);
expect(context.setFieldState.mock.calls[0][0].state(oldState)).toEqual(
expect.objectContaining({
...oldState,
_validateOnBlur: true,
})
);
});
});

describe('on validateOnUpdate change', () => {
it('calls setFieldState with new value', () => {
const oldState = { ...args };

args = {
...args,
validateOnUpdate: true,
};

wrapper.setProps({});

expect(context.setFieldState).toBeCalledTimes(1);
expect(context.setFieldState.mock.calls[0][0].state(oldState)).toEqual(
expect.objectContaining({
...oldState,
_validateOnUpdate: true,
})
);
});
});

describe('on validateOnChange change', () => {
it('calls setFieldState with new value', () => {
const oldState = { ...args };

args = {
...args,
validateOnChange: false,
};

wrapper.setProps({});

expect(context.setFieldState).toBeCalledTimes(1);
expect(context.setFieldState.mock.calls[0][0].state(oldState)).toEqual(
expect.objectContaining({
...oldState,
_validateOnChange: false,
})
);
});
});
});
41 changes: 37 additions & 4 deletions src/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ export const useField = <T = any>({
destroyOnUnmount = false,
}: FieldConfig<T>): UseFieldResponse => {
const destroyRef = useRef(destroyOnUnmount);
const initialMount = useRef(true);
const {
fields,
blurField,
mountField,
unmountField,
setFieldValue,
setFieldState,
} = useContext<FormState>(FielderContext);

const name = useMemo(() => initialName, []);
Expand All @@ -81,6 +83,7 @@ export const useField = <T = any>({

useMemo(() => (destroyRef.current = destroyOnUnmount), [destroyOnUnmount]);

/** (re)mount field on first render */
useLayoutEffect(() => {
if (fields[name] && fields[name]._isActive) {
console.warn(
Expand All @@ -101,11 +104,41 @@ export const useField = <T = any>({
});

return () => unmountField({ name, destroy: destroyRef.current });
}, [mountField]);
}, [mountField, name]);

const onBlur = useCallback(() => {
blurField({ name });
}, [blurField]);
/** Update field state on validation config change. */
useLayoutEffect(() => {
if (initialMount.current) {
initialMount.current = false;
return;
}

setFieldState({
name,
state: (s) => {
if (
s._validate === validate &&
s._validateOnBlur === validateOnBlur &&
s._validateOnChange === validateOnChange &&
s._validateOnUpdate === validateOnUpdate
) {
return s;
}

return {
...s,
_validate: validate,
_validateOnBlur: validateOnBlur,
_validateOnChange: validateOnChange,
_validateOnUpdate: validateOnUpdate,
};
},
validate: (s) =>
s._validate !== validate && validateOnChange && s.hasChanged,
});
}, [validate, validateOnBlur, validateOnChange, validateOnUpdate, name]);

const onBlur = useCallback(() => blurField({ name }), [blurField]);

const onChange = useCallback<UseFieldProps['onChange']>(
(e) => {
Expand Down
Loading

0 comments on commit 846a4a9

Please sign in to comment.