From d116a151fbdfb79ca5eb7533bfef017ad7c80344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gromit=20=28=EC=A0=84=EB=AF=BC=EC=9E=AC=29?= <64779472+ssi02014@users.noreply.github.com> Date: Sat, 11 Nov 2023 05:41:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=A7=20chore:=20improved=20`isObjec?= =?UTF-8?q?tType`=20and=20`swap`=20(#11183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/isObject.ts | 3 ++- src/utils/swap.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/isObject.ts b/src/utils/isObject.ts index 90fef68e4af8..7d06d2d9dbdb 100644 --- a/src/utils/isObject.ts +++ b/src/utils/isObject.ts @@ -1,7 +1,8 @@ import isDateObject from './isDateObject'; import isNullOrUndefined from './isNullOrUndefined'; -export const isObjectType = (value: unknown) => typeof value === 'object'; +export const isObjectType = (value: unknown): value is object => + typeof value === 'object'; export default (value: unknown): value is T => !isNullOrUndefined(value) && diff --git a/src/utils/swap.ts b/src/utils/swap.ts index e26ff44d9e36..8b685a6fdc51 100644 --- a/src/utils/swap.ts +++ b/src/utils/swap.ts @@ -1,3 +1,3 @@ export default (data: T[], indexA: number, indexB: number): void => { - data[indexA] = [data[indexB], (data[indexB] = data[indexA])][0]; + [data[indexA], data[indexB]] = [data[indexB], data[indexA]]; }; From c7b7eacb00cd11fe8fd075c958589b5b00b2c49d Mon Sep 17 00:00:00 2001 From: Kotaro Sugawara Date: Tue, 14 Nov 2023 13:46:45 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=AE=20feat:=20add=20`errors`=20pro?= =?UTF-8?q?p=20at=20`useForm`=20(#11188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add errors prop * feat: update api-extractor * fix: unuse deepMerge * Update src/logic/createFormControl.ts Co-authored-by: Beier (Bill) * Update src/useForm.ts Co-authored-by: Beier (Bill) * refactor: minor code cleanup --------- Co-authored-by: Beier (Bill) --- reports/api-extractor.md | 4 ++- src/__tests__/useForm.server.test.tsx | 26 ++++++++++++++ src/__tests__/useForm.test.tsx | 50 +++++++++++++++++++++++++++ src/logic/createFormControl.ts | 12 ++++++- src/types/form.ts | 2 ++ src/useForm.ts | 8 ++++- 6 files changed, 99 insertions(+), 3 deletions(-) diff --git a/reports/api-extractor.md b/reports/api-extractor.md index 79d67c9e15cf..04931f5d2395 100644 --- a/reports/api-extractor.md +++ b/reports/api-extractor.md @@ -60,6 +60,7 @@ export type Control; _updateFieldArray: BatchFieldArrayUpdate; _getFieldArray: (name: InternalFieldName) => Partial[]; + _setErrors: (errors: FieldErrors) => void; _updateDisabledField: (props: { disabled?: boolean; name: FieldName; @@ -676,6 +677,7 @@ export type UseFormProps; defaultValues: DefaultValues | AsyncDefaultValues; values: TFieldValues; + errors: FieldErrors; resetOptions: Parameters>[1]; resolver: Resolver; context: TContext; @@ -861,7 +863,7 @@ export type WatchObserver = (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) diff --git a/src/__tests__/useForm.server.test.tsx b/src/__tests__/useForm.server.test.tsx index 425beefd71bf..e3d30ec41d1e 100644 --- a/src/__tests__/useForm.server.test.tsx +++ b/src/__tests__/useForm.server.test.tsx @@ -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 ( +
+ + {errors.test && errors.test.message} +
+ ); + }; + + expect(renderToString()).toEqual( + '
test error
', + ); + }); + it('should not pass down constrained API for server side rendering', () => { const App = () => { const { register } = useForm<{ diff --git a/src/__tests__/useForm.test.tsx b/src/__tests__/useForm.test.tsx index 3bed576c74ce..b2a906f48760 100644 --- a/src/__tests__/useForm.test.tsx +++ b/src/__tests__/useForm.test.tsx @@ -12,6 +12,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { VALIDATION_MODE } from '../constants'; import { Control, + FieldErrors, RegisterOptions, UseFormRegister, UseFormReturn, @@ -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 + >({ + test1: { type: 'test1', message: 'test1 error' }, + }); + const { + register, + formState: { errors }, + } = useForm({ + errors: errorsState, + }); + + return ( +
+ + {errors.test1 && errors.test1.message} + + {errors.test2 && errors.test2.message} + +
+ ); + }; + + render(
); + + 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', () => { diff --git a/src/logic/createFormControl.ts b/src/logic/createFormControl.ts index 83a86261bd7c..53735fa9b034 100644 --- a/src/logic/createFormControl.ts +++ b/src/logic/createFormControl.ts @@ -8,6 +8,7 @@ import { EventType, Field, FieldError, + FieldErrors, FieldNamesMarkedBoolean, FieldPath, FieldRefs, @@ -111,7 +112,7 @@ export function createFormControl< isValid: false, touchedFields: {}, dirtyFields: {}, - errors: {}, + errors: _options.errors || {}, disabled: false, }; let _fields: FieldRefs = {}; @@ -246,6 +247,14 @@ export function createFormControl< }); }; + const _setErrors = (errors: FieldErrors) => { + _formState.errors = errors; + _subjects.state.next({ + errors: _formState.errors, + isValid: false, + }); + }; + const updateValidAndValue = ( name: InternalFieldName, shouldSkipSetValueAs: boolean, @@ -1330,6 +1339,7 @@ export function createFormControl< _disableForm, _subjects, _proxyFormState, + _setErrors, get _fields() { return _fields; }, diff --git a/src/types/form.ts b/src/types/form.ts index a5916f9e18e3..c577cdf6ff0a 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -113,6 +113,7 @@ export type UseFormProps< reValidateMode: Exclude; defaultValues: DefaultValues | AsyncDefaultValues; values: TFieldValues; + errors: FieldErrors; resetOptions: Parameters>[1]; resolver: Resolver; context: TContext; @@ -785,6 +786,7 @@ export type Control< _getFieldArray: ( name: InternalFieldName, ) => Partial[]; + _setErrors: (errors: FieldErrors) => void; _updateDisabledField: ( props: { disabled?: boolean; diff --git a/src/useForm.ts b/src/useForm.ts index b63067fba5b7..24c332d972ea 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -65,7 +65,7 @@ export function useForm< submitCount: 0, dirtyFields: {}, touchedFields: {}, - errors: {}, + errors: props.errors || {}, disabled: false, defaultValues: isFunction(props.defaultValues) ? undefined @@ -127,6 +127,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();