pulse-js-framework 1.7.5 → 1.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -392
- package/cli/dev.js +14 -0
- package/cli/docs-test.js +633 -0
- package/cli/index.js +313 -31
- package/cli/lint.js +13 -4
- package/cli/logger.js +32 -4
- package/cli/release.js +50 -20
- package/compiler/parser.js +1 -1
- package/package.json +11 -4
- package/runtime/dom-advanced.js +357 -0
- package/runtime/dom-binding.js +230 -0
- package/runtime/dom-conditional.js +133 -0
- package/runtime/dom-element.js +142 -0
- package/runtime/dom-lifecycle.js +178 -0
- package/runtime/dom-list.js +267 -0
- package/runtime/dom-selector.js +267 -0
- package/runtime/dom.js +119 -1279
- package/runtime/form.js +417 -22
- package/runtime/native.js +398 -52
- package/runtime/pulse.js +1 -1
- package/runtime/router.js +6 -5
- package/runtime/store.js +81 -6
- package/types/async.d.ts +310 -0
- package/types/form.d.ts +378 -0
- package/types/index.d.ts +44 -0
- /package/{core → runtime}/errors.js +0 -0
package/runtime/form.js
CHANGED
|
@@ -23,6 +23,15 @@ import { pulse, effect, computed, batch } from './pulse.js';
|
|
|
23
23
|
* @property {string} [message] - Default error message
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Check if a validator is async
|
|
28
|
+
* @param {ValidationRule} rule - Validation rule
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function isAsyncValidator(rule) {
|
|
32
|
+
return rule.async === true;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Built-in validation rules
|
|
28
37
|
*/
|
|
@@ -161,6 +170,112 @@ export const validators = {
|
|
|
161
170
|
}
|
|
162
171
|
return true;
|
|
163
172
|
}
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Async Validators
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Async custom validation function
|
|
181
|
+
* @param {function(any, Object): Promise<boolean|string>} fn - Async validation function
|
|
182
|
+
* @param {Object} [options] - Options
|
|
183
|
+
* @param {number} [options.debounce=300] - Debounce delay in ms
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* validators.asyncCustom(async (value) => {
|
|
187
|
+
* const exists = await checkUsername(value);
|
|
188
|
+
* return exists ? 'Username already taken' : true;
|
|
189
|
+
* })
|
|
190
|
+
*/
|
|
191
|
+
asyncCustom: (fn, options = {}) => ({
|
|
192
|
+
async: true,
|
|
193
|
+
debounce: options.debounce ?? 300,
|
|
194
|
+
validate: fn
|
|
195
|
+
}),
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Async email validation (check availability via API)
|
|
199
|
+
* @param {function(string): Promise<boolean>} checkFn - Returns true if email is available
|
|
200
|
+
* @param {string} [message='Email is already taken']
|
|
201
|
+
* @param {Object} [options] - Options
|
|
202
|
+
* @param {number} [options.debounce=300] - Debounce delay in ms
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* validators.asyncEmail(
|
|
206
|
+
* async (email) => {
|
|
207
|
+
* const res = await fetch(`/api/check-email?email=${email}`);
|
|
208
|
+
* const { available } = await res.json();
|
|
209
|
+
* return available;
|
|
210
|
+
* },
|
|
211
|
+
* 'This email is already registered'
|
|
212
|
+
* )
|
|
213
|
+
*/
|
|
214
|
+
asyncEmail: (checkFn, message = 'Email is already taken', options = {}) => ({
|
|
215
|
+
async: true,
|
|
216
|
+
debounce: options.debounce ?? 300,
|
|
217
|
+
validate: async (value, allValues) => {
|
|
218
|
+
if (!value) return true;
|
|
219
|
+
// First check format synchronously
|
|
220
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
221
|
+
if (!emailRegex.test(value)) {
|
|
222
|
+
return 'Invalid email address';
|
|
223
|
+
}
|
|
224
|
+
// Then check availability
|
|
225
|
+
const available = await checkFn(value, allValues);
|
|
226
|
+
return available ? true : message;
|
|
227
|
+
}
|
|
228
|
+
}),
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Async unique validation (check uniqueness via API)
|
|
232
|
+
* @param {function(any, Object): Promise<boolean>} checkFn - Returns true if value is unique
|
|
233
|
+
* @param {string} [message='This value is already taken']
|
|
234
|
+
* @param {Object} [options] - Options
|
|
235
|
+
* @param {number} [options.debounce=300] - Debounce delay in ms
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* validators.asyncUnique(
|
|
239
|
+
* async (username) => {
|
|
240
|
+
* const res = await fetch(`/api/check-username?q=${username}`);
|
|
241
|
+
* return (await res.json()).available;
|
|
242
|
+
* },
|
|
243
|
+
* 'Username is already taken'
|
|
244
|
+
* )
|
|
245
|
+
*/
|
|
246
|
+
asyncUnique: (checkFn, message = 'This value is already taken', options = {}) => ({
|
|
247
|
+
async: true,
|
|
248
|
+
debounce: options.debounce ?? 300,
|
|
249
|
+
validate: async (value, allValues) => {
|
|
250
|
+
if (!value) return true;
|
|
251
|
+
const unique = await checkFn(value, allValues);
|
|
252
|
+
return unique ? true : message;
|
|
253
|
+
}
|
|
254
|
+
}),
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Async server-side validation
|
|
258
|
+
* @param {function(any, Object): Promise<string|null>} validateFn - Returns error message or null
|
|
259
|
+
* @param {Object} [options] - Options
|
|
260
|
+
* @param {number} [options.debounce=300] - Debounce delay in ms
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* validators.asyncServer(async (value) => {
|
|
264
|
+
* const res = await fetch('/api/validate', {
|
|
265
|
+
* method: 'POST',
|
|
266
|
+
* body: JSON.stringify({ value })
|
|
267
|
+
* });
|
|
268
|
+
* const { error } = await res.json();
|
|
269
|
+
* return error; // null if valid, error message if invalid
|
|
270
|
+
* })
|
|
271
|
+
*/
|
|
272
|
+
asyncServer: (validateFn, options = {}) => ({
|
|
273
|
+
async: true,
|
|
274
|
+
debounce: options.debounce ?? 300,
|
|
275
|
+
validate: async (value, allValues) => {
|
|
276
|
+
const error = await validateFn(value, allValues);
|
|
277
|
+
return error === null ? true : error;
|
|
278
|
+
}
|
|
164
279
|
})
|
|
165
280
|
};
|
|
166
281
|
|
|
@@ -213,28 +328,123 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
213
328
|
const fields = {};
|
|
214
329
|
const fieldNames = Object.keys(initialValues);
|
|
215
330
|
|
|
331
|
+
// Version counters for async validation race condition handling
|
|
332
|
+
const validationVersions = {};
|
|
333
|
+
const debounceTimers = {};
|
|
334
|
+
|
|
216
335
|
for (const name of fieldNames) {
|
|
217
336
|
const initialValue = initialValues[name];
|
|
218
337
|
const rules = validationSchema[name] || [];
|
|
219
338
|
|
|
339
|
+
// Separate sync and async rules
|
|
340
|
+
const syncRules = rules.filter(r => !isAsyncValidator(r));
|
|
341
|
+
const asyncRules = rules.filter(r => isAsyncValidator(r));
|
|
342
|
+
|
|
220
343
|
const value = pulse(initialValue);
|
|
221
344
|
const error = pulse(null);
|
|
222
345
|
const touched = pulse(false);
|
|
346
|
+
const validating = pulse(false);
|
|
223
347
|
const dirty = computed(() => value.get() !== initialValue);
|
|
224
|
-
const valid = computed(() => error.get() === null);
|
|
348
|
+
const valid = computed(() => error.get() === null && !validating.get());
|
|
225
349
|
|
|
226
|
-
//
|
|
227
|
-
|
|
350
|
+
// Initialize version counter
|
|
351
|
+
validationVersions[name] = 0;
|
|
352
|
+
debounceTimers[name] = new Map();
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Run sync validators only
|
|
356
|
+
*/
|
|
357
|
+
const validateFieldSync = () => {
|
|
228
358
|
const currentValue = value.get();
|
|
229
359
|
const allValues = getValues();
|
|
230
360
|
|
|
231
|
-
for (const rule of
|
|
361
|
+
for (const rule of syncRules) {
|
|
232
362
|
const result = rule.validate(currentValue, allValues);
|
|
233
363
|
if (result !== true) {
|
|
234
364
|
error.set(typeof result === 'string' ? result : rule.message || 'Invalid');
|
|
235
365
|
return false;
|
|
236
366
|
}
|
|
237
367
|
}
|
|
368
|
+
return true;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Run async validators with debouncing
|
|
373
|
+
*/
|
|
374
|
+
const validateFieldAsync = async () => {
|
|
375
|
+
if (asyncRules.length === 0) return true;
|
|
376
|
+
|
|
377
|
+
const version = ++validationVersions[name];
|
|
378
|
+
validating.set(true);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const currentValue = value.get();
|
|
382
|
+
const allValues = getValues();
|
|
383
|
+
|
|
384
|
+
for (const rule of asyncRules) {
|
|
385
|
+
// Cancel previous debounce timer for this rule
|
|
386
|
+
const existingTimer = debounceTimers[name].get(rule);
|
|
387
|
+
if (existingTimer) {
|
|
388
|
+
clearTimeout(existingTimer);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Debounce async validation
|
|
392
|
+
await new Promise((resolve) => {
|
|
393
|
+
const timer = setTimeout(resolve, rule.debounce || 300);
|
|
394
|
+
debounceTimers[name].set(rule, timer);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Check if this validation is still current
|
|
398
|
+
if (version !== validationVersions[name]) {
|
|
399
|
+
return null; // Cancelled
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const result = await rule.validate(currentValue, allValues);
|
|
403
|
+
|
|
404
|
+
// Check again after async operation
|
|
405
|
+
if (version !== validationVersions[name]) {
|
|
406
|
+
return null; // Cancelled
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (result !== true) {
|
|
410
|
+
error.set(typeof result === 'string' ? result : rule.message || 'Invalid');
|
|
411
|
+
validating.set(false);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// All async validations passed
|
|
417
|
+
if (version === validationVersions[name]) {
|
|
418
|
+
error.set(null);
|
|
419
|
+
validating.set(false);
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (version === validationVersions[name]) {
|
|
424
|
+
error.set(err.message || 'Validation failed');
|
|
425
|
+
validating.set(false);
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Full validation (sync + async)
|
|
433
|
+
*/
|
|
434
|
+
const validateField = async () => {
|
|
435
|
+
// First run sync validators
|
|
436
|
+
if (!validateFieldSync()) {
|
|
437
|
+
validating.set(false);
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Then run async validators
|
|
442
|
+
if (asyncRules.length > 0) {
|
|
443
|
+
const result = await validateFieldAsync();
|
|
444
|
+
if (result === null) return null; // Cancelled
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
|
|
238
448
|
error.set(null);
|
|
239
449
|
return true;
|
|
240
450
|
};
|
|
@@ -250,7 +460,14 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
250
460
|
value.set(newValue);
|
|
251
461
|
|
|
252
462
|
if (validateOnChange && (mode === 'onChange' || touched.get())) {
|
|
253
|
-
|
|
463
|
+
// Run sync validation immediately
|
|
464
|
+
validateFieldSync();
|
|
465
|
+
// Trigger async validation (debounced)
|
|
466
|
+
if (asyncRules.length > 0) {
|
|
467
|
+
validateField();
|
|
468
|
+
} else {
|
|
469
|
+
error.set(null);
|
|
470
|
+
}
|
|
254
471
|
}
|
|
255
472
|
};
|
|
256
473
|
|
|
@@ -271,15 +488,19 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
271
488
|
touched,
|
|
272
489
|
dirty,
|
|
273
490
|
valid,
|
|
491
|
+
validating,
|
|
274
492
|
validate: validateField,
|
|
493
|
+
validateSync: validateFieldSync,
|
|
275
494
|
onChange,
|
|
276
495
|
onBlur,
|
|
277
496
|
onFocus,
|
|
278
497
|
reset: () => {
|
|
498
|
+
validationVersions[name]++; // Cancel pending async validation
|
|
279
499
|
batch(() => {
|
|
280
500
|
value.set(initialValue);
|
|
281
501
|
error.set(null);
|
|
282
502
|
touched.set(false);
|
|
503
|
+
validating.set(false);
|
|
283
504
|
});
|
|
284
505
|
},
|
|
285
506
|
setError: (msg) => error.set(msg),
|
|
@@ -296,6 +517,10 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
296
517
|
return fieldNames.every(name => fields[name].valid.get());
|
|
297
518
|
});
|
|
298
519
|
|
|
520
|
+
const isValidating = computed(() => {
|
|
521
|
+
return fieldNames.some(name => fields[name].validating.get());
|
|
522
|
+
});
|
|
523
|
+
|
|
299
524
|
const isDirty = computed(() => {
|
|
300
525
|
return fieldNames.some(name => fields[name].dirty.get());
|
|
301
526
|
});
|
|
@@ -354,18 +579,41 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
354
579
|
}
|
|
355
580
|
|
|
356
581
|
/**
|
|
357
|
-
* Validate all fields
|
|
582
|
+
* Validate all fields (sync only, for immediate check)
|
|
358
583
|
*/
|
|
359
|
-
function
|
|
584
|
+
function validateAllSync() {
|
|
360
585
|
let allValid = true;
|
|
361
586
|
for (const name of fieldNames) {
|
|
362
|
-
if (!fields[name].
|
|
587
|
+
if (!fields[name].validateSync()) {
|
|
363
588
|
allValid = false;
|
|
364
589
|
}
|
|
365
590
|
}
|
|
366
591
|
return allValid;
|
|
367
592
|
}
|
|
368
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Validate all fields (sync + async)
|
|
596
|
+
* @returns {Promise<boolean>}
|
|
597
|
+
*/
|
|
598
|
+
async function validateAll() {
|
|
599
|
+
// First run all sync validations
|
|
600
|
+
let allValid = validateAllSync();
|
|
601
|
+
|
|
602
|
+
// Then run async validations in parallel
|
|
603
|
+
const asyncResults = await Promise.all(
|
|
604
|
+
fieldNames.map(name => fields[name].validate())
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// Check results (null means cancelled, which we treat as valid for now)
|
|
608
|
+
for (const result of asyncResults) {
|
|
609
|
+
if (result === false) {
|
|
610
|
+
allValid = false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return allValid;
|
|
615
|
+
}
|
|
616
|
+
|
|
369
617
|
/**
|
|
370
618
|
* Reset form to initial values
|
|
371
619
|
*/
|
|
@@ -400,7 +648,7 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
400
648
|
|
|
401
649
|
// Validate if required
|
|
402
650
|
if (validateOnSubmit) {
|
|
403
|
-
const valid = validateAll();
|
|
651
|
+
const valid = await validateAll();
|
|
404
652
|
if (!valid) {
|
|
405
653
|
if (onError) {
|
|
406
654
|
onError(errors.get());
|
|
@@ -453,6 +701,7 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
453
701
|
return {
|
|
454
702
|
fields,
|
|
455
703
|
isValid,
|
|
704
|
+
isValidating,
|
|
456
705
|
isDirty,
|
|
457
706
|
isTouched,
|
|
458
707
|
isSubmitting,
|
|
@@ -462,6 +711,7 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
462
711
|
setValues,
|
|
463
712
|
setValue,
|
|
464
713
|
validateAll,
|
|
714
|
+
validateAllSync,
|
|
465
715
|
reset,
|
|
466
716
|
handleSubmit,
|
|
467
717
|
setErrors,
|
|
@@ -474,35 +724,151 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
474
724
|
*
|
|
475
725
|
* @param {any} initialValue - Initial field value
|
|
476
726
|
* @param {ValidationRule[]} [rules=[]] - Validation rules
|
|
727
|
+
* @param {Object} [options={}] - Field options
|
|
728
|
+
* @param {boolean} [options.validateOnChange=true] - Validate on change (after touched)
|
|
729
|
+
* @param {boolean} [options.validateOnBlur=true] - Validate on blur
|
|
477
730
|
* @returns {Object} Field state and controls
|
|
478
731
|
*
|
|
479
732
|
* @example
|
|
480
733
|
* const email = useField('', [validators.required(), validators.email()]);
|
|
481
734
|
*
|
|
735
|
+
* // With async validation
|
|
736
|
+
* const username = useField('', [
|
|
737
|
+
* validators.required(),
|
|
738
|
+
* validators.asyncUnique(async (value) => checkUsernameAvailable(value))
|
|
739
|
+
* ]);
|
|
740
|
+
*
|
|
482
741
|
* // Bind to input
|
|
483
742
|
* el('input', { value: email.value.get(), onInput: email.onChange, onBlur: email.onBlur });
|
|
484
743
|
*/
|
|
485
|
-
export function useField(initialValue, rules = []) {
|
|
744
|
+
export function useField(initialValue, rules = [], options = {}) {
|
|
745
|
+
const { validateOnChange = true, validateOnBlur = true } = options;
|
|
746
|
+
|
|
486
747
|
const value = pulse(initialValue);
|
|
487
748
|
const error = pulse(null);
|
|
488
749
|
const touched = pulse(false);
|
|
750
|
+
const validating = pulse(false);
|
|
489
751
|
const dirty = computed(() => value.get() !== initialValue);
|
|
490
|
-
const valid = computed(() => error.get() === null);
|
|
752
|
+
const valid = computed(() => error.get() === null && !validating.get());
|
|
491
753
|
|
|
492
|
-
|
|
493
|
-
|
|
754
|
+
// Separate sync and async rules
|
|
755
|
+
const syncRules = rules.filter(r => !isAsyncValidator(r));
|
|
756
|
+
const asyncRules = rules.filter(r => isAsyncValidator(r));
|
|
757
|
+
|
|
758
|
+
// Version counter for race condition handling
|
|
759
|
+
let validationVersion = 0;
|
|
760
|
+
|
|
761
|
+
// Debounce timers per async rule
|
|
762
|
+
const debounceTimers = new Map();
|
|
494
763
|
|
|
495
|
-
|
|
496
|
-
|
|
764
|
+
/**
|
|
765
|
+
* Run synchronous validators
|
|
766
|
+
*/
|
|
767
|
+
const validateSync = (currentValue, allValues = {}) => {
|
|
768
|
+
for (const rule of syncRules) {
|
|
769
|
+
const result = rule.validate(currentValue, allValues);
|
|
497
770
|
if (result !== true) {
|
|
498
771
|
error.set(typeof result === 'string' ? result : rule.message || 'Invalid');
|
|
499
772
|
return false;
|
|
500
773
|
}
|
|
501
774
|
}
|
|
775
|
+
return true;
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Run async validators with debouncing and race condition handling
|
|
780
|
+
*/
|
|
781
|
+
const validateAsync = async (currentValue, allValues = {}) => {
|
|
782
|
+
if (asyncRules.length === 0) return true;
|
|
783
|
+
|
|
784
|
+
const version = ++validationVersion;
|
|
785
|
+
validating.set(true);
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
for (const rule of asyncRules) {
|
|
789
|
+
// Cancel previous debounce timer for this rule
|
|
790
|
+
const existingTimer = debounceTimers.get(rule);
|
|
791
|
+
if (existingTimer) {
|
|
792
|
+
clearTimeout(existingTimer);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Debounce async validation
|
|
796
|
+
await new Promise((resolve) => {
|
|
797
|
+
const timer = setTimeout(resolve, rule.debounce || 300);
|
|
798
|
+
debounceTimers.set(rule, timer);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Check if this validation is still current
|
|
802
|
+
if (version !== validationVersion) {
|
|
803
|
+
return null; // Cancelled
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const result = await rule.validate(currentValue, allValues);
|
|
807
|
+
|
|
808
|
+
// Check again after async operation
|
|
809
|
+
if (version !== validationVersion) {
|
|
810
|
+
return null; // Cancelled
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (result !== true) {
|
|
814
|
+
error.set(typeof result === 'string' ? result : rule.message || 'Invalid');
|
|
815
|
+
validating.set(false);
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// All async validations passed
|
|
821
|
+
if (version === validationVersion) {
|
|
822
|
+
error.set(null);
|
|
823
|
+
validating.set(false);
|
|
824
|
+
}
|
|
825
|
+
return true;
|
|
826
|
+
} catch (err) {
|
|
827
|
+
if (version === validationVersion) {
|
|
828
|
+
error.set(err.message || 'Validation failed');
|
|
829
|
+
validating.set(false);
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Full validation (sync + async)
|
|
837
|
+
*/
|
|
838
|
+
const validate = async (allValues = {}) => {
|
|
839
|
+
const currentValue = value.get();
|
|
840
|
+
|
|
841
|
+
// First run sync validators
|
|
842
|
+
if (!validateSync(currentValue, allValues)) {
|
|
843
|
+
validating.set(false);
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Then run async validators
|
|
848
|
+
if (asyncRules.length > 0) {
|
|
849
|
+
const result = await validateAsync(currentValue, allValues);
|
|
850
|
+
if (result === null) return null; // Cancelled
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
|
|
502
854
|
error.set(null);
|
|
503
855
|
return true;
|
|
504
856
|
};
|
|
505
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Sync-only validation (for immediate feedback)
|
|
860
|
+
*/
|
|
861
|
+
const validateSyncOnly = (allValues = {}) => {
|
|
862
|
+
const currentValue = value.get();
|
|
863
|
+
if (!validateSync(currentValue, allValues)) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
if (syncRules.length > 0 && asyncRules.length === 0) {
|
|
867
|
+
error.set(null);
|
|
868
|
+
}
|
|
869
|
+
return true;
|
|
870
|
+
};
|
|
871
|
+
|
|
506
872
|
const onChange = (eventOrValue) => {
|
|
507
873
|
const newValue = eventOrValue?.target
|
|
508
874
|
? (eventOrValue.target.type === 'checkbox'
|
|
@@ -512,21 +878,30 @@ export function useField(initialValue, rules = []) {
|
|
|
512
878
|
|
|
513
879
|
value.set(newValue);
|
|
514
880
|
|
|
515
|
-
if (touched.get()) {
|
|
516
|
-
|
|
881
|
+
if (validateOnChange && touched.get()) {
|
|
882
|
+
// Run sync validation immediately
|
|
883
|
+
validateSyncOnly();
|
|
884
|
+
// Trigger async validation (debounced)
|
|
885
|
+
if (asyncRules.length > 0) {
|
|
886
|
+
validate();
|
|
887
|
+
}
|
|
517
888
|
}
|
|
518
889
|
};
|
|
519
890
|
|
|
520
891
|
const onBlur = () => {
|
|
521
892
|
touched.set(true);
|
|
522
|
-
|
|
893
|
+
if (validateOnBlur) {
|
|
894
|
+
validate();
|
|
895
|
+
}
|
|
523
896
|
};
|
|
524
897
|
|
|
525
898
|
const reset = () => {
|
|
899
|
+
validationVersion++; // Cancel any pending async validation
|
|
526
900
|
batch(() => {
|
|
527
901
|
value.set(initialValue);
|
|
528
902
|
error.set(null);
|
|
529
903
|
touched.set(false);
|
|
904
|
+
validating.set(false);
|
|
530
905
|
});
|
|
531
906
|
};
|
|
532
907
|
|
|
@@ -536,7 +911,9 @@ export function useField(initialValue, rules = []) {
|
|
|
536
911
|
touched,
|
|
537
912
|
dirty,
|
|
538
913
|
valid,
|
|
914
|
+
validating,
|
|
539
915
|
validate,
|
|
916
|
+
validateSync: validateSyncOnly,
|
|
540
917
|
onChange,
|
|
541
918
|
onBlur,
|
|
542
919
|
reset,
|
|
@@ -628,12 +1005,29 @@ export function useFieldArray(initialValues = [], itemRules = []) {
|
|
|
628
1005
|
fieldsArray.set(newValues.map(createField));
|
|
629
1006
|
};
|
|
630
1007
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1008
|
+
/**
|
|
1009
|
+
* Validate all fields synchronously only
|
|
1010
|
+
*/
|
|
1011
|
+
const validateAllSync = () => {
|
|
1012
|
+
const results = fieldsArray.get().map(f => f.validateSync());
|
|
634
1013
|
return results.every(r => r);
|
|
635
1014
|
};
|
|
636
1015
|
|
|
1016
|
+
/**
|
|
1017
|
+
* Validate all fields (sync + async)
|
|
1018
|
+
*/
|
|
1019
|
+
const validateAll = async () => {
|
|
1020
|
+
// First run sync validation on all
|
|
1021
|
+
const syncResults = validateAllSync();
|
|
1022
|
+
|
|
1023
|
+
// Then run async validation on all
|
|
1024
|
+
const asyncResults = await Promise.all(
|
|
1025
|
+
fieldsArray.get().map(f => f.validate())
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
return asyncResults.every(r => r === true);
|
|
1029
|
+
};
|
|
1030
|
+
|
|
637
1031
|
return {
|
|
638
1032
|
fields: fieldsArray,
|
|
639
1033
|
values,
|
|
@@ -647,7 +1041,8 @@ export function useFieldArray(initialValues = [], itemRules = []) {
|
|
|
647
1041
|
swap,
|
|
648
1042
|
replace,
|
|
649
1043
|
reset,
|
|
650
|
-
validateAll
|
|
1044
|
+
validateAll,
|
|
1045
|
+
validateAllSync
|
|
651
1046
|
};
|
|
652
1047
|
}
|
|
653
1048
|
|