react-f0rm 0.7.0 → 0.9.0

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/dist/index.umd.js CHANGED
@@ -263,18 +263,21 @@
263
263
  const { emitter, values, deleted } = form;
264
264
  values.set(path.key, value);
265
265
  reviveBranch(deleted, path);
266
+ pruneDirtyBaselines(form, path);
267
+ if (options?.shouldDirty === false) setDirtyBaseline(form, path, value);
266
268
  bumpDirtyVersion(form);
267
269
  if (options?.shouldTouch) setTouchedByPath(form, path);
268
270
  if (options?.shouldValidate) form.validators.get(path.key)?.();
269
271
  emit(emitter, "change", path);
270
272
  }
271
- function changeValue(form, name, value) {
272
- changeValueByPath(form, create$1(name), value);
273
+ function changeValue(form, name, value, options) {
274
+ changeValueByPath(form, create$1(name), value, options);
273
275
  }
274
- function changeValueByPath(form, path, value) {
276
+ function changeValueByPath(form, path, value, options) {
277
+ if (options?.shouldDirty === false) setDirtyBaseline(form, path, value);
275
278
  const change = form.changeHandlers.get(path.key);
276
279
  if (change) change(value);
277
- else setValueByPath(form, path, value);
280
+ else setValueByPath(form, path, value, options);
278
281
  }
279
282
  function getError(form, name) {
280
283
  return getErrorByPath(form, create$1(name));
@@ -302,13 +305,15 @@
302
305
  }
303
306
  function getFieldState(form, name) {
304
307
  const path = create$1(name);
305
- const { initialValues, values, touched, validating } = form;
308
+ const { values, touched, validating } = form;
306
309
  const live = values.get(path.key);
307
310
  return {
308
311
  value: getValueByPath(form, path),
309
312
  error: getErrorByPath(form, path),
310
313
  errors: getFieldErrorsByPath(form, path),
311
- isDirty: values.has(path.key) && get(initialValues, path.value) !== live,
314
+ // Same rule as getDirtyFields, committed baselines included: the field
315
+ // is dirty while its live value differs from its effective baseline.
316
+ isDirty: values.has(path.key) && getDirtyBaseline(form, path.key, path.value) !== live,
312
317
  isTouched: touched.has(path.key),
313
318
  isValidating: validating.has(path.key)
314
319
  };
@@ -391,11 +396,39 @@
391
396
  });
392
397
  return dirty;
393
398
  }
394
- function forEachDirtyField({ initialValues, values }, fn) {
395
- for (const [key, value] of values) {
399
+ function forEachDirtyField(form, fn) {
400
+ for (const [key, value] of form.values) {
396
401
  const path = JSON.parse(key);
397
- if (get(initialValues, path) !== value) fn(path.join("."));
402
+ if (getDirtyBaseline(form, key, path) !== value) fn(path.join("."));
403
+ }
404
+ }
405
+ const dirtyBaselines = /* @__PURE__ */ new WeakMap();
406
+ function getDirtyBaseline(form, key, segments) {
407
+ const baselines = dirtyBaselines.get(form);
408
+ if (baselines?.has(key)) return baselines.get(key);
409
+ return get(form.initialValues, segments);
410
+ }
411
+ function setDirtyBaseline(form, { key }, value) {
412
+ let baselines = dirtyBaselines.get(form);
413
+ if (!baselines) {
414
+ baselines = /* @__PURE__ */ new Map();
415
+ dirtyBaselines.set(form, baselines);
398
416
  }
417
+ baselines.set(key, value);
418
+ }
419
+ function pruneDirtyBaselines(form, { key }) {
420
+ const baselines = dirtyBaselines.get(form);
421
+ if (!baselines?.size) return;
422
+ const stem = `${key.slice(0, -1)},`;
423
+ for (const baselineKey of baselines.keys()) {
424
+ if (baselineKey.startsWith(stem)) baselines.delete(baselineKey);
425
+ }
426
+ }
427
+ function clearDirtyBaselines(form, key) {
428
+ const baselines = dirtyBaselines.get(form);
429
+ if (!baselines) return;
430
+ if (key === void 0) baselines.clear();
431
+ else baselines.delete(key);
399
432
  }
400
433
  const dirtyFieldsCaches = /* @__PURE__ */ new WeakMap();
401
434
  function bumpDirtyVersion(form) {
@@ -444,6 +477,7 @@
444
477
  touched.delete(key);
445
478
  errors.delete(key);
446
479
  validating.delete(key);
480
+ clearDirtyBaselines(form, key);
447
481
  if (!hasLiveBranch(values, segments)) deleted.add(key);
448
482
  bumpDirtyVersion(form);
449
483
  emit(emitter, "change");
@@ -477,6 +511,7 @@
477
511
  form.parsedValues = void 0;
478
512
  form.values.clear();
479
513
  form.deleted.clear();
514
+ clearDirtyBaselines(form);
480
515
  bumpDirtyVersion(form);
481
516
  emit(form.emitter, "change");
482
517
  }
@@ -491,6 +526,7 @@
491
526
  const { emitter, touched, values, deleted, validating } = form;
492
527
  values.clear();
493
528
  deleted.clear();
529
+ clearDirtyBaselines(form);
494
530
  if (!options?.keepTouched) touched.clear();
495
531
  validating.clear();
496
532
  if (!options?.keepIsSubmitting) form.isSubmitting = false;
@@ -512,6 +548,7 @@
512
548
  const path = create$1(name);
513
549
  const { emitter, values, touched, errors, deleted } = form;
514
550
  values.delete(path.key);
551
+ clearDirtyBaselines(form, path.key);
515
552
  if (form.parsedValues !== void 0) {
516
553
  form.parsedValues = unset(form.parsedValues, path.value);
517
554
  const initial = get(form.initialValues, path.value);
@@ -537,16 +574,13 @@
537
574
  const settle = () => waitUntil(
538
575
  form.emitter,
539
576
  "validating",
540
- () => !form.validating.size,
577
+ () => fieldsSettled(form),
541
578
  () => false
542
579
  );
543
580
  if (name === void 0) {
544
581
  form.validators.forEach((validator) => validator());
545
582
  await settle();
546
- if (form.validate) {
547
- const result = await form.validate(getValues(form));
548
- applyValidateResult(form, result);
549
- }
583
+ if (form.validate) await runFormValidate(form);
550
584
  return !hasErrors(form);
551
585
  }
552
586
  const keys = typeof name === "string" || isSegmentsPath(name) ? [create$1(name).key] : name.map((one) => create$1(one).key);
@@ -589,19 +623,110 @@
589
623
  }
590
624
  setFormErrors(form, result);
591
625
  }
626
+ const FORM_VALIDATING_KEY = "__form_validate__";
627
+ function fieldsSettled(form) {
628
+ for (const key of form.validating) {
629
+ if (key !== FORM_VALIDATING_KEY) return false;
630
+ }
631
+ return true;
632
+ }
633
+ const SETTLED = /* @__PURE__ */ Symbol("form-validate-settled");
634
+ const formValidateStates = /* @__PURE__ */ new WeakMap();
635
+ function getFormValidateState(form) {
636
+ let state = formValidateStates.get(form);
637
+ if (!state) {
638
+ state = {
639
+ timer: null,
640
+ controller: null,
641
+ round: null,
642
+ marked: false,
643
+ waiters: []
644
+ };
645
+ formValidateStates.set(form, state);
646
+ }
647
+ return state;
648
+ }
649
+ function runFormValidate(form) {
650
+ const validate2 = form.validate;
651
+ if (!validate2) return Promise.resolve();
652
+ const debounce = form.validateDebounce ?? 0;
653
+ if (debounce <= 0) {
654
+ const controller = new AbortController();
655
+ return Promise.resolve(
656
+ validate2(getValues(form), { form, signal: controller.signal })
657
+ ).then((result) => {
658
+ applyValidateResult(form, result);
659
+ });
660
+ }
661
+ const state = getFormValidateState(form);
662
+ if (state.timer !== null) clearTimeout(state.timer);
663
+ else {
664
+ state.marked = true;
665
+ form.validating.add(FORM_VALIDATING_KEY);
666
+ emit(form.emitter, "validating");
667
+ }
668
+ state.timer = setTimeout(() => {
669
+ state.timer = null;
670
+ const round = state.round = {};
671
+ runFormValidateRound(form, state, round).then(
672
+ () => settleFormValidate(form, state, round, SETTLED),
673
+ (error) => settleFormValidate(form, state, round, error)
674
+ );
675
+ }, debounce);
676
+ return new Promise((resolve, reject) => {
677
+ state.waiters.push({ resolve, reject });
678
+ });
679
+ }
680
+ function runFormValidateRound(form, state, round) {
681
+ const validate2 = form.validate;
682
+ if (!validate2) return Promise.resolve();
683
+ state.controller?.abort();
684
+ const controller = state.controller = new AbortController();
685
+ let outcome;
686
+ try {
687
+ outcome = Promise.resolve(
688
+ validate2(getValues(form), { form, signal: controller.signal })
689
+ );
690
+ } catch (error) {
691
+ outcome = Promise.reject(error);
692
+ }
693
+ return outcome.then(
694
+ (result) => {
695
+ if (state.round === round) applyValidateResult(form, result);
696
+ },
697
+ (error) => {
698
+ if (state.round === round) throw error;
699
+ }
700
+ );
701
+ }
702
+ function settleFormValidate(form, state, round, outcome) {
703
+ if (state.round !== round) return;
704
+ state.round = null;
705
+ if (state.timer !== null) return;
706
+ if (state.marked) {
707
+ state.marked = false;
708
+ form.validating.delete(FORM_VALIDATING_KEY);
709
+ emit(form.emitter, "validating");
710
+ }
711
+ const waiters = state.waiters;
712
+ state.waiters = [];
713
+ for (const waiter of waiters) {
714
+ if (outcome === SETTLED) waiter.resolve();
715
+ else waiter.reject(outcome);
716
+ }
717
+ }
592
718
  async function ensureValidate(form) {
593
719
  form.validators.forEach((validator) => validator());
594
720
  await waitUntil(
595
721
  form.emitter,
596
722
  "validating",
597
- () => !form.validating.size,
723
+ () => fieldsSettled(form),
598
724
  () => hasErrors(form)
599
725
  ).catch(() => {
600
726
  throw new Error(getFirstError(form));
601
727
  });
602
728
  if (form.validate) {
603
- const result = await form.validate(getValues(form));
604
- applyValidateResult(form, result);
729
+ await runFormValidate(form);
605
730
  if (hasErrors(form)) throw new Error(getFirstError(form));
606
731
  }
607
732
  }
@@ -1139,6 +1264,24 @@
1139
1264
  function useIsSubmitting(form) {
1140
1265
  return useWatch(form.emitter, "submitting", () => form.isSubmitting);
1141
1266
  }
1267
+ function useCanSubmit(form) {
1268
+ const { emitter } = form;
1269
+ const subscribeFactory = React.useCallback(
1270
+ (invalidate) => {
1271
+ const offErrors = on(emitter, "errors", invalidate);
1272
+ const offSubmitting = on(emitter, "submitting", invalidate);
1273
+ return () => {
1274
+ offErrors();
1275
+ offSubmitting();
1276
+ };
1277
+ },
1278
+ [emitter]
1279
+ );
1280
+ return useWatchCore(
1281
+ subscribeFactory,
1282
+ () => !form.isSubmitting && !hasErrors(form)
1283
+ );
1284
+ }
1142
1285
  function useSubmitCount(form) {
1143
1286
  return useWatch(form.emitter, "submitCount", () => form.submitCount);
1144
1287
  }
@@ -1811,6 +1954,7 @@
1811
1954
  exports.subscribe = subscribe;
1812
1955
  exports.trigger = trigger;
1813
1956
  exports.unsetValidatingByPath = unsetValidatingByPath;
1957
+ exports.useCanSubmit = useCanSubmit;
1814
1958
  exports.useCheckboxGroupContext = useCheckboxGroupContext;
1815
1959
  exports.useDirtyFields = useDirtyFields;
1816
1960
  exports.useError = useError;