From 7bfbb40748d373a06479c3d17126051bc67f6072 Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 10 Jul 2021 12:55:26 +0200 Subject: [PATCH] fix(native-validation): clear error --- .../src/__tests__/Form-native-validation.tsx | 15 +++++++ class-validator/src/class-validator.ts | 30 +++++++------ .../src/__tests__/Form-native-validation.tsx | 15 +++++++ computed-types/src/computed-types.ts | 8 +++- .../src/__tests__/Form-native-validation.tsx | 15 +++++++ io-ts/src/io-ts.ts | 15 ++++--- joi/src/__tests__/Form-native-validation.tsx | 15 +++++++ joi/src/joi.ts | 31 ++++++++------ nope/src/__tests__/Form-native-validation.tsx | 15 +++++++ nope/src/nope.ts | 12 ++++-- .../__snapshots__/toNestObject.ts.snap | 42 ------------------- src/__tests__/toNestObject.ts | 19 --------- src/__tests__/validateFieldsNatively.ts | 42 +++++++++++++++++++ src/index.ts | 1 + src/toNestError.ts | 13 ++---- src/validateFieldsNatively.ts | 23 ++++++++++ .../src/__tests__/Form-native-validation.tsx | 15 +++++++ superstruct/src/superstruct.ts | 17 +++++--- vest/src/__tests__/Form-native-validation.tsx | 17 +++++++- vest/src/vest.ts | 30 +++++++------ yup/src/__tests__/Form-native-validation.tsx | 15 +++++++ yup/src/yup.ts | 4 +- zod/src/__tests__/Form-native-validation.tsx | 15 +++++++ zod/src/zod.ts | 12 ++++-- 24 files changed, 305 insertions(+), 131 deletions(-) create mode 100644 src/__tests__/validateFieldsNatively.ts create mode 100644 src/validateFieldsNatively.ts diff --git a/class-validator/src/__tests__/Form-native-validation.tsx b/class-validator/src/__tests__/Form-native-validation.tsx index 721ef651..93b4da81 100644 --- a/class-validator/src/__tests__/Form-native-validation.tsx +++ b/class-validator/src/__tests__/Form-native-validation.tsx @@ -65,4 +65,19 @@ test("form's native validation with Class Validator", async () => { passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; expect(passwordField.validity.valid).toBe(false); expect(passwordField.validationMessage).toBe('password should not be empty'); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/class-validator/src/class-validator.ts b/class-validator/src/class-validator.ts index 1f8edc02..4eeeede0 100644 --- a/class-validator/src/class-validator.ts +++ b/class-validator/src/class-validator.ts @@ -1,5 +1,5 @@ import { FieldErrors } from 'react-hook-form'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import { plainToClass } from 'class-transformer'; import { validate, validateSync, ValidationError } from 'class-validator'; import type { Resolver } from './types'; @@ -42,17 +42,21 @@ export const classValidatorResolver: Resolver = ? validateSync : validate)(user, schemaOptions); - return rawErrors.length - ? { - values: {}, - errors: toNestError( - parseErrors( - rawErrors, - !options.shouldUseNativeValidation && - options.criteriaMode === 'all', - ), - options, + if (rawErrors.length) { + return { + values: {}, + errors: toNestError( + parseErrors( + rawErrors, + !options.shouldUseNativeValidation && + options.criteriaMode === 'all', ), - } - : { values, errors: {} }; + options, + ), + }; + } + + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + + return { values, errors: {} }; }; diff --git a/computed-types/src/__tests__/Form-native-validation.tsx b/computed-types/src/__tests__/Form-native-validation.tsx index c77dc726..c0d84002 100644 --- a/computed-types/src/__tests__/Form-native-validation.tsx +++ b/computed-types/src/__tests__/Form-native-validation.tsx @@ -67,4 +67,19 @@ test("form's native validation with computed-types", async () => { passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; expect(passwordField.validity.valid).toBe(false); expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/computed-types/src/computed-types.ts b/computed-types/src/computed-types.ts index a570e3d3..68fea7cb 100644 --- a/computed-types/src/computed-types.ts +++ b/computed-types/src/computed-types.ts @@ -1,5 +1,5 @@ import type { FieldErrors } from 'react-hook-form'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import type { ValidationError } from 'computed-types'; import type { Resolver } from './types'; @@ -26,9 +26,13 @@ const parseErrorSchema = ( export const computedTypesResolver: Resolver = (schema) => async (values, _, options) => { try { + const data = await schema(values); + + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + return { errors: {}, - values: await schema(values), + values: data, }; } catch (error) { return { diff --git a/io-ts/src/__tests__/Form-native-validation.tsx b/io-ts/src/__tests__/Form-native-validation.tsx index 40edfd81..03a94017 100644 --- a/io-ts/src/__tests__/Form-native-validation.tsx +++ b/io-ts/src/__tests__/Form-native-validation.tsx @@ -71,4 +71,19 @@ test("form's native validation with io-ts", async () => { passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; expect(passwordField.validity.valid).toBe(false); expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/io-ts/src/io-ts.ts b/io-ts/src/io-ts.ts index ef216c02..acaabc8a 100644 --- a/io-ts/src/io-ts.ts +++ b/io-ts/src/io-ts.ts @@ -1,6 +1,6 @@ import * as Either from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/function'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import errorsToRecord from './errorsToRecord'; import { Resolver } from './types'; @@ -19,9 +19,14 @@ export const ioTsResolver: Resolver = (codec) => (values, _context, options) => values: {}, errors, }), - (values) => ({ - values, - errors: {}, - }), + (values) => { + options.shouldUseNativeValidation && + validateFieldsNatively({}, options); + + return { + values, + errors: {}, + }; + }, ), ); diff --git a/joi/src/__tests__/Form-native-validation.tsx b/joi/src/__tests__/Form-native-validation.tsx index a7d677e2..90382d95 100644 --- a/joi/src/__tests__/Form-native-validation.tsx +++ b/joi/src/__tests__/Form-native-validation.tsx @@ -71,4 +71,19 @@ test("form's native validation with Joi", async () => { expect(passwordField.validationMessage).toBe( '"password" is not allowed to be empty', ); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/joi/src/joi.ts b/joi/src/joi.ts index 81bdaa91..29f69380 100644 --- a/joi/src/joi.ts +++ b/joi/src/joi.ts @@ -1,5 +1,5 @@ import { appendErrors, FieldError } from 'react-hook-form'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import type { ValidationError } from 'joi'; import { Resolver } from './types'; @@ -58,17 +58,24 @@ export const joiResolver: Resolver = } } + if (result.error) { + return { + values: {}, + errors: toNestError( + parseErrorSchema( + result.error, + !options.shouldUseNativeValidation && + options.criteriaMode === 'all', + ), + options, + ), + }; + } + + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + return { - values: result.error ? {} : result.value, - errors: result.error - ? toNestError( - parseErrorSchema( - result.error, - !options.shouldUseNativeValidation && - options.criteriaMode === 'all', - ), - options, - ) - : {}, + errors: {}, + values: result.value, }; }; diff --git a/nope/src/__tests__/Form-native-validation.tsx b/nope/src/__tests__/Form-native-validation.tsx index 761b9318..df5817e6 100644 --- a/nope/src/__tests__/Form-native-validation.tsx +++ b/nope/src/__tests__/Form-native-validation.tsx @@ -71,4 +71,19 @@ test("form's native validation with Nope", async () => { passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; expect(passwordField.validity.valid).toBe(false); expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/nope/src/nope.ts b/nope/src/nope.ts index 221bc417..2d2bcfd4 100644 --- a/nope/src/nope.ts +++ b/nope/src/nope.ts @@ -1,5 +1,5 @@ import type { FieldErrors } from 'react-hook-form'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import type { ShapeErrors } from 'nope-validator/lib/cjs/types'; import type { Resolver } from './types'; @@ -36,7 +36,11 @@ export const nopeResolver: Resolver = | ShapeErrors | undefined; - return result - ? { values: {}, errors: toNestError(parseErrors(result), options) } - : { values, errors: {} }; + if (result) { + return { values: {}, errors: toNestError(parseErrors(result), options) }; + } + + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + + return { values, errors: {} }; }; diff --git a/src/__tests__/__snapshots__/toNestObject.ts.snap b/src/__tests__/__snapshots__/toNestObject.ts.snap index 8a1f9f33..9f8d2140 100644 --- a/src/__tests__/__snapshots__/toNestObject.ts.snap +++ b/src/__tests__/__snapshots__/toNestObject.ts.snap @@ -2,16 +2,6 @@ exports[`transforms flat object to nested object 1`] = ` Object { - "n": Object { - "test": Object { - "message": "third message", - "ref": Object { - "reportValidity": [MockFunction], - "setCustomValidity": [MockFunction], - }, - "type": "rd", - }, - }, "name": Object { "message": "first message", "ref": Object { @@ -34,38 +24,6 @@ Object { exports[`transforms flat object to nested object and shouldUseNativeValidation: true 1`] = ` Object { - "n": Object { - "test": Object { - "message": "third message", - "ref": Object { - "reportValidity": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "setCustomValidity": [MockFunction] { - "calls": Array [ - Array [ - "third message", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - }, - "type": "rd", - }, - }, "name": Object { "message": "first message", "ref": Object { diff --git a/src/__tests__/toNestObject.ts b/src/__tests__/toNestObject.ts index e5a7056b..c6b36505 100644 --- a/src/__tests__/toNestObject.ts +++ b/src/__tests__/toNestObject.ts @@ -1,11 +1,9 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { Field, FieldError, InternalFieldName } from 'react-hook-form'; import { toNestError } from '../toNestError'; const flatObject: Record = { name: { type: 'st', message: 'first message' }, 'test.0.name': { type: 'nd', message: 'second message' }, - 'n.test': { type: 'rd', message: 'third message' }, }; const fields = { @@ -15,14 +13,6 @@ const fields = { setCustomValidity: jest.fn(), }, }, - n: { - test: { - ref: { - reportValidity: jest.fn(), - setCustomValidity: jest.fn(), - }, - }, - }, unused: { ref: { name: 'unusedRef' }, }, @@ -47,13 +37,4 @@ test('transforms flat object to nested object and shouldUseNativeValidation: tru expect( (fields.name.ref as HTMLInputElement).setCustomValidity, ).toHaveBeenCalledWith(flatObject.name.message); - - // @ts-expect-error - expect(fields.n?.test.ref.reportValidity).toHaveBeenCalledTimes(1); - // @ts-expect-error - expect(fields.n.test.ref.setCustomValidity).toHaveBeenCalledTimes(1); - // @ts-expect-error - expect(fields.n.test.ref.setCustomValidity).toHaveBeenCalledWith( - flatObject['n.test'].message, - ); }); diff --git a/src/__tests__/validateFieldsNatively.ts b/src/__tests__/validateFieldsNatively.ts new file mode 100644 index 00000000..9840562e --- /dev/null +++ b/src/__tests__/validateFieldsNatively.ts @@ -0,0 +1,42 @@ +import { Field, FieldError, InternalFieldName } from 'react-hook-form'; +import { validateFieldsNatively } from '../validateFieldsNatively'; + +const flatObject: Record = { + name: { type: 'st', message: 'first message' }, +}; + +const fields = { + name: { + ref: { + reportValidity: jest.fn(), + setCustomValidity: jest.fn(), + }, + }, + nd: { + ref: { + reportValidity: jest.fn(), + setCustomValidity: jest.fn(), + }, + }, +} as any as Record; + +test('validates natively fields', () => { + validateFieldsNatively(flatObject, { + fields, + shouldUseNativeValidation: true, + }); + + expect( + (fields.name.ref as HTMLInputElement).setCustomValidity, + ).toHaveBeenCalledWith(flatObject.name.message); + expect( + (fields.name.ref as HTMLInputElement).reportValidity, + ).toHaveBeenCalledTimes(1); + + expect( + (fields.nd.ref as HTMLInputElement).setCustomValidity, + ).toHaveBeenCalledWith(''); + expect( + (fields.nd.ref as HTMLInputElement).reportValidity, + ).toHaveBeenCalledTimes(1); +}); diff --git a/src/index.ts b/src/index.ts index 3e3a504b..d110470d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export * from './toNestError'; +export * from './validateFieldsNatively'; diff --git a/src/toNestError.ts b/src/toNestError.ts index 59f812e9..e651340a 100644 --- a/src/toNestError.ts +++ b/src/toNestError.ts @@ -6,25 +6,18 @@ import { Field, ResolverOptions, } from 'react-hook-form'; +import { validateFieldsNatively } from './validateFieldsNatively'; export const toNestError = ( errors: Record, options: ResolverOptions, ): FieldErrors => { + options.shouldUseNativeValidation && validateFieldsNatively(errors, options); + const fieldErrors: FieldErrors = {}; for (const path in errors) { const field = get(options.fields, path) as Field['_f'] | undefined; - // Native validation (web only) - if ( - options.shouldUseNativeValidation && - field && - 'reportValidity' in field.ref - ) { - field.ref.setCustomValidity(errors[path].message || ''); - field.ref.reportValidity(); - } - set( fieldErrors, path, diff --git a/src/validateFieldsNatively.ts b/src/validateFieldsNatively.ts new file mode 100644 index 00000000..b2b497a4 --- /dev/null +++ b/src/validateFieldsNatively.ts @@ -0,0 +1,23 @@ +import { get, FieldError, ResolverOptions } from 'react-hook-form'; + +// Native validation (web only) +export const validateFieldsNatively = ( + errors: Record, + options: ResolverOptions, +): void => { + for (const fieldPath in options.fields) { + const field = options.fields[fieldPath]; + + if (field && field.ref && 'reportValidity' in field.ref) { + const error = get(errors, fieldPath) as FieldError | undefined; + + if (error) { + field.ref.setCustomValidity(error.message || ''); + } else { + field.ref.setCustomValidity(''); + } + + field.ref.reportValidity(); + } + } +}; diff --git a/superstruct/src/__tests__/Form-native-validation.tsx b/superstruct/src/__tests__/Form-native-validation.tsx index e4277c39..7e6549ae 100644 --- a/superstruct/src/__tests__/Form-native-validation.tsx +++ b/superstruct/src/__tests__/Form-native-validation.tsx @@ -68,4 +68,19 @@ test("form's native validation with Superstruct", async () => { expect(passwordField.validationMessage).toBe( 'Expected a string with a length of `6` but received one with a length of `0`', ); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'jo'); + user.type(screen.getByPlaceholderText(/password/i), 'passwo'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/superstruct/src/superstruct.ts b/superstruct/src/superstruct.ts index 981e6bbb..8dcc74bf 100644 --- a/superstruct/src/superstruct.ts +++ b/superstruct/src/superstruct.ts @@ -1,5 +1,5 @@ import { FieldError } from 'react-hook-form'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import { StructError, validate } from 'superstruct'; import { Resolver } from './types'; @@ -18,10 +18,17 @@ export const superstructResolver: Resolver = (schema, resolverOptions) => (values, _, options) => { const result = validate(values, schema, resolverOptions); + if (result[0]) { + return { + values: {}, + errors: toNestError(parseErrorSchema(result[0]), options), + }; + } + + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + return { - values: result[1] || {}, - errors: result[0] - ? toNestError(parseErrorSchema(result[0]), options) - : {}, + values: result[1], + errors: {}, }; }; diff --git a/vest/src/__tests__/Form-native-validation.tsx b/vest/src/__tests__/Form-native-validation.tsx index 921b8dc8..998e87ca 100644 --- a/vest/src/__tests__/Form-native-validation.tsx +++ b/vest/src/__tests__/Form-native-validation.tsx @@ -19,7 +19,7 @@ const validationSuite = vest.create('form', (data: FormData) => { }); vest.test('password', PASSWORD_SYMBOL_MESSAGE, () => { - vest.enforce(data.password).matches(/[^A-Za-z0-9]/); + vest.enforce(data.password).isNotEmpty(); }); }); @@ -75,4 +75,19 @@ test("form's native validation with Vest", async () => { passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; expect(passwordField.validity.valid).toBe(false); expect(passwordField.validationMessage).toBe(PASSWORD_SYMBOL_MESSAGE); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/vest/src/vest.ts b/vest/src/vest.ts index 1477a3ef..7860362b 100644 --- a/vest/src/vest.ts +++ b/vest/src/vest.ts @@ -1,4 +1,4 @@ -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import { FieldError } from 'react-hook-form'; import promisify from 'vest/promisify'; import type { VestErrors, Resolver } from './types'; @@ -31,17 +31,21 @@ export const vestResolver: Resolver = ? schema(values) : await promisify(schema)(values); - return result.hasErrors() - ? { - values: {}, - errors: toNestError( - parseErrorSchema( - result.getErrors(), - !options.shouldUseNativeValidation && - options.criteriaMode === 'all', - ), - options, + if (result.hasErrors()) { + return { + values: {}, + errors: toNestError( + parseErrorSchema( + result.getErrors(), + !options.shouldUseNativeValidation && + options.criteriaMode === 'all', ), - } - : { values, errors: {} }; + options, + ), + }; + } + + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + + return { values, errors: {} }; }; diff --git a/yup/src/__tests__/Form-native-validation.tsx b/yup/src/__tests__/Form-native-validation.tsx index 95b7852c..e095ef94 100644 --- a/yup/src/__tests__/Form-native-validation.tsx +++ b/yup/src/__tests__/Form-native-validation.tsx @@ -67,4 +67,19 @@ test("form's native validation with Yup", async () => { passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; expect(passwordField.validity.valid).toBe(false); expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/yup/src/yup.ts b/yup/src/yup.ts index 474f1fb1..9a642366 100644 --- a/yup/src/yup.ts +++ b/yup/src/yup.ts @@ -1,5 +1,5 @@ import Yup from 'yup'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import { appendErrors, FieldError } from 'react-hook-form'; import { Resolver } from './types'; @@ -53,6 +53,8 @@ export const yupResolver: Resolver = Object.assign({ abortEarly: false }, schemaOptions, { context }), ); + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + return { values: result, errors: {}, diff --git a/zod/src/__tests__/Form-native-validation.tsx b/zod/src/__tests__/Form-native-validation.tsx index c09f397a..c6f5f4bf 100644 --- a/zod/src/__tests__/Form-native-validation.tsx +++ b/zod/src/__tests__/Form-native-validation.tsx @@ -67,4 +67,19 @@ test("form's native validation with Zod", async () => { passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; expect(passwordField.validity.valid).toBe(false); expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE); + + await act(async () => { + user.type(screen.getByPlaceholderText(/username/i), 'joe'); + user.type(screen.getByPlaceholderText(/password/i), 'password'); + }); + + // username + usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement; + expect(usernameField.validity.valid).toBe(true); + expect(usernameField.validationMessage).toBe(''); + + // password + passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement; + expect(passwordField.validity.valid).toBe(true); + expect(passwordField.validationMessage).toBe(''); }); diff --git a/zod/src/zod.ts b/zod/src/zod.ts index aadd5a6c..2b395c05 100644 --- a/zod/src/zod.ts +++ b/zod/src/zod.ts @@ -1,6 +1,6 @@ import { appendErrors, FieldError } from 'react-hook-form'; import * as z from 'zod'; -import { toNestError } from '@hookform/resolvers'; +import { toNestError, validateFieldsNatively } from '@hookform/resolvers'; import type { Resolver } from './types'; const parseErrorSchema = ( @@ -57,11 +57,15 @@ export const zodResolver: Resolver = (schema, schemaOptions, resolverOptions = {}) => async (values, _, options) => { try { + const data = await schema[ + resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync' + ](values, schemaOptions); + + options.shouldUseNativeValidation && validateFieldsNatively({}, options); + return { errors: {}, - values: await schema[ - resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync' - ](values, schemaOptions), + values: data, }; } catch (error) { return {