@strictly/react-form 0.0.24 → 0.0.26

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.
@@ -7,12 +7,12 @@ $ tsup
7
7
  CLI Target: es6
8
8
  CJS Build start
9
9
  ESM Build start
10
- CJS dist/index.cjs 60.74 KB
11
- CJS ⚡️ Build success in 148ms
12
- ESM dist/index.js 56.72 KB
13
- ESM ⚡️ Build success in 148ms
10
+ ESM dist/index.js 59.53 KB
11
+ ESM ⚡️ Build success in 105ms
12
+ CJS dist/index.cjs 63.62 KB
13
+ CJS ⚡️ Build success in 110ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 31697ms
16
- DTS dist/index.d.cts 37.27 KB
17
- DTS dist/index.d.ts 37.27 KB
18
- Done in 32.89s.
15
+ DTS ⚡️ Build success in 30914ms
16
+ DTS dist/index.d.cts 38.03 KB
17
+ DTS dist/index.d.ts 38.03 KB
18
+ Done in 32.12s.
@@ -1,3 +1,3 @@
1
1
  yarn run v1.22.22
2
2
  $ tsc -b
3
- Done in 0.41s.
3
+ Done in 0.43s.
@@ -1,3 +1,3 @@
1
1
  yarn run v1.22.22
2
2
  $ json -f package.json -f package.exports.json --merge > package.release.json
3
- Done in 0.12s.
3
+ Done in 0.11s.
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  type Accessor,
14
14
  type AnyValueType,
15
+ equals,
15
16
  flattenAccessorsOfType,
16
17
  type FlattenedValuesOfType,
17
18
  flattenTypesOfType,
@@ -88,6 +89,12 @@ type FlattenedFieldOverrides<
88
89
  >
89
90
  }
90
91
 
92
+ type FlattenedErrorOverrides<
93
+ ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>,
94
+ > = {
95
+ -readonly [K in keyof ValuePathsToAdapters]?: ErrorOfFieldAdapter<ValuePathsToAdapters[K]>
96
+ }
97
+
91
98
  export enum Validation {
92
99
  None = 0,
93
100
  Changed = 1,
@@ -144,6 +151,8 @@ export abstract class FormModel<
144
151
  @observable.shallow
145
152
  accessor fieldOverrides: FlattenedFieldOverrides<ValuePathsToAdapters>
146
153
  @observable.shallow
154
+ accessor errorOverrides: FlattenedErrorOverrides<ValuePathsToAdapters> = {}
155
+ @observable.shallow
147
156
  accessor validation: FlattenedValidation<ValuePathsToAdapters> = {}
148
157
 
149
158
  private readonly flattenedTypeDefs: Readonly<Record<string, Type>>
@@ -273,7 +282,7 @@ export abstract class FormModel<
273
282
  return this.synthesizeFieldByPaths(valuePath, typePath)
274
283
  }
275
284
 
276
- private synthesizeFieldByPaths(valuePath: keyof ValuePathsToAdapters, typePath: keyof TypePathsToAdapters) {
285
+ private getField(valuePath: keyof ValuePathsToAdapters, typePath: keyof TypePathsToAdapters) {
277
286
  const adapter = this.adapters[typePath]
278
287
  if (adapter == null) {
279
288
  // invalid path, which can happen
@@ -311,43 +320,74 @@ export abstract class FormModel<
311
320
  valuePath as string,
312
321
  context,
313
322
  )
314
- let error: unknown = undefined
315
323
  const displayedValue = fieldOverride != null ? fieldOverride[0] : value
324
+
325
+ return {
326
+ context,
327
+ convert,
328
+ create,
329
+ revert,
330
+ displayedValue,
331
+ // value,
332
+ required,
333
+ readonly,
334
+ defaultValue,
335
+ }
336
+ }
337
+
338
+ private synthesizeFieldByPaths(valuePath: keyof ValuePathsToAdapters, typePath: keyof TypePathsToAdapters) {
339
+ const field = this.getField(valuePath, typePath)
340
+ if (field == null) {
341
+ return
342
+ }
343
+ const {
344
+ context,
345
+ convert,
346
+ revert,
347
+ displayedValue,
348
+ required,
349
+ readonly,
350
+ defaultValue,
351
+ } = field
316
352
  const validation = this.validation[valuePath] ?? Validation.None
317
- switch (validation) {
318
- case Validation.None:
319
- // skip validation
320
- break
321
- case Validation.Changed:
322
- if (revert != null) {
323
- const originalValue = valuePath in this.originalValues
353
+ let error: unknown = this.errorOverrides[valuePath]
354
+ if (error == null) {
355
+ switch (validation) {
356
+ case Validation.None:
357
+ // skip validation
358
+ break
359
+ case Validation.Changed:
360
+ if (revert != null) {
361
+ const originalValue = valuePath in this.originalValues
362
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
363
+ ? this.originalValues[valuePath as string]
364
+ : defaultValue
324
365
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
325
- ? this.originalValues[valuePath as string]
326
- : defaultValue
327
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
328
- const { value: originalDisplayedValue } = convert(originalValue, valuePath as string, context)
329
- // TODO better comparisons, displayed values can still be complex
330
- if (displayedValue !== originalDisplayedValue) {
331
- const revertResult = revert(displayedValue, valuePath, context)
366
+ const { value: originalDisplayedValue } = convert(originalValue, valuePath as string, context)
367
+ // TODO better comparisons, displayed values can still be complex
368
+ if (displayedValue !== originalDisplayedValue) {
369
+ const revertResult = revert(displayedValue, valuePath, context)
370
+ if (revertResult?.type === UnreliableFieldConversionType.Failure) {
371
+ ;({ error } = revertResult)
372
+ }
373
+ }
374
+ }
375
+ break
376
+ case Validation.Always:
377
+ {
378
+ const revertResult = revert?.(displayedValue, valuePath, context)
332
379
  if (revertResult?.type === UnreliableFieldConversionType.Failure) {
333
380
  ;({ error } = revertResult)
334
381
  }
335
382
  }
336
- }
337
- break
338
- case Validation.Always:
339
- {
340
- const revertResult = revert?.(displayedValue, valuePath, context)
341
- if (revertResult?.type === UnreliableFieldConversionType.Failure) {
342
- ;({ error } = revertResult)
343
- }
344
- }
345
- break
346
- default:
347
- throw new UnreachableError(validation)
383
+ break
384
+ default:
385
+ throw new UnreachableError(validation)
386
+ }
348
387
  }
388
+
349
389
  return {
350
- value: fieldOverride != null ? fieldOverride[0] : value,
390
+ value: displayedValue,
351
391
  error,
352
392
  readonly: readonly && !this.forceMutableFields,
353
393
  required,
@@ -386,6 +426,14 @@ export abstract class FormModel<
386
426
  )
387
427
  }
388
428
 
429
+ @computed
430
+ get dirty() {
431
+ return Object.keys(this.accessors).some((valuePath) => {
432
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
433
+ return this.isFieldDirty(valuePath as keyof ValuePathsToAdapters)
434
+ })
435
+ }
436
+
389
437
  typePath<K extends keyof ValueToTypePaths>(valuePath: K): ValueToTypePaths[K] {
390
438
  return valuePathToTypePath<ValueToTypePaths, K>(this.type, valuePath, true)
391
439
  }
@@ -574,6 +622,7 @@ export abstract class FormModel<
574
622
  const accessor = this.getAccessorForValuePath(valuePath)
575
623
  return runInAction(() => {
576
624
  this.fieldOverrides[valuePath] = [value]
625
+ delete this.errorOverrides[valuePath]
577
626
  if (validation != null) {
578
627
  this.validation[valuePath] = validation
579
628
  }
@@ -592,11 +641,30 @@ export abstract class FormModel<
592
641
  })
593
642
  }
594
643
 
644
+ /**
645
+ * Forces an error onto a field. Error will be removed if the field value changes
646
+ * @param valuePath the field to display an error for
647
+ * @param error the error to display
648
+ */
649
+ overrideFieldError<K extends keyof ValuePathsToAdapters>(
650
+ valuePath: K,
651
+ error?: ErrorOfFieldAdapter<ValuePathsToAdapters[K]>,
652
+ ) {
653
+ runInAction(() => {
654
+ if (error) {
655
+ this.errorOverrides[valuePath] = error
656
+ } else {
657
+ delete this.errorOverrides[valuePath]
658
+ }
659
+ })
660
+ }
661
+
595
662
  clearFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K) {
596
663
  const fieldOverride = this.fieldOverrides[valuePath]
597
664
  if (fieldOverride != null) {
598
665
  runInAction(() => {
599
666
  delete this.validation[valuePath]
667
+ delete this.errorOverrides[valuePath]
600
668
  })
601
669
  }
602
670
  }
@@ -623,6 +691,7 @@ export abstract class FormModel<
623
691
  runInAction(() => {
624
692
  this.fieldOverrides[key] = [displayValue]
625
693
  delete this.validation[key]
694
+ delete this.errorOverrides[key]
626
695
  })
627
696
  }
628
697
 
@@ -631,6 +700,7 @@ export abstract class FormModel<
631
700
  this.validation = {}
632
701
  // TODO this isn't correct, should reload from value
633
702
  this.fieldOverrides = {}
703
+ this.errorOverrides = {}
634
704
  this.value = mobxCopy(this.type, value)
635
705
  })
636
706
  }
@@ -646,12 +716,59 @@ export abstract class FormModel<
646
716
  return this.validation[valuePath] ?? Validation.None
647
717
  }
648
718
 
719
+ isFieldDirty<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean {
720
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
721
+ const typePath = valuePathToTypePath<ValueToTypePaths, keyof ValueToTypePaths>(
722
+ this.type,
723
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
724
+ valuePath as keyof ValueToTypePaths,
725
+ true,
726
+ ) as keyof TypePathsToAdapters
727
+ const field = this.getField(valuePath, typePath)
728
+
729
+ if (field == null) {
730
+ return false
731
+ }
732
+
733
+ const {
734
+ displayedValue,
735
+ convert,
736
+ revert,
737
+ context,
738
+ defaultValue,
739
+ } = field
740
+
741
+ // if either the display value, or the stored value, match the original, then assume it's not dirty
742
+ const originalValue = valuePath in this.originalValues
743
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
744
+ ? this.originalValues[valuePath as string]
745
+ : defaultValue
746
+ if (revert != null) {
747
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
748
+ const typeDef = this.flattenedTypeDefs[typePath as string]
749
+ const {
750
+ value,
751
+ type,
752
+ } = revert(displayedValue, valuePath, context)
753
+ if (type === UnreliableFieldConversionType.Success) {
754
+ if (equals(typeDef, originalValue, value)) {
755
+ return false
756
+ }
757
+ }
758
+ }
759
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
760
+ const { value: originalDisplayedValue } = convert(originalValue, valuePath as string, context)
761
+ // try to compare the displayed values directly if we can't revert the displayed value
762
+ return displayedValue !== originalDisplayedValue
763
+ }
764
+
649
765
  validateField<K extends keyof ValuePathsToAdapters>(
650
766
  valuePath: K,
651
767
  validation: Validation = Validation.Always,
652
768
  ): boolean {
653
769
  runInAction(() => {
654
770
  this.validation[valuePath] = validation
771
+ delete this.errorOverrides[valuePath]
655
772
  })
656
773
  return this.fields[valuePath].error == null
657
774
  }
@@ -673,4 +790,8 @@ export abstract class FormModel<
673
790
  },
674
791
  )
675
792
  }
793
+
794
+ validateSubmit() {
795
+ return this.validateAll()
796
+ }
676
797
  }
@@ -41,8 +41,8 @@ export function useDefaultMobxFormHooks<
41
41
  path: Path,
42
42
  value: ValueTypeOfField<F[Path]>,
43
43
  ) {
44
- // clear validation once there are no errors for the field
45
- model.setFieldValue<Path>(path, value)
44
+ const validation = Math.min(model.getValidation(path), Validation.Changed)
45
+ model.setFieldValue<Path>(path, value, validation)
46
46
  },
47
47
  [model],
48
48
  )
@@ -66,7 +66,8 @@ export function useDefaultMobxFormHooks<
66
66
  // (e.g. changing a discriminator)
67
67
  // TODO debounce?
68
68
  setTimeout(function () {
69
- if (model.isValuePathActive(path)) {
69
+ // only start validation if the user has changed the field
70
+ if (model.isValuePathActive(path) && model.isFieldDirty(path)) {
70
71
  // further workaround to make sure we don't downgrade the existing validation
71
72
  model.validateField(path, Math.max(Validation.Changed, model.getValidation(path)))
72
73
  }
@@ -77,7 +78,7 @@ export function useDefaultMobxFormHooks<
77
78
 
78
79
  const onFormSubmit = useCallback(
79
80
  function () {
80
- if (model.validateAll()) {
81
+ if (model.validateSubmit()) {
81
82
  onValidFormSubmit?.(model.value)
82
83
  }
83
84
  },
package/dist/index.cjs CHANGED
@@ -363,8 +363,8 @@ var Validation = /* @__PURE__ */ ((Validation2) => {
363
363
  Validation2[Validation2["Always"] = 2] = "Always";
364
364
  return Validation2;
365
365
  })(Validation || {});
366
- var _accessors_dec, _knownFields_dec, _fields_dec, _validation_dec, _fieldOverrides_dec, _value_dec, _init, _value, _fieldOverrides, _validation;
367
- _value_dec = [import_mobx.observable.ref], _fieldOverrides_dec = [import_mobx.observable.shallow], _validation_dec = [import_mobx.observable.shallow], _fields_dec = [import_mobx.computed], _knownFields_dec = [import_mobx.computed], _accessors_dec = [import_mobx.computed];
366
+ var _dirty_dec, _accessors_dec, _knownFields_dec, _fields_dec, _validation_dec, _errorOverrides_dec, _fieldOverrides_dec, _value_dec, _init, _value, _fieldOverrides, _errorOverrides, _validation;
367
+ _value_dec = [import_mobx.observable.ref], _fieldOverrides_dec = [import_mobx.observable.shallow], _errorOverrides_dec = [import_mobx.observable.shallow], _validation_dec = [import_mobx.observable.shallow], _fields_dec = [import_mobx.computed], _knownFields_dec = [import_mobx.computed], _accessors_dec = [import_mobx.computed], _dirty_dec = [import_mobx.computed];
368
368
  var FormModel = class {
369
369
  constructor(type, originalValue, adapters, mode) {
370
370
  this.type = type;
@@ -373,7 +373,8 @@ var FormModel = class {
373
373
  __runInitializers(_init, 5, this);
374
374
  __privateAdd(this, _value, __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
375
375
  __privateAdd(this, _fieldOverrides, __runInitializers(_init, 12, this)), __runInitializers(_init, 15, this);
376
- __privateAdd(this, _validation, __runInitializers(_init, 16, this, {})), __runInitializers(_init, 19, this);
376
+ __privateAdd(this, _errorOverrides, __runInitializers(_init, 16, this, {})), __runInitializers(_init, 19, this);
377
+ __privateAdd(this, _validation, __runInitializers(_init, 20, this, {})), __runInitializers(_init, 23, this);
377
378
  __publicField(this, "flattenedTypeDefs");
378
379
  // cannot be type safe
379
380
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -463,8 +464,7 @@ var FormModel = class {
463
464
  }
464
465
  return this.synthesizeFieldByPaths(valuePath, typePath);
465
466
  }
466
- synthesizeFieldByPaths(valuePath, typePath) {
467
- var _a;
467
+ getField(valuePath, typePath) {
468
468
  const adapter2 = this.adapters[typePath];
469
469
  if (adapter2 == null) {
470
470
  return;
@@ -492,39 +492,68 @@ var FormModel = class {
492
492
  valuePath,
493
493
  context
494
494
  );
495
- let error = void 0;
496
495
  const displayedValue = fieldOverride != null ? fieldOverride[0] : value;
496
+ return {
497
+ context,
498
+ convert,
499
+ create,
500
+ revert,
501
+ displayedValue,
502
+ // value,
503
+ required,
504
+ readonly,
505
+ defaultValue
506
+ };
507
+ }
508
+ synthesizeFieldByPaths(valuePath, typePath) {
509
+ var _a;
510
+ const field = this.getField(valuePath, typePath);
511
+ if (field == null) {
512
+ return;
513
+ }
514
+ const {
515
+ context,
516
+ convert,
517
+ revert,
518
+ displayedValue,
519
+ required,
520
+ readonly,
521
+ defaultValue
522
+ } = field;
497
523
  const validation = (_a = this.validation[valuePath]) != null ? _a : 0 /* None */;
498
- switch (validation) {
499
- case 0 /* None */:
500
- break;
501
- case 1 /* Changed */:
502
- if (revert != null) {
503
- const originalValue = valuePath in this.originalValues ? this.originalValues[valuePath] : defaultValue;
504
- const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
505
- if (displayedValue !== originalDisplayedValue) {
506
- const revertResult = revert(displayedValue, valuePath, context);
524
+ let error = this.errorOverrides[valuePath];
525
+ if (error == null) {
526
+ switch (validation) {
527
+ case 0 /* None */:
528
+ break;
529
+ case 1 /* Changed */:
530
+ if (revert != null) {
531
+ const originalValue = valuePath in this.originalValues ? this.originalValues[valuePath] : defaultValue;
532
+ const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
533
+ if (displayedValue !== originalDisplayedValue) {
534
+ const revertResult = revert(displayedValue, valuePath, context);
535
+ if ((revertResult == null ? void 0 : revertResult.type) === 1 /* Failure */) {
536
+ ;
537
+ ({ error } = revertResult);
538
+ }
539
+ }
540
+ }
541
+ break;
542
+ case 2 /* Always */:
543
+ {
544
+ const revertResult = revert == null ? void 0 : revert(displayedValue, valuePath, context);
507
545
  if ((revertResult == null ? void 0 : revertResult.type) === 1 /* Failure */) {
508
546
  ;
509
547
  ({ error } = revertResult);
510
548
  }
511
549
  }
512
- }
513
- break;
514
- case 2 /* Always */:
515
- {
516
- const revertResult = revert == null ? void 0 : revert(displayedValue, valuePath, context);
517
- if ((revertResult == null ? void 0 : revertResult.type) === 1 /* Failure */) {
518
- ;
519
- ({ error } = revertResult);
520
- }
521
- }
522
- break;
523
- default:
524
- throw new import_base2.UnreachableError(validation);
550
+ break;
551
+ default:
552
+ throw new import_base2.UnreachableError(validation);
553
+ }
525
554
  }
526
555
  return {
527
- value: fieldOverride != null ? fieldOverride[0] : value,
556
+ value: displayedValue,
528
557
  error,
529
558
  readonly: readonly && !this.forceMutableFields,
530
559
  required
@@ -554,6 +583,11 @@ var FormModel = class {
554
583
  valuePath
555
584
  );
556
585
  }
586
+ get dirty() {
587
+ return Object.keys(this.accessors).some((valuePath) => {
588
+ return this.isFieldDirty(valuePath);
589
+ });
590
+ }
557
591
  typePath(valuePath) {
558
592
  return (0, import_define.valuePathToTypePath)(this.type, valuePath, true);
559
593
  }
@@ -699,6 +733,7 @@ var FormModel = class {
699
733
  const accessor = this.getAccessorForValuePath(valuePath);
700
734
  return (0, import_mobx.runInAction)(() => {
701
735
  this.fieldOverrides[valuePath] = [value];
736
+ delete this.errorOverrides[valuePath];
702
737
  if (validation != null) {
703
738
  this.validation[valuePath] = validation;
704
739
  }
@@ -716,11 +751,26 @@ var FormModel = class {
716
751
  }
717
752
  });
718
753
  }
754
+ /**
755
+ * Forces an error onto a field. Error will be removed if the field value changes
756
+ * @param valuePath the field to display an error for
757
+ * @param error the error to display
758
+ */
759
+ overrideFieldError(valuePath, error) {
760
+ (0, import_mobx.runInAction)(() => {
761
+ if (error) {
762
+ this.errorOverrides[valuePath] = error;
763
+ } else {
764
+ delete this.errorOverrides[valuePath];
765
+ }
766
+ });
767
+ }
719
768
  clearFieldError(valuePath) {
720
769
  const fieldOverride = this.fieldOverrides[valuePath];
721
770
  if (fieldOverride != null) {
722
771
  (0, import_mobx.runInAction)(() => {
723
772
  delete this.validation[valuePath];
773
+ delete this.errorOverrides[valuePath];
724
774
  });
725
775
  }
726
776
  }
@@ -743,12 +793,14 @@ var FormModel = class {
743
793
  (0, import_mobx.runInAction)(() => {
744
794
  this.fieldOverrides[key] = [displayValue];
745
795
  delete this.validation[key];
796
+ delete this.errorOverrides[key];
746
797
  });
747
798
  }
748
799
  clearAll(value) {
749
800
  (0, import_mobx.runInAction)(() => {
750
801
  this.validation = {};
751
802
  this.fieldOverrides = {};
803
+ this.errorOverrides = {};
752
804
  this.value = (0, import_define.mobxCopy)(this.type, value);
753
805
  });
754
806
  }
@@ -761,9 +813,44 @@ var FormModel = class {
761
813
  var _a;
762
814
  return (_a = this.validation[valuePath]) != null ? _a : 0 /* None */;
763
815
  }
816
+ isFieldDirty(valuePath) {
817
+ const typePath = (0, import_define.valuePathToTypePath)(
818
+ this.type,
819
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
820
+ valuePath,
821
+ true
822
+ );
823
+ const field = this.getField(valuePath, typePath);
824
+ if (field == null) {
825
+ return false;
826
+ }
827
+ const {
828
+ displayedValue,
829
+ convert,
830
+ revert,
831
+ context,
832
+ defaultValue
833
+ } = field;
834
+ const originalValue = valuePath in this.originalValues ? this.originalValues[valuePath] : defaultValue;
835
+ if (revert != null) {
836
+ const typeDef = this.flattenedTypeDefs[typePath];
837
+ const {
838
+ value,
839
+ type
840
+ } = revert(displayedValue, valuePath, context);
841
+ if (type === 0 /* Success */) {
842
+ if ((0, import_define.equals)(typeDef, originalValue, value)) {
843
+ return false;
844
+ }
845
+ }
846
+ }
847
+ const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
848
+ return displayedValue !== originalDisplayedValue;
849
+ }
764
850
  validateField(valuePath, validation = 2 /* Always */) {
765
851
  (0, import_mobx.runInAction)(() => {
766
852
  this.validation[valuePath] = validation;
853
+ delete this.errorOverrides[valuePath];
767
854
  });
768
855
  return this.fields[valuePath].error == null;
769
856
  }
@@ -781,17 +868,23 @@ var FormModel = class {
781
868
  }
782
869
  );
783
870
  }
871
+ validateSubmit() {
872
+ return this.validateAll();
873
+ }
784
874
  };
785
875
  _init = __decoratorStart(null);
786
876
  _value = new WeakMap();
787
877
  _fieldOverrides = new WeakMap();
878
+ _errorOverrides = new WeakMap();
788
879
  _validation = new WeakMap();
789
880
  __decorateElement(_init, 4, "value", _value_dec, FormModel, _value);
790
881
  __decorateElement(_init, 4, "fieldOverrides", _fieldOverrides_dec, FormModel, _fieldOverrides);
882
+ __decorateElement(_init, 4, "errorOverrides", _errorOverrides_dec, FormModel, _errorOverrides);
791
883
  __decorateElement(_init, 4, "validation", _validation_dec, FormModel, _validation);
792
884
  __decorateElement(_init, 2, "fields", _fields_dec, FormModel);
793
885
  __decorateElement(_init, 2, "knownFields", _knownFields_dec, FormModel);
794
886
  __decorateElement(_init, 2, "accessors", _accessors_dec, FormModel);
887
+ __decorateElement(_init, 2, "dirty", _dirty_dec, FormModel);
795
888
  __decoratorMetadata(_init, FormModel);
796
889
 
797
890
  // core/mobx/hooks.tsx
@@ -802,7 +895,8 @@ function useDefaultMobxFormHooks(model, {
802
895
  } = {}) {
803
896
  const onFieldValueChange = (0, import_react.useCallback)(
804
897
  function(path, value) {
805
- model.setFieldValue(path, value);
898
+ const validation = Math.min(model.getValidation(path), 1 /* Changed */);
899
+ model.setFieldValue(path, value, validation);
806
900
  },
807
901
  [model]
808
902
  );
@@ -821,7 +915,7 @@ function useDefaultMobxFormHooks(model, {
821
915
  const onFieldBlur = (0, import_react.useCallback)(
822
916
  function(path) {
823
917
  setTimeout(function() {
824
- if (model.isValuePathActive(path)) {
918
+ if (model.isValuePathActive(path) && model.isFieldDirty(path)) {
825
919
  model.validateField(path, Math.max(1 /* Changed */, model.getValidation(path)));
826
920
  }
827
921
  }, 100);
@@ -830,7 +924,7 @@ function useDefaultMobxFormHooks(model, {
830
924
  );
831
925
  const onFormSubmit = (0, import_react.useCallback)(
832
926
  function() {
833
- if (model.validateAll()) {
927
+ if (model.validateSubmit()) {
834
928
  onValidFormSubmit == null ? void 0 : onValidFormSubmit(model.value);
835
929
  }
836
930
  },
package/dist/index.d.cts CHANGED
@@ -112,6 +112,9 @@ type FieldOverride<V = any> = Maybe<V>;
112
112
  type FlattenedFieldOverrides<ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>> = {
113
113
  -readonly [K in keyof ValuePathsToAdapters]?: FieldOverride<ToOfFieldAdapter<ValuePathsToAdapters[K]>>;
114
114
  };
115
+ type FlattenedErrorOverrides<ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>> = {
116
+ -readonly [K in keyof ValuePathsToAdapters]?: ErrorOfFieldAdapter<ValuePathsToAdapters[K]>;
117
+ };
115
118
  declare enum Validation {
116
119
  None = 0,
117
120
  Changed = 1,
@@ -133,6 +136,7 @@ declare abstract class FormModel<T extends Type, ValueToTypePaths extends Readon
133
136
  protected readonly mode: FormMode;
134
137
  accessor value: MobxValueOfType<T>;
135
138
  accessor fieldOverrides: FlattenedFieldOverrides<ValuePathsToAdapters>;
139
+ accessor errorOverrides: FlattenedErrorOverrides<ValuePathsToAdapters>;
136
140
  accessor validation: FlattenedValidation<ValuePathsToAdapters>;
137
141
  private readonly flattenedTypeDefs;
138
142
  private readonly originalValues;
@@ -142,23 +146,33 @@ declare abstract class FormModel<T extends Type, ValueToTypePaths extends Readon
142
146
  get fields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>>;
143
147
  private get knownFields();
144
148
  private maybeSynthesizeFieldByValuePath;
149
+ private getField;
145
150
  private synthesizeFieldByPaths;
146
151
  getAccessorForValuePath(valuePath: keyof ValuePathsToAdapters): Accessor | undefined;
147
152
  get accessors(): Readonly<Record<string, Accessor>>;
148
153
  private maybeGetAdapterForValuePath;
149
154
  private getAdapterForValuePath;
155
+ get dirty(): boolean;
150
156
  typePath<K extends keyof ValueToTypePaths>(valuePath: K): ValueToTypePaths[K];
151
157
  setFieldValue<K extends keyof ValuePathsToAdapters>(valuePath: K, value: ToOfFieldAdapter<ValuePathsToAdapters[K]>, validation?: Validation): boolean;
152
158
  addListItem<K extends keyof FlattenedListTypesOfType<T>>(valuePath: K, elementValue?: Maybe<ElementOfArray<FlattenedValuesOfType<T>[K]>>, index?: number): void;
153
159
  removeListItem<K extends keyof FlattenedListTypesOfType<T>>(...elementValuePaths: readonly `${K}.${number}`[]): void;
154
160
  private internalSetFieldValue;
161
+ /**
162
+ * Forces an error onto a field. Error will be removed if the field value changes
163
+ * @param valuePath the field to display an error for
164
+ * @param error the error to display
165
+ */
166
+ overrideFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K, error?: ErrorOfFieldAdapter<ValuePathsToAdapters[K]>): void;
155
167
  clearFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K): void;
156
168
  clearFieldValue<K extends StringKeyOf<ValuePathsToAdapters>>(valuePath: K): void;
157
169
  clearAll(value: ValueOfType<T>): void;
158
170
  isValuePathActive<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
159
171
  getValidation<K extends keyof ValuePathsToAdapters>(valuePath: K): Validation;
172
+ isFieldDirty<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
160
173
  validateField<K extends keyof ValuePathsToAdapters>(valuePath: K, validation?: Validation): boolean;
161
174
  validateAll(validation?: Validation): boolean;
175
+ validateSubmit(): boolean;
162
176
  }
163
177
 
164
178
  type ValueOfModel<M extends FormModel<any, any, any, any, any>> = M extends FormModel<infer T, any, any, any, any> ? ValueOfType<ReadonlyTypeOfType<T>> : never;