Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add errors prop #11188

Merged
merged 6 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion reports/api-extractor.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type Control<TFieldValues extends FieldValues = FieldValues, TContext = a
_getWatch: WatchInternal<TFieldValues>;
_updateFieldArray: BatchFieldArrayUpdate;
_getFieldArray: <TFieldArrayValues>(name: InternalFieldName) => Partial<TFieldArrayValues>[];
_setErrors: (errors: FieldErrors<TFieldValues>) => void;
_updateDisabledField: (props: {
disabled?: boolean;
name: FieldName<any>;
Expand Down Expand Up @@ -676,6 +677,7 @@ export type UseFormProps<TFieldValues extends FieldValues = FieldValues, TContex
reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
defaultValues: DefaultValues<TFieldValues> | AsyncDefaultValues<TFieldValues>;
values: TFieldValues;
errors: FieldErrors<TFieldValues>;
resetOptions: Parameters<UseFormReset<TFieldValues>>[1];
resolver: Resolver<TFieldValues, TContext>;
context: TContext;
Expand Down Expand Up @@ -861,7 +863,7 @@ export type WatchObserver<TFieldValues extends FieldValues> = (value: DeepPartia

// Warnings were encountered during analysis:
//
// src/types/form.ts:439:3 - (ae-forgotten-export) The symbol "Subscription" needs to be exported by the entry point index.d.ts
// src/types/form.ts:440:3 - (ae-forgotten-export) The symbol "Subscription" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
26 changes: 26 additions & 0 deletions src/__tests__/useForm.server.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ describe('useForm with SSR', () => {
expect(spy).not.toHaveBeenCalled();
});

it('should display error with errors prop', () => {
const App = () => {
const {
register,
formState: { errors },
} = useForm<{
test: string;
}>({
errors: {
test: { type: 'test', message: 'test error' },
},
});

return (
<div>
<input {...register('test')} />
<span role="alert">{errors.test && errors.test.message}</span>
</div>
);
};

expect(renderToString(<App />)).toEqual(
'<div><input name="test"/><span role="alert">test error</span></div>',
);
});

it('should not pass down constrained API for server side rendering', () => {
const App = () => {
const { register } = useForm<{
Expand Down
50 changes: 50 additions & 0 deletions src/__tests__/useForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { act, renderHook } from '@testing-library/react-hooks';
import { VALIDATION_MODE } from '../constants';
import {
Control,
FieldErrors,
RegisterOptions,
UseFormRegister,
UseFormReturn,
Expand Down Expand Up @@ -551,6 +552,55 @@ describe('useForm', () => {

await waitFor(() => expect(span.textContent).toBe('data'));
});

it('should display the latest error message with errors prop', () => {
const Form = () => {
type FormValues = {
test1: string;
test2: string;
};
const [errorsState, setErrorsState] = React.useState<
FieldErrors<FormValues>
>({
test1: { type: 'test1', message: 'test1 error' },
});
const {
register,
formState: { errors },
} = useForm<FormValues>({
errors: errorsState,
});

return (
<div>
<input {...register('test1')} type="text" />
<span role="alert">{errors.test1 && errors.test1.message}</span>
<input {...register('test2')} type="text" />
<span role="alert">{errors.test2 && errors.test2.message}</span>
<button
onClick={() =>
setErrorsState((errors) => ({
...errors,
test2: { type: 'test2', message: 'test2 error' },
}))
}
>
Set Errors
</button>
</div>
);
};

render(<Form />);

const alert1 = screen.getAllByRole('alert')[0];
expect(alert1.textContent).toBe('test1 error');

fireEvent.click(screen.getByRole('button'));

const alert2 = screen.getAllByRole('alert')[1];
expect(alert2.textContent).toBe('test2 error');
});
});

describe('handleChangeRef', () => {
Expand Down
13 changes: 12 additions & 1 deletion src/logic/createFormControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EventType,
Field,
FieldError,
FieldErrors,
FieldNamesMarkedBoolean,
FieldPath,
FieldRefs,
Expand Down Expand Up @@ -45,6 +46,7 @@ import compact from '../utils/compact';
import convertToArrayPayload from '../utils/convertToArrayPayload';
import createSubject from '../utils/createSubject';
import deepEqual from '../utils/deepEqual';
import { deepMerge } from '../utils/deepMerge';
import get from '../utils/get';
import isBoolean from '../utils/isBoolean';
import isCheckBoxInput from '../utils/isCheckBoxInput';
Expand Down Expand Up @@ -111,7 +113,7 @@ export function createFormControl<
isValid: false,
touchedFields: {},
dirtyFields: {},
errors: {},
errors: isObject(_options.errors) ? _options.errors || {} : {},
kotarella1110 marked this conversation as resolved.
Show resolved Hide resolved
disabled: false,
};
let _fields: FieldRefs = {};
Expand Down Expand Up @@ -246,6 +248,14 @@ export function createFormControl<
});
};

const _setErrors = (errors: FieldErrors<TFieldValues>) => {
deepMerge(_formState.errors, errors);
_subjects.state.next({
errors: _formState.errors,
isValid: false,
});
bluebill1049 marked this conversation as resolved.
Show resolved Hide resolved
};

const updateValidAndValue = (
name: InternalFieldName,
shouldSkipSetValueAs: boolean,
Expand Down Expand Up @@ -1330,6 +1340,7 @@ export function createFormControl<
_disableForm,
_subjects,
_proxyFormState,
_setErrors,
get _fields() {
return _fields;
},
Expand Down
2 changes: 2 additions & 0 deletions src/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export type UseFormProps<
reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
defaultValues: DefaultValues<TFieldValues> | AsyncDefaultValues<TFieldValues>;
values: TFieldValues;
errors: FieldErrors<TFieldValues>;
resetOptions: Parameters<UseFormReset<TFieldValues>>[1];
resolver: Resolver<TFieldValues, TContext>;
context: TContext;
Expand Down Expand Up @@ -785,6 +786,7 @@ export type Control<
_getFieldArray: <TFieldArrayValues>(
name: InternalFieldName,
) => Partial<TFieldArrayValues>[];
_setErrors: (errors: FieldErrors<TFieldValues>) => void;
_updateDisabledField: (
props: {
disabled?: boolean;
Expand Down
9 changes: 8 additions & 1 deletion src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import getProxyFormState from './logic/getProxyFormState';
import shouldRenderFormState from './logic/shouldRenderFormState';
import deepEqual from './utils/deepEqual';
import isFunction from './utils/isFunction';
import isObject from './utils/isObject';
import {
FieldValues,
FormState,
Expand Down Expand Up @@ -65,7 +66,7 @@ export function useForm<
submitCount: 0,
dirtyFields: {},
touchedFields: {},
errors: {},
errors: isObject(props.errors) ? props.errors || {} : {},
kotarella1110 marked this conversation as resolved.
Show resolved Hide resolved
disabled: false,
defaultValues: isFunction(props.defaultValues)
? undefined
Expand Down Expand Up @@ -127,6 +128,12 @@ export function useForm<
}
}, [props.values, control]);

React.useEffect(() => {
if (props.errors) {
control._setErrors(props.errors);
}
}, [props.errors, control]);

React.useEffect(() => {
if (!control._state.mount) {
control._updateValid();
Expand Down