@@ -7,7 +7,8 @@ import { browser } from '$app/environment';
77import {
88 SuperFormError ,
99 type TaintedFields ,
10- type SuperValidated
10+ type SuperValidated ,
11+ type ZodValidation
1112} from '../index.js' ;
1213import type { z , AnyZodObject } from 'zod' ;
1314import { stringify } from 'devalue' ;
@@ -16,6 +17,8 @@ import type { FormOptions, SuperForm } from './index.js';
1617import { clientValidation , validateField } from './clientValidation.js' ;
1718import { Form } from './form.js' ;
1819import { onDestroy } from 'svelte' ;
20+ import { traversePath } from '$lib/traversal.js' ;
21+ import { mergePath , splitPath } from '$lib/stringPath.js' ;
1922
2023export type FormUpdate = (
2124 result : Exclude < ActionResult , { type : 'error' } > ,
@@ -61,6 +64,34 @@ export function shouldSyncFlash<T extends AnyZodObject, M>(
6164 return options . syncFlashMessage ;
6265}
6366
67+ ///// Custom validity /////
68+
69+ const noCustomValidityDataAttribute = 'noCustomValidity' ;
70+
71+ function setCustomValidity (
72+ el : HTMLInputElement ,
73+ errors : string [ ] | undefined
74+ ) {
75+ const message = errors && errors . length ? errors . join ( '\n' ) : '' ;
76+ el . setCustomValidity ( message ) ;
77+ if ( message ) el . reportValidity ( ) ;
78+ }
79+
80+ function setCustomValidityForm < T extends AnyZodObject , M > (
81+ formEl : HTMLFormElement ,
82+ errors : SuperValidated < ZodValidation < T > , M > [ 'errors' ]
83+ ) {
84+ for ( const el of formEl . querySelectorAll ( 'input' ) ) {
85+ if ( noCustomValidityDataAttribute in el . dataset ) continue ;
86+
87+ const error = traversePath ( errors , splitPath ( el . name ) ) ;
88+ setCustomValidity ( el , error ?. value ) ;
89+ if ( error ?. value ) return ;
90+ }
91+ }
92+
93+ //////////////////////////////////
94+
6495/**
6596 * Custom use:enhance version. Flash message support, friendly error messages, for usage with initializeForm.
6697 * @param formEl Form element from the use:formEnhance default parameter.
@@ -92,15 +123,47 @@ export function formEnhance<T extends AnyZodObject, M>(
92123 // Using this type in the function argument causes a type recursion error.
93124 const errors = errs as SuperForm < T , M > [ 'errors' ] ;
94125
95- async function validateChange ( change : string [ ] ) {
96- await validateField ( change , options , data , errors , tainted ) ;
126+ async function validateChange (
127+ change : string [ ] ,
128+ event : 'blur' | 'input' ,
129+ validityEl : HTMLElement | null
130+ ) {
131+ if ( options . customValidity && validityEl ) {
132+ // Always reset validity, in case it has been validated on the server.
133+ if ( 'setCustomValidity' in validityEl ) {
134+ ( validityEl as HTMLInputElement ) . setCustomValidity ( '' ) ;
135+ }
136+
137+ // If event is input but element shouldn't use custom validity,
138+ // return immediately since validateField don't have to be called
139+ // in this case, validation is happening elsewhere.
140+ if ( noCustomValidityDataAttribute in validityEl . dataset )
141+ if ( event == 'input' ) return ;
142+ else validityEl = null ;
143+ }
144+
145+ const newErrors = await validateField (
146+ change ,
147+ options ,
148+ data ,
149+ errors ,
150+ tainted
151+ ) ;
152+
153+ if ( validityEl ) {
154+ setCustomValidity ( validityEl as any , newErrors ) ;
155+ }
97156 }
98157
158+ /**
159+ * Some input fields have timing issues with the stores, need to wait in that case.
160+ */
99161 function timingIssue ( el : EventTarget | null ) {
100162 return (
101163 el &&
102164 ( el instanceof HTMLSelectElement ||
103- ( el instanceof HTMLInputElement && el . type == 'radio' ) )
165+ ( el instanceof HTMLInputElement &&
166+ ( el . type == 'radio' || el . type == 'checkbox' ) ) )
104167 ) ;
105168 }
106169
@@ -113,26 +176,56 @@ export function formEnhance<T extends AnyZodObject, M>(
113176 return ;
114177 }
115178
116- // Some form fields have some timing issue, need to wait
117179 if ( timingIssue ( e . target ) ) {
118180 await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
119181 }
120182
121183 for ( const change of get ( lastChanges ) ) {
122- //console.log('🚀 ~ file: index.ts:905 ~ BLUR:', change);
123- validateChange ( change ) ;
184+ let validityEl : HTMLElement | null = null ;
185+
186+ if ( options . customValidity ) {
187+ const name = CSS . escape ( mergePath ( change ) ) ;
188+ validityEl = formEl . querySelector < HTMLElement > ( `[name="${ name } "]` ) ;
189+ }
190+
191+ validateChange ( change , 'blur' , validityEl ) ;
124192 }
125193 // Clear last changes after blur (not after input)
126194 lastChanges . set ( [ ] ) ;
127195 }
128196 formEl . addEventListener ( 'focusout' , checkBlur ) ;
129197
130- const htmlForm = Form ( formEl , { submitting, delayed, timeout } , options ) ;
198+ // Add input event, for custom validity
199+ async function checkCustomValidity ( e : Event ) {
200+ if ( timingIssue ( e . target ) ) {
201+ await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
202+ }
203+
204+ for ( const change of get ( lastChanges ) ) {
205+ const name = CSS . escape ( mergePath ( change ) ) ;
206+ const validityEl = formEl . querySelector < HTMLElement > (
207+ `[name="${ name } "]`
208+ ) ;
209+ if ( ! validityEl ) continue ;
210+
211+ const hadErrors = traversePath ( get ( errors ) , change as any ) ;
212+ if ( hadErrors && hadErrors . key in hadErrors . parent ) {
213+ // Problem - store hasn't updated here with new value yet.
214+ setTimeout ( ( ) => validateChange ( change , 'input' , validityEl ) , 0 ) ;
215+ }
216+ }
217+ }
218+ if ( options . customValidity ) {
219+ formEl . addEventListener ( 'input' , checkCustomValidity ) ;
220+ }
131221
132222 onDestroy ( ( ) => {
133223 formEl . removeEventListener ( 'focusout' , checkBlur ) ;
224+ formEl . removeEventListener ( 'input' , checkCustomValidity ) ;
134225 } ) ;
135226
227+ const htmlForm = Form ( formEl , { submitting, delayed, timeout } , options ) ;
228+
136229 let currentRequest : AbortController | null ;
137230
138231 return enhance ( formEl , async ( submit ) => {
@@ -319,6 +412,10 @@ export function formEnhance<T extends AnyZodObject, M>(
319412 for ( const event of formEvents . onUpdate ) {
320413 await event ( data ) ;
321414 }
415+
416+ if ( ! cancelled && options . customValidity ) {
417+ setCustomValidityForm ( formEl , data . form . errors ) ;
418+ }
322419 }
323420 }
324421
0 commit comments