@strictly/react-form 0.0.16 → 0.0.18

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.
@@ -23,15 +23,19 @@ export type ValuePathsToAdaptersOf<TypePathsToAdapters extends Partial<Readonly<
23
23
  export type ContextOf<TypePathsToAdapters extends Partial<Readonly<Record<string, FieldAdapter>>>> = UnionToIntersection<{
24
24
  readonly [K in keyof TypePathsToAdapters]: TypePathsToAdapters[K] extends undefined ? undefined : unknown extends ContextOfFieldAdapter<NonNullable<TypePathsToAdapters[K]>> ? never : ContextOfFieldAdapter<NonNullable<TypePathsToAdapters[K]>>;
25
25
  }[keyof TypePathsToAdapters] | {}>;
26
+ export type FormMode = 'edit' | 'create';
26
27
  export declare abstract class FormModel<T extends Type, ValueToTypePaths extends Readonly<Record<string, string>>, TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<FlattenedValuesOfType<T, '*'>, ContextType>, ContextType = ContextOf<TypePathsToAdapters>, ValuePathsToAdapters extends ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths> = ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths>> {
27
28
  readonly type: T;
29
+ private readonly originalValue;
28
30
  protected readonly adapters: TypePathsToAdapters;
31
+ protected readonly mode: FormMode;
29
32
  accessor value: MobxValueOfType<T>;
30
33
  accessor fieldOverrides: FlattenedFieldOverrides<ValuePathsToAdapters>;
31
34
  accessor errors: FlattenedErrors<ValuePathsToAdapters>;
32
35
  private readonly flattenedTypeDefs;
33
- constructor(type: T, value: ValueOfType<ReadonlyTypeOfType<T>>, adapters: TypePathsToAdapters);
36
+ constructor(type: T, originalValue: ValueOfType<ReadonlyTypeOfType<T>>, adapters: TypePathsToAdapters, mode: FormMode);
34
37
  protected abstract toContext(value: ValueOfType<ReadonlyTypeOfType<T>>, valuePath: keyof ValuePathsToAdapters): ContextType;
38
+ get forceMutableFields(): boolean;
35
39
  get fields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>>;
36
40
  private get knownFields();
37
41
  private maybeSynthesizeFieldByValuePath;
@@ -51,6 +55,6 @@ export declare abstract class FormModel<T extends Type, ValueToTypePaths extends
51
55
  clearAll(value: ValueOfType<T>): void;
52
56
  isValuePathActive<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean;
53
57
  validateField<K extends keyof ValuePathsToAdapters>(valuePath: K, ignoreDefaultValue?: boolean): boolean;
54
- validateAll(): boolean;
58
+ validateAll(force?: boolean): boolean;
55
59
  }
56
60
  export {};
@@ -70,19 +70,31 @@ let FormModel = (() => {
70
70
  set fieldOverrides(value) { __classPrivateFieldSet(this, _FormModel_fieldOverrides_accessor_storage, value, "f"); }
71
71
  get errors() { return __classPrivateFieldGet(this, _FormModel_errors_accessor_storage, "f"); }
72
72
  set errors(value) { __classPrivateFieldSet(this, _FormModel_errors_accessor_storage, value, "f"); }
73
- constructor(type, value, adapters) {
73
+ constructor(type, originalValue, adapters, mode) {
74
74
  Object.defineProperty(this, "type", {
75
75
  enumerable: true,
76
76
  configurable: true,
77
77
  writable: true,
78
78
  value: (__runInitializers(this, _instanceExtraInitializers), type)
79
79
  });
80
+ Object.defineProperty(this, "originalValue", {
81
+ enumerable: true,
82
+ configurable: true,
83
+ writable: true,
84
+ value: originalValue
85
+ });
80
86
  Object.defineProperty(this, "adapters", {
81
87
  enumerable: true,
82
88
  configurable: true,
83
89
  writable: true,
84
90
  value: adapters
85
91
  });
92
+ Object.defineProperty(this, "mode", {
93
+ enumerable: true,
94
+ configurable: true,
95
+ writable: true,
96
+ value: mode
97
+ });
86
98
  _FormModel_value_accessor_storage.set(this, __runInitializers(this, _value_initializers, void 0));
87
99
  _FormModel_fieldOverrides_accessor_storage.set(this, (__runInitializers(this, _value_extraInitializers), __runInitializers(this, _fieldOverrides_initializers, void 0)));
88
100
  _FormModel_errors_accessor_storage.set(this, (__runInitializers(this, _fieldOverrides_extraInitializers), __runInitializers(this, _errors_initializers, {})));
@@ -92,13 +104,13 @@ let FormModel = (() => {
92
104
  writable: true,
93
105
  value: __runInitializers(this, _errors_extraInitializers)
94
106
  });
95
- this.value = mobxCopy(type, value);
107
+ this.value = mobxCopy(type, originalValue);
96
108
  this.flattenedTypeDefs = flattenTypesOfType(type);
97
109
  // pre-populate field overrides for consistent behavior when default information is overwritten
98
110
  // then returned to
99
111
  const conversions = flattenValueTo(type, this.value, () => { }, (_t, fieldValue, _setter, typePath, valuePath) => {
100
112
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
101
- const contextValue = this.toContext(value, valuePath);
113
+ const contextValue = this.toContext(originalValue, valuePath);
102
114
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
103
115
  const adapter = this.adapters[typePath];
104
116
  if (adapter == null) {
@@ -117,6 +129,16 @@ let FormModel = (() => {
117
129
  return v && [v.value];
118
130
  });
119
131
  }
132
+ get forceMutableFields() {
133
+ switch (this.mode) {
134
+ case 'create':
135
+ return true;
136
+ case 'edit':
137
+ return false;
138
+ default:
139
+ return this.mode;
140
+ }
141
+ }
120
142
  get fields() {
121
143
  return new Proxy(this.knownFields, {
122
144
  get: (target, prop) => {
@@ -186,7 +208,7 @@ let FormModel = (() => {
186
208
  return {
187
209
  value: fieldOverride != null ? fieldOverride[0] : value,
188
210
  error,
189
- readonly,
211
+ readonly: readonly && !this.forceMutableFields,
190
212
  required,
191
213
  };
192
214
  }
@@ -452,11 +474,12 @@ let FormModel = (() => {
452
474
  }
453
475
  });
454
476
  }
455
- validateAll() {
477
+ validateAll(force = this.mode === 'create') {
456
478
  // sort keys shortest to longest so parent changes don't overwrite child changes
457
479
  const accessors = toArray(this.accessors).toSorted(function ([a], [b]) {
458
480
  return a.length - b.length;
459
481
  });
482
+ const flattenedOriginalValues = flattenValuesOfType(this.type, this.originalValue);
460
483
  return runInAction(() => {
461
484
  return accessors.reduce((success, [valuePath, accessor,]) => {
462
485
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -478,25 +501,33 @@ let FormModel = (() => {
478
501
  const value = fieldOverride != null
479
502
  ? fieldOverride[0]
480
503
  : storedValue;
481
- // TODO more nuanced comparison
504
+ // TODO customizable comparisons
482
505
  const dirty = fieldOverride != null && fieldOverride[0] !== storedValue;
483
- const conversion = revert(value, valuePath, context);
484
- switch (conversion.type) {
485
- case UnreliableFieldConversionType.Failure:
486
- this.errors[adapterPath] = conversion.error;
487
- if (conversion.value != null && dirty) {
488
- accessor.set(conversion.value[0]);
489
- }
490
- return false;
491
- case UnreliableFieldConversionType.Success:
492
- if (dirty) {
493
- accessor.set(conversion.value);
494
- }
495
- delete this.errors[adapterPath];
496
- return success;
497
- default:
498
- throw new UnreachableError(conversion);
506
+ const needsValidation = force
507
+ || !(valuePath in flattenedOriginalValues)
508
+ || storedValue !== convert(flattenedOriginalValues[valuePath], valuePath,
509
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
510
+ this.toContext(this.originalValue, valuePath)).value;
511
+ if (needsValidation) {
512
+ const conversion = revert(value, valuePath, context);
513
+ switch (conversion.type) {
514
+ case UnreliableFieldConversionType.Failure:
515
+ this.errors[adapterPath] = conversion.error;
516
+ if (conversion.value != null && dirty) {
517
+ accessor.set(conversion.value[0]);
518
+ }
519
+ return false;
520
+ case UnreliableFieldConversionType.Success:
521
+ if (dirty) {
522
+ accessor.set(conversion.value);
523
+ }
524
+ delete this.errors[adapterPath];
525
+ return success;
526
+ default:
527
+ throw new UnreachableError(conversion);
528
+ }
499
529
  }
530
+ return success;
500
531
  }, true);
501
532
  });
502
533
  }
@@ -1,7 +1,8 @@
1
1
  import { expectDefinedAndReturn } from '@strictly/base';
2
- import { booleanType, list, nullType, numberType, object, record, stringType, union, } from '@strictly/define';
2
+ import { booleanType, flattenValidatorsOfValidatingType, list, nullType, numberType, object, record, stringType, union, } from '@strictly/define';
3
3
  import { adapterFromTwoWayConverter, identityAdapter, } from 'core/mobx/field_adapter_builder';
4
4
  import { FormModel, } from 'core/mobx/form_model';
5
+ import { mergeAdaptersWithValidators } from 'core/mobx/merge_field_adapters_with_validators';
5
6
  import { IntegerToStringConverter } from 'field_converters/integer_to_string_converter';
6
7
  import { NullableToBooleanConverter } from 'field_converters/nullable_to_boolean_converter';
7
8
  import { SelectDiscriminatedUnionConverter } from 'field_converters/select_value_type_converter';
@@ -72,7 +73,7 @@ describe('all', function () {
72
73
  let model;
73
74
  beforeEach(function () {
74
75
  originalValue = 5;
75
- model = new TestFormModel(typeDef, originalValue, adapters);
76
+ model = new TestFormModel(typeDef, originalValue, adapters, 'create');
76
77
  });
77
78
  describe('accessors', function () {
78
79
  it('gets the expected value', function () {
@@ -113,7 +114,7 @@ describe('all', function () {
113
114
  readonly: false,
114
115
  });
115
116
  originalValue = 5;
116
- model = new TestFormModel(typeDef, originalValue, adapters);
117
+ model = new TestFormModel(typeDef, originalValue, adapters, 'create');
117
118
  });
118
119
  it('reports required status', function () {
119
120
  expect(model.fields).toEqual(expect.objectContaining({
@@ -138,7 +139,7 @@ describe('all', function () {
138
139
  4,
139
140
  17,
140
141
  ];
141
- model = new TestFormModel(typeDef, value, adapters);
142
+ model = new TestFormModel(typeDef, value, adapters, 'create');
142
143
  });
143
144
  describe('accessors', function () {
144
145
  it.each([
@@ -200,7 +201,7 @@ describe('all', function () {
200
201
  a: 1,
201
202
  b: 2,
202
203
  };
203
- model = new TestFormModel(typeDef, value, converters);
204
+ model = new TestFormModel(typeDef, value, converters, 'create');
204
205
  });
205
206
  describe('accessors', function () {
206
207
  it.each([
@@ -251,7 +252,7 @@ describe('all', function () {
251
252
  a: 1,
252
253
  b: true,
253
254
  };
254
- model = new TestFormModel(typeDef, value, converters);
255
+ model = new TestFormModel(typeDef, value, converters, 'create');
255
256
  });
256
257
  describe('accessors', function () {
257
258
  it.each([
@@ -297,7 +298,7 @@ describe('all', function () {
297
298
  const originalValue = 2;
298
299
  let model;
299
300
  beforeEach(function () {
300
- model = new TestFormModel(typeDef, originalValue, adapters);
301
+ model = new TestFormModel(typeDef, originalValue, adapters, 'create');
301
302
  });
302
303
  describe('setFieldValueAndValidate', function () {
303
304
  describe('success', function () {
@@ -399,7 +400,7 @@ describe('all', function () {
399
400
  3,
400
401
  7,
401
402
  ];
402
- model = new TestFormModel(typeDef, originalValue, converters);
403
+ model = new TestFormModel(typeDef, originalValue, converters, 'create');
403
404
  });
404
405
  describe('setFieldValueAndValidate', function () {
405
406
  describe('success', function () {
@@ -697,7 +698,7 @@ describe('all', function () {
697
698
  let model;
698
699
  beforeEach(function () {
699
700
  originalValue = null;
700
- model = new TestFormModel(type, originalValue, adapters);
701
+ model = new TestFormModel(type, originalValue, adapters, 'create');
701
702
  });
702
703
  it('has the expected fields', function () {
703
704
  expect(model.fields).toEqual({
@@ -745,7 +746,7 @@ describe('all', function () {
745
746
  const model = new TestFormModel(type, {
746
747
  d: 'x',
747
748
  a: 1,
748
- }, adapters);
749
+ }, adapters, 'create');
749
750
  it.each([
750
751
  [
751
752
  '$',
@@ -768,7 +769,7 @@ describe('all', function () {
768
769
  const model = new TestFormModel(type, {
769
770
  d: 'y',
770
771
  b: false,
771
- }, adapters);
772
+ }, adapters, 'create');
772
773
  it.each([
773
774
  [
774
775
  '$',
@@ -800,7 +801,7 @@ describe('all', function () {
800
801
  let model;
801
802
  beforeEach(function () {
802
803
  originalValue = 1;
803
- model = new TestFormModel(typeDef, originalValue, converters);
804
+ model = new TestFormModel(typeDef, originalValue, converters, 'create');
804
805
  });
805
806
  it('returns the default value for the fake field', function () {
806
807
  expect(model.fields['$.fake']).toEqual(expect.objectContaining({
@@ -821,5 +822,54 @@ describe('all', function () {
821
822
  });
822
823
  });
823
824
  });
825
+ describe('interaction with create and edit modes', () => {
826
+ const typeDef = object().readonlyField('n', numberType.enforce(n => n < 10 ? 'err' : null));
827
+ const adapters = mergeAdaptersWithValidators({
828
+ $: identityAdapter({ n: 0 }),
829
+ '$.n': integerToStringAdapter,
830
+ }, flattenValidatorsOfValidatingType(typeDef));
831
+ let originalValue;
832
+ beforeEach(() => {
833
+ originalValue = {
834
+ n: 1,
835
+ };
836
+ });
837
+ describe('create mode', () => {
838
+ let model;
839
+ beforeEach(() => {
840
+ model = new TestFormModel(typeDef, originalValue, adapters, 'create');
841
+ });
842
+ it('makes the field editable', () => {
843
+ expect(model.fields['$.n'].readonly).toBeFalsy();
844
+ });
845
+ it('fails validation', () => {
846
+ expect(model.validateAll()).toBeFalsy();
847
+ });
848
+ it('passes validation with valid data', () => {
849
+ model.setFieldValue('$.n', '10');
850
+ expect(model.validateAll()).toBeTruthy();
851
+ });
852
+ });
853
+ describe('edit model', () => {
854
+ let model;
855
+ beforeEach(function () {
856
+ model = new TestFormModel(typeDef, originalValue, adapters, 'edit');
857
+ });
858
+ it('respects the field being readonly', () => {
859
+ expect(model.fields['$.n'].readonly).toBeTruthy();
860
+ });
861
+ it('validates successfully with clean, but invalid data', () => {
862
+ expect(model.validateAll()).toBeTruthy();
863
+ });
864
+ it('fails validation with invalid, dirty data', () => {
865
+ model.setFieldValue('$.n', '2');
866
+ expect(model.validateAll()).toBeFalsy();
867
+ });
868
+ it('passes validation with valid, dirty data', () => {
869
+ model.setFieldValue('$.n', '10');
870
+ expect(model.validateAll()).toBeTruthy();
871
+ });
872
+ });
873
+ });
824
874
  });
825
875
  });
@@ -67,7 +67,7 @@ export function useMantineFormFields({ onFieldValueChange, onFieldBlur, onFieldF
67
67
  const form = useMemo(function () {
68
68
  return new MantineFormImpl(fields);
69
69
  },
70
- // handled separately below
70
+ // fields handled separately below
71
71
  // eslint-disable-next-line react-hooks/exhaustive-deps
72
72
  []);
73
73
  useEffect(function () {