@@ -2,6 +2,8 @@ import { describe, expect, it, vi } from 'vitest'
22import { z } from 'zod'
33import { FieldApi , FormApi } from '../src/index'
44import { defaultValidationLogic , revalidateLogic } from '../src/ValidationLogic'
5+ import { sleep } from './utils'
6+ import type { ValidationLogicFn , ValidationLogicProps , ValidationLogicValidatorsFn } from '../src/ValidationLogic'
57
68describe ( 'custom validation' , ( ) => {
79 it ( 'should handle default validation logic' , async ( ) => {
@@ -233,4 +235,172 @@ describe('custom validation', () => {
233235
234236 expect ( field . state . meta . errorMap . onDynamic ) . toBe ( undefined )
235237 } )
238+
239+ describe ( 'customised field-level validation logic' , ( ) => {
240+ const validationLogic : ValidationLogicFn = ( props ) => {
241+ const validatorNames = Object . keys ( props . validators ?? { } )
242+ if ( validatorNames . length === 0 ) {
243+ // No validators is a valid case, just return
244+ return props . runValidation ( {
245+ validators : [ ] ,
246+ form : props . form ,
247+ } )
248+ }
249+
250+ let validators : ValidationLogicValidatorsFn [ ] = [ ]
251+ defaultValidationLogic ( {
252+ ...props ,
253+ runValidation : ( vProps ) => {
254+ validators = vProps . validators . slice ( ) as ValidationLogicValidatorsFn [ ]
255+ }
256+ } )
257+
258+ let addDynamicValidator = props . event . type === 'submit'
259+ if ( ! addDynamicValidator ) {
260+ const hasFormSubmitted = props . form . state . submissionAttempts > 0
261+ const modesToWatch : ValidationLogicProps [ 'event' ] [ 'type' ] [ ] = hasFormSubmitted ? [ 'change' ] : ( props . event . fieldName ? (
262+ props . form . state . fieldMeta [ props . event . fieldName ] ?. isBlurred ? [ 'change' , 'blur' ] : [ 'blur' ]
263+ ) : [ 'blur' ] )
264+ addDynamicValidator = modesToWatch . includes ( props . event . type )
265+ }
266+ if ( addDynamicValidator ) {
267+ validators . push ( {
268+ fn : props . event . async
269+ ? props . validators ! [ 'onDynamicAsync' ]
270+ : props . validators ! [ 'onDynamic' ] ,
271+ cause : 'dynamic' ,
272+ } )
273+ }
274+
275+ return props . runValidation ( {
276+ validators,
277+ form : props . form ,
278+ } )
279+ }
280+
281+ it ( 'should support sync validation' , async ( ) => {
282+ const form = new FormApi ( {
283+ defaultValues : {
284+ firstName : '' ,
285+ lastName : '' ,
286+ } ,
287+ validationLogic,
288+ } )
289+ form . mount ( )
290+
291+ const fieldFirstName = new FieldApi ( {
292+ form,
293+ name : 'firstName' ,
294+ validators : {
295+ onDynamic : ( { value } ) => value . length >= 3 ? undefined : 'First name must be at least 3 characters long'
296+ } ,
297+ } )
298+ fieldFirstName . mount ( )
299+
300+ const fieldLastName = new FieldApi ( {
301+ form,
302+ name : 'lastName' ,
303+ validators : {
304+ onDynamic : ( { value } ) => value . length >= 3 ? undefined : 'Last name must be at least 3 characters long'
305+ } ,
306+ } )
307+ fieldLastName . mount ( )
308+
309+ expect ( fieldFirstName . state . value ) . toBe ( '' )
310+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
311+
312+ // Should not validate on change initially
313+ fieldFirstName . handleChange ( 'Jo' )
314+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
315+
316+ // But validation should occur immediately on blur
317+ fieldFirstName . handleBlur ( )
318+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( 'First name must be at least 3 characters long' )
319+
320+ // And after that point, validation should occur on change
321+ fieldFirstName . handleChange ( 'Matt' )
322+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
323+
324+ // This field hasn't been touched, so it shouldn't have an error
325+ expect ( fieldLastName . state . value ) . toBe ( '' )
326+ expect ( fieldLastName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
327+
328+ // But after form submission, it should immediately have an error
329+ await form . handleSubmit ( )
330+ expect ( fieldLastName . state . meta . errorMap . onDynamic ) . toBe ( 'Last name must be at least 3 characters long' )
331+
332+ // And it should immediately validate on change now
333+ fieldLastName . handleChange ( 'Smith' )
334+ expect ( fieldLastName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
335+ } )
336+
337+ it ( 'should support async validation' , async ( ) => {
338+ vi . useFakeTimers ( )
339+
340+ const form = new FormApi ( {
341+ defaultValues : {
342+ firstName : '' ,
343+ lastName : '' ,
344+ } ,
345+ validationLogic,
346+ } )
347+ form . mount ( )
348+
349+ const fieldFirstName = new FieldApi ( {
350+ form,
351+ name : 'firstName' ,
352+ validators : {
353+ onDynamicAsync : async ( { value } ) => {
354+ await sleep ( 100 )
355+ return value . length >= 3 ? undefined : 'First name must be at least 3 characters long'
356+ } ,
357+ } ,
358+ } )
359+ fieldFirstName . mount ( )
360+
361+ const fieldLastName = new FieldApi ( {
362+ form,
363+ name : 'lastName' ,
364+ validators : {
365+ onDynamicAsync : async ( { value } ) => {
366+ await sleep ( 100 )
367+ return value . length >= 3 ? undefined : 'Last name must be at least 3 characters long'
368+ } ,
369+ } ,
370+ } )
371+ fieldLastName . mount ( )
372+
373+ expect ( fieldFirstName . state . value ) . toBe ( '' )
374+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
375+
376+ // Should not validate on change initially
377+ fieldFirstName . handleChange ( 'Jo' )
378+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
379+
380+ // But validation should occur immediately on blur
381+ fieldFirstName . handleBlur ( )
382+ await vi . runAllTimersAsync ( )
383+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( 'First name must be at least 3 characters long' )
384+
385+ // And after that point, validation should occur on change
386+ fieldFirstName . handleChange ( 'Matt' )
387+ await vi . runAllTimersAsync ( )
388+ expect ( fieldFirstName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
389+
390+ // This field hasn't been touched, so it shouldn't have an error
391+ expect ( fieldLastName . state . value ) . toBe ( '' )
392+ expect ( fieldLastName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
393+
394+ // But after form submission, it should immediately have an error
395+ const submitPromise = form . handleSubmit ( )
396+ await vi . runAllTimersAsync ( )
397+ await submitPromise
398+ expect ( fieldLastName . state . meta . errorMap . onDynamic ) . toBe ( 'Last name must be at least 3 characters long' )
399+
400+ // And it should immediately validate on change now
401+ fieldLastName . handleChange ( 'Smith' )
402+ await vi . runAllTimersAsync ( )
403+ expect ( fieldLastName . state . meta . errorMap . onDynamic ) . toBe ( undefined )
404+ } )
405+ } ) ;
236406} )
0 commit comments