diff --git a/.changeset/hungry-seals-bathe.md b/.changeset/hungry-seals-bathe.md new file mode 100644 index 000000000..270362e21 --- /dev/null +++ b/.changeset/hungry-seals-bathe.md @@ -0,0 +1,6 @@ +--- +'formik': patch +'formik-native': patch +--- + +Validate `setFieldTouched` with high priority diff --git a/app/pages/sign-in.js b/app/pages/sign-in.js index 5caf2d21a..65a679610 100644 --- a/app/pages/sign-in.js +++ b/app/pages/sign-in.js @@ -1,12 +1,16 @@ import React, { useEffect, useState } from 'react'; import { ErrorMessage, Field, Form, FormikProvider, useFormik } from 'formik'; import * as Yup from 'yup'; +import { useRouter } from 'next/router'; const SignIn = () => { + const router = useRouter(); const [errorLog, setErrorLog] = useState([]); const formik = useFormik({ - validateOnMount: true, + validateOnMount: router.query.validateOnMount === 'true', + validateOnBlur: router.query.validateOnBlur !== 'false', + validateOnChange: router.query.validateOnChange !== 'false', initialValues: { username: '', password: '' }, validationSchema: Yup.object().shape({ username: Yup.string().required('Required'), @@ -69,6 +73,15 @@ const SignIn = () => { Submit + +
{JSON.stringify(errorLog, null, 2)}
diff --git a/cypress/integration/basic.spec.ts b/cypress/integration/basic.spec.ts index 618eabfe8..9e119100b 100644 --- a/cypress/integration/basic.spec.ts +++ b/cypress/integration/basic.spec.ts @@ -15,7 +15,7 @@ describe('basic validation', () => { cy.get('#renderCounter').contains('0'); }); - it('should validate show errors on blur', () => { + it('should validate show errors on change and blur', () => { cy.visit('http://localhost:3000/sign-in'); cy.get('input[name="username"]') @@ -33,6 +33,40 @@ describe('basic validation', () => { cy.get('#error-log').should('have.text', '[]'); }); + it('should validate show errors on blur only', () => { + cy.visit('http://localhost:3000/sign-in', { + qs: { + validateOnMount: false, + validateOnChange: false, + }, + }); + + cy.get('input[name="username"]') + .type('john') + .blur() + .siblings('p') + .should('have.length', 0); + + cy.get('input[name="password"]') + .type('123') + .blur() + .siblings('p') + .should('have.length', 0); + + cy.get('#error-log').should( + 'have.text', + JSON.stringify( + [ + // It will quickly flash after `password` blur because `yup` schema + // validation is async. + { name: 'password', value: '123', error: 'Required' }, + ], + null, + 2 + ) + ); + }); + it('should validate autofill', () => { // React overrides `input.value` setters, so we have to call // native input setter diff --git a/packages/formik/src/Formik.tsx b/packages/formik/src/Formik.tsx index d9e95ee04..8a5afc9cc 100755 --- a/packages/formik/src/Formik.tsx +++ b/packages/formik/src/Formik.tsx @@ -728,7 +728,7 @@ export function useFormik({ const willValidate = shouldValidate === undefined ? validateOnBlur : shouldValidate; return willValidate - ? validateFormWithLowPriority(state.values) + ? validateFormWithHighPriority(state.values) : Promise.resolve(); } ); diff --git a/packages/formik/test/Formik.test.tsx b/packages/formik/test/Formik.test.tsx index 1354597f7..2ddaa6e98 100644 --- a/packages/formik/test/Formik.test.tsx +++ b/packages/formik/test/Formik.test.tsx @@ -1625,63 +1625,5 @@ describe('', () => { expect(renderedErrors).toHaveLength(0); }); - - it('bails low priority validations on blur', async () => { - const { validate, getByRole, renderedErrors } = renderForm({ - validateOnChange: false, - initialValues: { name: '' }, - }); - - expect(validate).not.toBeCalled(); - - act(() => { - fireEvent.change(getByRole('textbox'), { - persist: noop, - target: { name: 'name', value: 'i' }, - }); - }); - - act(() => { - fireEvent.blur(getByRole('textbox')); - }); - - expect(validate).not.toBeCalled(); - expect(renderedErrors).toHaveLength(0); - - act(() => { - fireEvent.change(getByRole('textbox'), { - persist: noop, - target: { name: 'name', value: 'ian' }, - }); - }); - - act(() => { - fireEvent.blur(getByRole('textbox')); - }); - - expect(validate).not.toBeCalled(); - expect(renderedErrors).toHaveLength(0); - - act(() => { - fireEvent.submit(getByRole('form')); - }); - - expect(validate).toBeCalledTimes(1); - expect(renderedErrors).toHaveLength(0); - - await waitFor(() => { - expect(validate).toBeCalledTimes(3); - expect(validate.mock.calls).toEqual([ - // Triggered by submit - [{ name: 'ian' }, undefined], - // Scheduled on first blur - [{ name: 'i' }, undefined], - // Scheduled on second blur - [{ name: 'ian' }, undefined], - ]); - }); - - expect(renderedErrors).toHaveLength(0); - }); }); }); diff --git a/packages/formik/types/index.d.ts b/packages/formik/types/index.d.ts index 74ad6734e..75d92d59e 100644 --- a/packages/formik/types/index.d.ts +++ b/packages/formik/types/index.d.ts @@ -33,4 +33,33 @@ declare module 'deepmerge' { function all(objects: Array>, options?: Options): T; } } -declare module 'scheduler'; + +declare module 'scheduler' { + export const unstable_NoPriority = 0; + export const unstable_ImmediatePriority = 1; + export const unstable_UserBlockingPriority = 2; + export const unstable_NormalPriority = 3; + export const unstable_LowPriority = 4; + export const unstable_IdlePriority = 5; + + export function unstable_runWithPriority( + priorityLevel: number, + eventHandler: () => T + ): T; + + export interface Task { + id: number; + } + + export interface ScheduleCallbackOptions { + delay?: number; + } + + export function unstable_scheduleCallback( + priorityLevel: number, + callback: () => void, + options?: ScheduleCallbackOptions + ): Task; + + export function unstable_cancelCallback(task: Task): void; +}