@strictly/react-form 0.0.16 → 0.0.17
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/.out/core/mobx/form_model.d.ts +6 -2
- package/.out/core/mobx/form_model.js +53 -22
- package/.out/core/mobx/specs/form_model.tests.js +62 -12
- package/.out/core/props.d.ts +2 -0
- package/.out/mantine/hooks.js +1 -1
- package/.out/tsconfig.tsbuildinfo +1 -1
- package/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-check-types.log +1 -1
- package/core/mobx/form_model.ts +49 -23
- package/core/mobx/specs/form_model.tests.ts +103 -0
- package/core/props.ts +4 -0
- package/dist/index.cjs +44 -22
- package/dist/index.d.cts +20 -15
- package/dist/index.d.ts +20 -15
- package/dist/index.js +44 -22
- package/mantine/hooks.tsx +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ElementOfArray, type Maybe } from '@strictly/base';
|
|
2
2
|
import { type Accessor, type FlattenedValuesOfType, type MobxValueOfType, type ReadonlyTypeOfType, type Type, type ValueOfType } from '@strictly/define';
|
|
3
|
+
import { type FormMode } from 'core/props';
|
|
3
4
|
import { type ReadonlyDeep, type SimplifyDeep, type StringKeyOf, type UnionToIntersection, type ValueOf } from 'type-fest';
|
|
4
5
|
import { type Field } from 'types/field';
|
|
5
6
|
import { type ContextOfFieldAdapter, type ErrorOfFieldAdapter, type FieldAdapter, type ToOfFieldAdapter } from './field_adapter';
|
|
@@ -25,13 +26,16 @@ export type ContextOf<TypePathsToAdapters extends Partial<Readonly<Record<string
|
|
|
25
26
|
}[keyof TypePathsToAdapters] | {}>;
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
|
504
|
+
// TODO customizable comparisons
|
|
482
505
|
const dirty = fieldOverride != null && fieldOverride[0] !== storedValue;
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
});
|
package/.out/core/props.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export type FieldsViewProps<F extends Fields> = {
|
|
|
6
6
|
onFieldBlur?(this: void, key: keyof F): void;
|
|
7
7
|
onFieldSubmit?(this: void, key: keyof F): boolean | void;
|
|
8
8
|
};
|
|
9
|
+
export type FormMode = 'edit' | 'create';
|
|
9
10
|
export type FormProps<O> = {
|
|
10
11
|
value: O;
|
|
11
12
|
onValueChange: (value: O) => void;
|
|
13
|
+
mode: FormMode;
|
|
12
14
|
};
|
package/.out/mantine/hooks.js
CHANGED
|
@@ -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 () {
|