@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.
@@ -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;
@@ -44,22 +48,32 @@ export declare abstract class FormModel<T extends Type, ValueToTypePaths extends
44
48
  get fields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>>;
45
49
  private get knownFields();
46
50
  private maybeSynthesizeFieldByValuePath;
51
+ private getField;
47
52
  private synthesizeFieldByPaths;
48
53
  getAccessorForValuePath(valuePath: keyof ValuePathsToAdapters): Accessor | undefined;
49
54
  get accessors(): Readonly<Record<string, Accessor>>;
50
55
  private maybeGetAdapterForValuePath;
51
56
  private getAdapterForValuePath;
57
+ get dirty(): boolean;
52
58
  typePath<K extends keyof ValueToTypePaths>(valuePath: K): ValueToTypePaths[K];
53
59
  setFieldValue<K extends keyof ValuePathsToAdapters>(valuePath: K, value: ToOfFieldAdapter<ValuePathsToAdapters[K]>, validation?: Validation): boolean;
54
60
  addListItem<K extends keyof FlattenedListTypesOfType<T>>(valuePath: K, elementValue?: Maybe<ElementOfArray<FlattenedValuesOfType<T>[K]>>, index?: number): void;
55
61
  removeListItem<K extends keyof FlattenedListTypesOfType<T>>(...elementValuePaths: readonly `${K}.${number}`[]): void;
56
62
  private internalSetFieldValue;
63
+ /**
64
+ * Forces an error onto a field. Error will be removed if the field value changes
65
+ * @param valuePath the field to display an error for
66
+ * @param error the error to display
67
+ */
68
+ overrideFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K, error?: ErrorOfFieldAdapter<ValuePathsToAdapters[K]>): void;
57
69
  clearFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K): void;
58
70
  clearFieldValue<K extends StringKeyOf<ValuePathsToAdapters>>(valuePath: K): void;
59
71
  clearAll(value: ValueOfType<T>): void;
60
72
  isValuePathActive<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
61
73
  getValidation<K extends keyof ValuePathsToAdapters>(valuePath: K): Validation;
74
+ isFieldDirty<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
62
75
  validateField<K extends keyof ValuePathsToAdapters>(valuePath: K, validation?: Validation): boolean;
63
76
  validateAll(validation?: Validation): boolean;
77
+ validateSubmit(): boolean;
64
78
  }
65
79
  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,
@@ -190,8 +197,7 @@ let FormModel = (() => {
190
197
  }
191
198
  return this.synthesizeFieldByPaths(valuePath, typePath);
192
199
  }
193
- synthesizeFieldByPaths(valuePath, typePath) {
194
- var _b;
200
+ getField(valuePath, typePath) {
195
201
  const adapter = this.adapters[typePath];
196
202
  if (adapter == null) {
197
203
  // invalid path, which can happen
@@ -213,45 +219,66 @@ let FormModel = (() => {
213
219
  : defaultValue,
214
220
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
215
221
  valuePath, context);
216
- let error = undefined;
217
222
  const displayedValue = fieldOverride != null ? fieldOverride[0] : value;
223
+ return {
224
+ context,
225
+ convert,
226
+ create,
227
+ revert,
228
+ displayedValue,
229
+ // value,
230
+ required,
231
+ readonly,
232
+ defaultValue,
233
+ };
234
+ }
235
+ synthesizeFieldByPaths(valuePath, typePath) {
236
+ var _b;
237
+ const field = this.getField(valuePath, typePath);
238
+ if (field == null) {
239
+ return;
240
+ }
241
+ const { context, convert, revert, displayedValue, required, readonly, defaultValue, } = field;
218
242
  const validation = (_b = this.validation[valuePath]) !== null && _b !== void 0 ? _b : Validation.None;
219
- switch (validation) {
220
- case Validation.None:
221
- // skip validation
222
- break;
223
- case Validation.Changed:
224
- if (revert != null) {
225
- 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;
226
255
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
227
- ? this.originalValues[valuePath]
228
- : defaultValue;
229
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
230
- const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
231
- // TODO better comparisons, displayed values can still be complex
232
- if (displayedValue !== originalDisplayedValue) {
233
- 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);
234
270
  if ((revertResult === null || revertResult === void 0 ? void 0 : revertResult.type) === UnreliableFieldConversionType.Failure) {
235
271
  ;
236
272
  ({ error } = revertResult);
237
273
  }
238
274
  }
239
- }
240
- break;
241
- case Validation.Always:
242
- {
243
- const revertResult = revert === null || revert === void 0 ? void 0 : revert(displayedValue, valuePath, context);
244
- if ((revertResult === null || revertResult === void 0 ? void 0 : revertResult.type) === UnreliableFieldConversionType.Failure) {
245
- ;
246
- ({ error } = revertResult);
247
- }
248
- }
249
- break;
250
- default:
251
- throw new UnreachableError(validation);
275
+ break;
276
+ default:
277
+ throw new UnreachableError(validation);
278
+ }
252
279
  }
253
280
  return {
254
- value: fieldOverride != null ? fieldOverride[0] : value,
281
+ value: displayedValue,
255
282
  error,
256
283
  readonly: readonly && !this.forceMutableFields,
257
284
  required,
@@ -275,6 +302,12 @@ let FormModel = (() => {
275
302
  getAdapterForValuePath(valuePath) {
276
303
  return assertExistsAndReturn(this.maybeGetAdapterForValuePath(valuePath), 'expected adapter to be defined {}', valuePath);
277
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
+ }
278
311
  typePath(valuePath) {
279
312
  return valuePathToTypePath(this.type, valuePath, true);
280
313
  }
@@ -415,6 +448,7 @@ let FormModel = (() => {
415
448
  const accessor = this.getAccessorForValuePath(valuePath);
416
449
  return runInAction(() => {
417
450
  this.fieldOverrides[valuePath] = [value];
451
+ delete this.errorOverrides[valuePath];
418
452
  if (validation != null) {
419
453
  this.validation[valuePath] = validation;
420
454
  }
@@ -432,11 +466,27 @@ let FormModel = (() => {
432
466
  }
433
467
  });
434
468
  }
469
+ /**
470
+ * Forces an error onto a field. Error will be removed if the field value changes
471
+ * @param valuePath the field to display an error for
472
+ * @param error the error to display
473
+ */
474
+ overrideFieldError(valuePath, error) {
475
+ runInAction(() => {
476
+ if (error) {
477
+ this.errorOverrides[valuePath] = error;
478
+ }
479
+ else {
480
+ delete this.errorOverrides[valuePath];
481
+ }
482
+ });
483
+ }
435
484
  clearFieldError(valuePath) {
436
485
  const fieldOverride = this.fieldOverrides[valuePath];
437
486
  if (fieldOverride != null) {
438
487
  runInAction(() => {
439
488
  delete this.validation[valuePath];
489
+ delete this.errorOverrides[valuePath];
440
490
  });
441
491
  }
442
492
  }
@@ -457,6 +507,7 @@ let FormModel = (() => {
457
507
  runInAction(() => {
458
508
  this.fieldOverrides[key] = [displayValue];
459
509
  delete this.validation[key];
510
+ delete this.errorOverrides[key];
460
511
  });
461
512
  }
462
513
  clearAll(value) {
@@ -464,6 +515,7 @@ let FormModel = (() => {
464
515
  this.validation = {};
465
516
  // TODO this isn't correct, should reload from value
466
517
  this.fieldOverrides = {};
518
+ this.errorOverrides = {};
467
519
  this.value = mobxCopy(this.type, value);
468
520
  });
469
521
  }
@@ -477,9 +529,40 @@ let FormModel = (() => {
477
529
  var _b;
478
530
  return (_b = this.validation[valuePath]) !== null && _b !== void 0 ? _b : Validation.None;
479
531
  }
532
+ isFieldDirty(valuePath) {
533
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
534
+ const typePath = valuePathToTypePath(this.type,
535
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
536
+ valuePath, true);
537
+ const field = this.getField(valuePath, typePath);
538
+ if (field == null) {
539
+ return false;
540
+ }
541
+ const { displayedValue, convert, revert, context, defaultValue, } = field;
542
+ // if either the display value, or the stored value, match the original, then assume it's not dirty
543
+ const originalValue = valuePath in this.originalValues
544
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
545
+ ? this.originalValues[valuePath]
546
+ : defaultValue;
547
+ if (revert != null) {
548
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
549
+ const typeDef = this.flattenedTypeDefs[typePath];
550
+ const { value, type, } = revert(displayedValue, valuePath, context);
551
+ if (type === UnreliableFieldConversionType.Success) {
552
+ if (equals(typeDef, originalValue, value)) {
553
+ return false;
554
+ }
555
+ }
556
+ }
557
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
558
+ const { value: originalDisplayedValue } = convert(originalValue, valuePath, context);
559
+ // try to compare the displayed values directly if we can't revert the displayed value
560
+ return displayedValue !== originalDisplayedValue;
561
+ }
480
562
  validateField(valuePath, validation = Validation.Always) {
481
563
  runInAction(() => {
482
564
  this.validation[valuePath] = validation;
565
+ delete this.errorOverrides[valuePath];
483
566
  });
484
567
  return this.fields[valuePath].error == null;
485
568
  }
@@ -497,24 +580,32 @@ let FormModel = (() => {
497
580
  return (field === null || field === void 0 ? void 0 : field.error) == null;
498
581
  });
499
582
  }
583
+ validateSubmit() {
584
+ return this.validateAll();
585
+ }
500
586
  },
501
587
  _FormModel_value_accessor_storage = new WeakMap(),
502
588
  _FormModel_fieldOverrides_accessor_storage = new WeakMap(),
589
+ _FormModel_errorOverrides_accessor_storage = new WeakMap(),
503
590
  _FormModel_validation_accessor_storage = new WeakMap(),
504
591
  (() => {
505
592
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
506
593
  _value_decorators = [(_b = observable).ref.bind(_b)];
507
594
  _fieldOverrides_decorators = [(_c = observable).shallow.bind(_c)];
508
- _validation_decorators = [(_d = observable).shallow.bind(_d)];
595
+ _errorOverrides_decorators = [(_d = observable).shallow.bind(_d)];
596
+ _validation_decorators = [(_e = observable).shallow.bind(_e)];
509
597
  _get_fields_decorators = [computed];
510
598
  _get_knownFields_decorators = [computed];
511
599
  _get_accessors_decorators = [computed];
600
+ _get_dirty_decorators = [computed];
512
601
  __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);
513
602
  __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);
603
+ __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);
514
604
  __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);
515
605
  __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);
516
606
  __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);
517
607
  __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);
608
+ __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);
518
609
  if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
519
610
  })(),
520
611
  _a;
@@ -2,8 +2,8 @@ import { useCallback, } from 'react';
2
2
  import { Validation, } from './form_model';
3
3
  export function useDefaultMobxFormHooks(model, { onValidFieldSubmit, onValidFormSubmit, } = {}) {
4
4
  const onFieldValueChange = useCallback(function (path, value) {
5
- // clear validation once there are no errors for the field
6
- model.setFieldValue(path, value);
5
+ const validation = Math.min(model.getValidation(path), Validation.Changed);
6
+ model.setFieldValue(path, value, validation);
7
7
  }, [model]);
8
8
  const onFieldSubmit = useCallback(function (valuePath) {
9
9
  if (model.validateField(valuePath)) {
@@ -19,14 +19,15 @@ export function useDefaultMobxFormHooks(model, { onValidFieldSubmit, onValidForm
19
19
  // (e.g. changing a discriminator)
20
20
  // TODO debounce?
21
21
  setTimeout(function () {
22
- if (model.isValuePathActive(path)) {
22
+ // only start validation if the user has changed the field
23
+ if (model.isValuePathActive(path) && model.isFieldDirty(path)) {
23
24
  // further workaround to make sure we don't downgrade the existing validation
24
25
  model.validateField(path, Math.max(Validation.Changed, model.getValidation(path)));
25
26
  }
26
27
  }, 100);
27
28
  }, [model]);
28
29
  const onFormSubmit = useCallback(function () {
29
- if (model.validateAll()) {
30
+ if (model.validateSubmit()) {
30
31
  onValidFormSubmit === null || onValidFormSubmit === void 0 ? void 0 : onValidFormSubmit(model.value);
31
32
  }
32
33
  }, [