@strictly/react-form 0.0.25 → 0.0.27

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.
@@ -14,6 +14,9 @@ type FieldOverride<V = any> = Maybe<V>;
14
14
  type FlattenedFieldOverrides<ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>> = {
15
15
  -readonly [K in keyof ValuePathsToAdapters]?: FieldOverride<ToOfFieldAdapter<ValuePathsToAdapters[K]>>;
16
16
  };
17
+ type FlattenedErrorOverrides<ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>> = {
18
+ -readonly [K in keyof ValuePathsToAdapters]?: ErrorOfFieldAdapter<ValuePathsToAdapters[K]>;
19
+ };
17
20
  export declare enum Validation {
18
21
  None = 0,
19
22
  Changed = 1,
@@ -35,6 +38,7 @@ export declare abstract class FormModel<T extends Type, ValueToTypePaths extends
35
38
  protected readonly mode: FormMode;
36
39
  accessor value: MobxValueOfType<T>;
37
40
  accessor fieldOverrides: FlattenedFieldOverrides<ValuePathsToAdapters>;
41
+ accessor errorOverrides: FlattenedErrorOverrides<ValuePathsToAdapters>;
38
42
  accessor validation: FlattenedValidation<ValuePathsToAdapters>;
39
43
  private readonly flattenedTypeDefs;
40
44
  private readonly originalValues;
@@ -50,18 +54,27 @@ export declare abstract class FormModel<T extends Type, ValueToTypePaths extends
50
54
  get accessors(): Readonly<Record<string, Accessor>>;
51
55
  private maybeGetAdapterForValuePath;
52
56
  private getAdapterForValuePath;
57
+ get dirty(): boolean;
53
58
  typePath<K extends keyof ValueToTypePaths>(valuePath: K): ValueToTypePaths[K];
54
59
  setFieldValue<K extends keyof ValuePathsToAdapters>(valuePath: K, value: ToOfFieldAdapter<ValuePathsToAdapters[K]>, validation?: Validation): boolean;
55
60
  addListItem<K extends keyof FlattenedListTypesOfType<T>>(valuePath: K, elementValue?: Maybe<ElementOfArray<FlattenedValuesOfType<T>[K]>>, index?: number): void;
56
61
  removeListItem<K extends keyof FlattenedListTypesOfType<T>>(...elementValuePaths: readonly `${K}.${number}`[]): void;
62
+ protected moveListItem<K extends keyof FlattenedListTypesOfType<T>>(fromValuePath: K, toValuePath: K): void;
57
63
  private internalSetFieldValue;
64
+ /**
65
+ * Forces an error onto a field. Error will be removed if the field value changes
66
+ * @param valuePath the field to display an error for
67
+ * @param error the error to display
68
+ */
69
+ overrideFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K, error?: ErrorOfFieldAdapter<ValuePathsToAdapters[K]>): void;
58
70
  clearFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K): void;
59
71
  clearFieldValue<K extends StringKeyOf<ValuePathsToAdapters>>(valuePath: K): void;
60
72
  clearAll(value: ValueOfType<T>): void;
61
73
  isValuePathActive<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
62
74
  getValidation<K extends keyof ValuePathsToAdapters>(valuePath: K): Validation;
63
- isDirty<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
75
+ isFieldDirty<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
64
76
  validateField<K extends keyof ValuePathsToAdapters>(valuePath: K, validation?: Validation): boolean;
65
77
  validateAll(validation?: Validation): boolean;
78
+ validateSubmit(): boolean;
66
79
  }
67
80
  export {};
@@ -44,7 +44,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
44
44
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
45
45
  };
46
46
  import { assertExists, assertExistsAndReturn, assertState, checkValidNumber, map, toArray, UnreachableError, } from '@strictly/base';
47
- import { flattenAccessorsOfType, flattenTypesOfType, flattenValuesOfType, flattenValueTo, jsonPathPop, mobxCopy, valuePathToTypePath, } from '@strictly/define';
47
+ import { equals, flattenAccessorsOfType, flattenTypesOfType, flattenValuesOfType, flattenValueTo, jsonPathPop, mobxCopy, valuePathToTypePath, } from '@strictly/define';
48
48
  import { computed, observable, runInAction, } from 'mobx';
49
49
  import { UnreliableFieldConversionType, } from 'types/field_converters';
50
50
  export var Validation;
@@ -54,8 +54,8 @@ export var Validation;
54
54
  Validation[Validation["Always"] = 2] = "Always";
55
55
  })(Validation || (Validation = {}));
56
56
  let FormModel = (() => {
57
- var _a, _FormModel_value_accessor_storage, _FormModel_fieldOverrides_accessor_storage, _FormModel_validation_accessor_storage;
58
- var _b, _c, _d;
57
+ var _a, _FormModel_value_accessor_storage, _FormModel_fieldOverrides_accessor_storage, _FormModel_errorOverrides_accessor_storage, _FormModel_validation_accessor_storage;
58
+ var _b, _c, _d, _e;
59
59
  let _instanceExtraInitializers = [];
60
60
  let _value_decorators;
61
61
  let _value_initializers = [];
@@ -63,17 +63,23 @@ let FormModel = (() => {
63
63
  let _fieldOverrides_decorators;
64
64
  let _fieldOverrides_initializers = [];
65
65
  let _fieldOverrides_extraInitializers = [];
66
+ let _errorOverrides_decorators;
67
+ let _errorOverrides_initializers = [];
68
+ let _errorOverrides_extraInitializers = [];
66
69
  let _validation_decorators;
67
70
  let _validation_initializers = [];
68
71
  let _validation_extraInitializers = [];
69
72
  let _get_fields_decorators;
70
73
  let _get_knownFields_decorators;
71
74
  let _get_accessors_decorators;
75
+ let _get_dirty_decorators;
72
76
  return _a = class FormModel {
73
77
  get value() { return __classPrivateFieldGet(this, _FormModel_value_accessor_storage, "f"); }
74
78
  set value(value) { __classPrivateFieldSet(this, _FormModel_value_accessor_storage, value, "f"); }
75
79
  get fieldOverrides() { return __classPrivateFieldGet(this, _FormModel_fieldOverrides_accessor_storage, "f"); }
76
80
  set fieldOverrides(value) { __classPrivateFieldSet(this, _FormModel_fieldOverrides_accessor_storage, value, "f"); }
81
+ get errorOverrides() { return __classPrivateFieldGet(this, _FormModel_errorOverrides_accessor_storage, "f"); }
82
+ set errorOverrides(value) { __classPrivateFieldSet(this, _FormModel_errorOverrides_accessor_storage, value, "f"); }
77
83
  get validation() { return __classPrivateFieldGet(this, _FormModel_validation_accessor_storage, "f"); }
78
84
  set validation(value) { __classPrivateFieldSet(this, _FormModel_validation_accessor_storage, value, "f"); }
79
85
  constructor(type, originalValue, adapters, mode) {
@@ -97,7 +103,8 @@ let FormModel = (() => {
97
103
  });
98
104
  _FormModel_value_accessor_storage.set(this, __runInitializers(this, _value_initializers, void 0));
99
105
  _FormModel_fieldOverrides_accessor_storage.set(this, (__runInitializers(this, _value_extraInitializers), __runInitializers(this, _fieldOverrides_initializers, void 0)));
100
- _FormModel_validation_accessor_storage.set(this, (__runInitializers(this, _fieldOverrides_extraInitializers), __runInitializers(this, _validation_initializers, {})));
106
+ _FormModel_errorOverrides_accessor_storage.set(this, (__runInitializers(this, _fieldOverrides_extraInitializers), __runInitializers(this, _errorOverrides_initializers, {})));
107
+ _FormModel_validation_accessor_storage.set(this, (__runInitializers(this, _errorOverrides_extraInitializers), __runInitializers(this, _validation_initializers, {})));
101
108
  Object.defineProperty(this, "flattenedTypeDefs", {
102
109
  enumerable: true,
103
110
  configurable: true,
@@ -233,40 +240,42 @@ let FormModel = (() => {
233
240
  }
234
241
  const { context, convert, revert, displayedValue, required, readonly, defaultValue, } = field;
235
242
  const validation = (_b = this.validation[valuePath]) !== null && _b !== void 0 ? _b : Validation.None;
236
- let error;
237
- switch (validation) {
238
- case Validation.None:
239
- // skip validation
240
- break;
241
- case Validation.Changed:
242
- if (revert != null) {
243
- const originalValue = valuePath in this.originalValues
243
+ let error = this.errorOverrides[valuePath];
244
+ if (error == null) {
245
+ switch (validation) {
246
+ case Validation.None:
247
+ // skip validation
248
+ break;
249
+ case Validation.Changed:
250
+ if (revert != null) {
251
+ const originalValue = valuePath in this.originalValues
252
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
253
+ ? this.originalValues[valuePath]
254
+ : defaultValue;
244
255
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
245
- ? this.originalValues[valuePath]
246
- : defaultValue;
247
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
248
- const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
249
- // TODO better comparisons, displayed values can still be complex
250
- if (displayedValue !== originalDisplayedValue) {
251
- const revertResult = revert(displayedValue, valuePath, context);
256
+ const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
257
+ // TODO better comparisons, displayed values can still be complex
258
+ if (displayedValue !== originalDisplayedValue) {
259
+ const revertResult = revert(displayedValue, valuePath, context);
260
+ if ((revertResult === null || revertResult === void 0 ? void 0 : revertResult.type) === UnreliableFieldConversionType.Failure) {
261
+ ;
262
+ ({ error } = revertResult);
263
+ }
264
+ }
265
+ }
266
+ break;
267
+ case Validation.Always:
268
+ {
269
+ const revertResult = revert === null || revert === void 0 ? void 0 : revert(displayedValue, valuePath, context);
252
270
  if ((revertResult === null || revertResult === void 0 ? void 0 : revertResult.type) === UnreliableFieldConversionType.Failure) {
253
271
  ;
254
272
  ({ error } = revertResult);
255
273
  }
256
274
  }
257
- }
258
- break;
259
- case Validation.Always:
260
- {
261
- const revertResult = revert === null || revert === void 0 ? void 0 : revert(displayedValue, valuePath, context);
262
- if ((revertResult === null || revertResult === void 0 ? void 0 : revertResult.type) === UnreliableFieldConversionType.Failure) {
263
- ;
264
- ({ error } = revertResult);
265
- }
266
- }
267
- break;
268
- default:
269
- throw new UnreachableError(validation);
275
+ break;
276
+ default:
277
+ throw new UnreachableError(validation);
278
+ }
270
279
  }
271
280
  return {
272
281
  value: displayedValue,
@@ -293,6 +302,12 @@ let FormModel = (() => {
293
302
  getAdapterForValuePath(valuePath) {
294
303
  return assertExistsAndReturn(this.maybeGetAdapterForValuePath(valuePath), 'expected adapter to be defined {}', valuePath);
295
304
  }
305
+ get dirty() {
306
+ return Object.keys(this.accessors).some((valuePath) => {
307
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
308
+ return this.isFieldDirty(valuePath);
309
+ });
310
+ }
296
311
  typePath(valuePath) {
297
312
  return valuePathToTypePath(this.type, valuePath, true);
298
313
  }
@@ -417,6 +432,8 @@ let FormModel = (() => {
417
432
  const validation = this.validation[fromJsonPath];
418
433
  delete this.validation[fromJsonPath];
419
434
  this.validation[toJsonPath] = validation;
435
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
436
+ this.moveListItem(fromJsonPath, toJsonPath);
420
437
  });
421
438
  accessor.set(newList);
422
439
  // delete any value overrides so the new list isn't shadowed
@@ -425,6 +442,12 @@ let FormModel = (() => {
425
442
  });
426
443
  });
427
444
  }
445
+ moveListItem(fromValuePath, toValuePath) {
446
+ // do nothing, this is for subclasses to override
447
+ // put in some nonsense so TS doesn't complain about the parameters not being used
448
+ fromValuePath;
449
+ toValuePath;
450
+ }
428
451
  internalSetFieldValue(valuePath, value, validation) {
429
452
  const { revert } = this.getAdapterForValuePath(valuePath);
430
453
  assertExists(revert, 'setting value not supported {}', valuePath);
@@ -433,6 +456,7 @@ let FormModel = (() => {
433
456
  const accessor = this.getAccessorForValuePath(valuePath);
434
457
  return runInAction(() => {
435
458
  this.fieldOverrides[valuePath] = [value];
459
+ delete this.errorOverrides[valuePath];
436
460
  if (validation != null) {
437
461
  this.validation[valuePath] = validation;
438
462
  }
@@ -450,11 +474,27 @@ let FormModel = (() => {
450
474
  }
451
475
  });
452
476
  }
477
+ /**
478
+ * Forces an error onto a field. Error will be removed if the field value changes
479
+ * @param valuePath the field to display an error for
480
+ * @param error the error to display
481
+ */
482
+ overrideFieldError(valuePath, error) {
483
+ runInAction(() => {
484
+ if (error) {
485
+ this.errorOverrides[valuePath] = error;
486
+ }
487
+ else {
488
+ delete this.errorOverrides[valuePath];
489
+ }
490
+ });
491
+ }
453
492
  clearFieldError(valuePath) {
454
493
  const fieldOverride = this.fieldOverrides[valuePath];
455
494
  if (fieldOverride != null) {
456
495
  runInAction(() => {
457
496
  delete this.validation[valuePath];
497
+ delete this.errorOverrides[valuePath];
458
498
  });
459
499
  }
460
500
  }
@@ -475,6 +515,7 @@ let FormModel = (() => {
475
515
  runInAction(() => {
476
516
  this.fieldOverrides[key] = [displayValue];
477
517
  delete this.validation[key];
518
+ delete this.errorOverrides[key];
478
519
  });
479
520
  }
480
521
  clearAll(value) {
@@ -482,6 +523,7 @@ let FormModel = (() => {
482
523
  this.validation = {};
483
524
  // TODO this isn't correct, should reload from value
484
525
  this.fieldOverrides = {};
526
+ this.errorOverrides = {};
485
527
  this.value = mobxCopy(this.type, value);
486
528
  });
487
529
  }
@@ -495,7 +537,7 @@ let FormModel = (() => {
495
537
  var _b;
496
538
  return (_b = this.validation[valuePath]) !== null && _b !== void 0 ? _b : Validation.None;
497
539
  }
498
- isDirty(valuePath) {
540
+ isFieldDirty(valuePath) {
499
541
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
500
542
  const typePath = valuePathToTypePath(this.type,
501
543
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -504,19 +546,31 @@ let FormModel = (() => {
504
546
  if (field == null) {
505
547
  return false;
506
548
  }
507
- const { displayedValue, convert, context, defaultValue, } = field;
549
+ const { displayedValue, convert, revert, context, defaultValue, } = field;
550
+ // if either the display value, or the stored value, match the original, then assume it's not dirty
508
551
  const originalValue = valuePath in this.originalValues
509
552
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
510
553
  ? this.originalValues[valuePath]
511
554
  : defaultValue;
555
+ if (revert != null) {
556
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
557
+ const typeDef = this.flattenedTypeDefs[typePath];
558
+ const { value, type, } = revert(displayedValue, valuePath, context);
559
+ if (type === UnreliableFieldConversionType.Success) {
560
+ if (equals(typeDef, originalValue, value)) {
561
+ return false;
562
+ }
563
+ }
564
+ }
512
565
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
513
566
  const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
514
- // TODO better comparisons, displayed values can still be complex
515
- return (displayedValue !== originalDisplayedValue);
567
+ // try to compare the displayed values directly if we can't revert the displayed value
568
+ return displayedValue !== originalDisplayedValue;
516
569
  }
517
570
  validateField(valuePath, validation = Validation.Always) {
518
571
  runInAction(() => {
519
572
  this.validation[valuePath] = validation;
573
+ delete this.errorOverrides[valuePath];
520
574
  });
521
575
  return this.fields[valuePath].error == null;
522
576
  }
@@ -534,24 +588,32 @@ let FormModel = (() => {
534
588
  return (field === null || field === void 0 ? void 0 : field.error) == null;
535
589
  });
536
590
  }
591
+ validateSubmit() {
592
+ return this.validateAll();
593
+ }
537
594
  },
538
595
  _FormModel_value_accessor_storage = new WeakMap(),
539
596
  _FormModel_fieldOverrides_accessor_storage = new WeakMap(),
597
+ _FormModel_errorOverrides_accessor_storage = new WeakMap(),
540
598
  _FormModel_validation_accessor_storage = new WeakMap(),
541
599
  (() => {
542
600
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
543
601
  _value_decorators = [(_b = observable).ref.bind(_b)];
544
602
  _fieldOverrides_decorators = [(_c = observable).shallow.bind(_c)];
545
- _validation_decorators = [(_d = observable).shallow.bind(_d)];
603
+ _errorOverrides_decorators = [(_d = observable).shallow.bind(_d)];
604
+ _validation_decorators = [(_e = observable).shallow.bind(_e)];
546
605
  _get_fields_decorators = [computed];
547
606
  _get_knownFields_decorators = [computed];
548
607
  _get_accessors_decorators = [computed];
608
+ _get_dirty_decorators = [computed];
549
609
  __esDecorate(_a, null, _value_decorators, { kind: "accessor", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, _value_initializers, _value_extraInitializers);
550
610
  __esDecorate(_a, null, _fieldOverrides_decorators, { kind: "accessor", name: "fieldOverrides", static: false, private: false, access: { has: obj => "fieldOverrides" in obj, get: obj => obj.fieldOverrides, set: (obj, value) => { obj.fieldOverrides = value; } }, metadata: _metadata }, _fieldOverrides_initializers, _fieldOverrides_extraInitializers);
611
+ __esDecorate(_a, null, _errorOverrides_decorators, { kind: "accessor", name: "errorOverrides", static: false, private: false, access: { has: obj => "errorOverrides" in obj, get: obj => obj.errorOverrides, set: (obj, value) => { obj.errorOverrides = value; } }, metadata: _metadata }, _errorOverrides_initializers, _errorOverrides_extraInitializers);
551
612
  __esDecorate(_a, null, _validation_decorators, { kind: "accessor", name: "validation", static: false, private: false, access: { has: obj => "validation" in obj, get: obj => obj.validation, set: (obj, value) => { obj.validation = value; } }, metadata: _metadata }, _validation_initializers, _validation_extraInitializers);
552
613
  __esDecorate(_a, null, _get_fields_decorators, { kind: "getter", name: "fields", static: false, private: false, access: { has: obj => "fields" in obj, get: obj => obj.fields }, metadata: _metadata }, null, _instanceExtraInitializers);
553
614
  __esDecorate(_a, null, _get_knownFields_decorators, { kind: "getter", name: "knownFields", static: false, private: false, access: { has: obj => "knownFields" in obj, get: obj => obj.knownFields }, metadata: _metadata }, null, _instanceExtraInitializers);
554
615
  __esDecorate(_a, null, _get_accessors_decorators, { kind: "getter", name: "accessors", static: false, private: false, access: { has: obj => "accessors" in obj, get: obj => obj.accessors }, metadata: _metadata }, null, _instanceExtraInitializers);
616
+ __esDecorate(_a, null, _get_dirty_decorators, { kind: "getter", name: "dirty", static: false, private: false, access: { has: obj => "dirty" in obj, get: obj => obj.dirty }, metadata: _metadata }, null, _instanceExtraInitializers);
555
617
  if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
556
618
  })(),
557
619
  _a;
@@ -19,15 +19,15 @@ export function useDefaultMobxFormHooks(model, { onValidFieldSubmit, onValidForm
19
19
  // (e.g. changing a discriminator)
20
20
  // TODO debounce?
21
21
  setTimeout(function () {
22
- // only start validation if the user has changed the field
23
- if (model.isValuePathActive(path) && model.isDirty(path)) {
22
+ // only start validation if the user has changed the field and there isn't already an error visible
23
+ if (model.isValuePathActive(path) && model.isFieldDirty(path) && model.fields[path].error == null) {
24
24
  // further workaround to make sure we don't downgrade the existing validation
25
25
  model.validateField(path, Math.max(Validation.Changed, model.getValidation(path)));
26
26
  }
27
27
  }, 100);
28
28
  }, [model]);
29
29
  const onFormSubmit = useCallback(function () {
30
- if (model.validateAll()) {
30
+ if (model.validateSubmit()) {
31
31
  onValidFormSubmit === null || onValidFormSubmit === void 0 ? void 0 : onValidFormSubmit(model.value);
32
32
  }
33
33
  }, [