@@ -131,27 +131,37 @@ export function formEnhance<T extends AnyZodObject, M>(
131131 const errors = errs as SuperForm < T , M > [ 'errors' ] ;
132132
133133 async function validateChange (
134- change : string [ ] ,
134+ validityEl : HTMLElement ,
135135 event : 'blur' | 'input' ,
136- validityEl : HTMLElement | null
136+ errors : string [ ] | undefined
137137 ) {
138+ if ( ! options . customValidity ) return ;
138139 if ( options . validationMethod == 'submit-only' ) return ;
139140
140- if ( options . customValidity && validityEl ) {
141- // Always reset validity, in case it has been validated on the server.
142- if ( 'setCustomValidity' in validityEl ) {
143- ( validityEl as HTMLInputElement ) . setCustomValidity ( '' ) ;
144- }
141+ // Always reset validity, in case it has been validated on the server.
142+ if ( 'setCustomValidity' in validityEl ) {
143+ ( validityEl as HTMLInputElement ) . setCustomValidity ( '' ) ;
144+ }
145145
146- if ( event == 'input' && options . validationMethod == 'onblur' ) return ;
146+ if ( event == 'input' && options . validationMethod == 'onblur' ) return ;
147147
148- // If event is input but element shouldn't use custom validity,
149- // return immediately since validateField don't have to be called
150- // in this case, validation is happening elsewhere.
151- if ( noCustomValidityDataAttribute in validityEl . dataset )
152- if ( event == 'input' ) return ;
153- else validityEl = null ;
154- }
148+ // If event is input but element shouldn't use custom validity,
149+ // return immediately since validateField don't have to be called
150+ // in this case, validation is happening elsewhere.
151+ if ( noCustomValidityDataAttribute in validityEl . dataset ) return ;
152+
153+ setCustomValidity ( validityEl as HTMLInputElement , errors ) ;
154+ }
155+
156+ // Called upon an event from a HTML element that affects the form.
157+ async function htmlInputChange (
158+ change : string [ ] ,
159+ event : 'blur' | 'input' ,
160+ target : HTMLElement | null
161+ ) {
162+ if ( options . validationMethod == 'submit-only' ) return ;
163+
164+ console . log ( 'htmlInputChange' , change , event , target ) ;
155165
156166 const result = await validateField (
157167 change ,
@@ -161,25 +171,33 @@ export function formEnhance<T extends AnyZodObject, M>(
161171 tainted
162172 ) ;
163173
164- if ( validityEl ) {
165- setCustomValidity ( validityEl as HTMLInputElement , result . errors ) ;
166- }
174+ // Update data if target exists (immediate is set, refactor please)
175+ if ( result . data && target ) data . set ( result . data ) ;
167176
168- // NOTE: Uncomment if Zod transformations should be immediately applied, not just when submitting.
169- // Not enabled because it's not great UX, and it's rare to have transforms, which will just result in
170- // redundant store updates.
171- //if (result.data) data.set(result.data);
177+ if ( options . customValidity ) {
178+ const name = CSS . escape ( mergePath ( change ) ) ;
179+ const el = formEl . querySelector < HTMLElement > ( `[name="${ name } "]` ) ;
180+ if ( el ) validateChange ( el , event , result . errors ) ;
181+ }
172182 }
173183
184+ const immediateInputTypes = [
185+ 'button' ,
186+ 'checkbox' ,
187+ 'radio' ,
188+ 'range' ,
189+ 'submit'
190+ ] ;
191+
174192 /**
175193 * Some input fields have timing issues with the stores, need to wait in that case.
176194 */
177- function timingIssue ( el : EventTarget | null ) {
195+ function immediateInput ( el : EventTarget | null ) {
178196 return (
179197 el &&
180198 ( el instanceof HTMLSelectElement ||
181199 ( el instanceof HTMLInputElement &&
182- ( el . type == 'radio' || el . type == 'checkbox' ) ) )
200+ immediateInputTypes . includes ( el . type ) ) )
183201 ) ;
184202 }
185203
@@ -192,61 +210,66 @@ export function formEnhance<T extends AnyZodObject, M>(
192210 return ;
193211 }
194212
195- if ( timingIssue ( e . target ) ) {
213+ const target = e . target instanceof HTMLElement ? e . target : null ;
214+ const immediateUpdate = immediateInput ( target ) ;
215+
216+ // Immediate inputs has a timing issue and needs to be waited for
217+ if ( immediateUpdate ) {
196218 await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
197219 }
198220
199221 for ( const change of get ( lastChanges ) ) {
200- let validityEl : HTMLElement | null = null ;
201-
202- if ( options . customValidity ) {
203- const name = CSS . escape ( mergePath ( change ) ) ;
204- validityEl = formEl . querySelector < HTMLElement > ( `[name="${ name } "]` ) ;
205- }
206-
207- validateChange ( change , 'blur' , validityEl ) ;
222+ htmlInputChange ( change , 'blur' , immediateUpdate ? null : target ) ;
208223 }
209224 // Clear last changes after blur (not after input)
210225 lastChanges . set ( [ ] ) ;
211226 }
212227 formEl . addEventListener ( 'focusout' , checkBlur ) ;
213228
214229 // Add input event, for custom validity
215- async function checkCustomValidity ( e : Event ) {
230+ async function checkInput ( e : Event ) {
216231 if (
217232 options . validationMethod == 'onblur' ||
218233 options . validationMethod == 'submit-only'
219234 ) {
220235 return ;
221236 }
222237
223- if ( timingIssue ( e . target ) ) {
238+ const immediateUpdate = immediateInput ( e . target ) ;
239+
240+ if ( immediateUpdate ) {
224241 await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
225242 }
226243
227- for ( const change of get ( lastChanges ) ) {
228- const name = CSS . escape ( mergePath ( change ) ) ;
229- const validityEl = formEl . querySelector < HTMLElement > (
230- `[name="${ name } "]`
231- ) ;
232- if ( ! validityEl ) continue ;
244+ const target = e . target instanceof HTMLElement ? e . target : null ;
233245
234- // eslint-disable-next-line @typescript-eslint/no-explicit-any
235- const hadErrors = traversePath ( get ( errors ) , change as any ) ;
236- if ( hadErrors && hadErrors . key in hadErrors . parent ) {
246+ for ( const change of get ( lastChanges ) ) {
247+ const hadErrors =
248+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
249+ immediateUpdate || traversePath ( get ( errors ) , change as any ) ;
250+ if (
251+ immediateUpdate ||
252+ ( typeof hadErrors == 'object' && hadErrors . key in hadErrors . parent )
253+ ) {
237254 // Problem - store hasn't updated here with new value yet.
238- setTimeout ( ( ) => validateChange ( change , 'input' , validityEl ) , 0 ) ;
255+ setTimeout (
256+ ( ) =>
257+ htmlInputChange (
258+ change ,
259+ 'input' ,
260+ immediateUpdate ? target : null
261+ ) ,
262+ 0
263+ ) ;
239264 }
240265 }
241266 }
242267
243- if ( options . customValidity ) {
244- formEl . addEventListener ( 'input' , checkCustomValidity ) ;
245- }
268+ formEl . addEventListener ( 'input' , checkInput ) ;
246269
247270 onDestroy ( ( ) => {
248271 formEl . removeEventListener ( 'focusout' , checkBlur ) ;
249- formEl . removeEventListener ( 'input' , checkCustomValidity ) ;
272+ formEl . removeEventListener ( 'input' , checkInput ) ;
250273 } ) ;
251274
252275 const htmlForm = Form ( formEl , { submitting, delayed, timeout } , options ) ;
0 commit comments