@strictly/react-form 0.0.15 → 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.
Files changed (40) hide show
  1. package/.out/core/mobx/field_adapter_builder.d.ts +1 -1
  2. package/.out/core/mobx/form_model.d.ts +9 -6
  3. package/.out/core/mobx/form_model.js +77 -42
  4. package/.out/core/mobx/specs/form_model.tests.js +80 -20
  5. package/.out/core/mobx/types.d.ts +4 -4
  6. package/.out/core/props.d.ts +2 -0
  7. package/.out/index.d.ts +0 -1
  8. package/.out/index.js +0 -1
  9. package/.out/mantine/create_field_view.d.ts +20 -0
  10. package/.out/mantine/create_field_view.js +54 -0
  11. package/.out/mantine/create_list.js +3 -2
  12. package/.out/mantine/hooks.d.ts +4 -1
  13. package/.out/mantine/hooks.js +14 -2
  14. package/.out/mantine/specs/field_view_hooks.stories.d.ts +12 -0
  15. package/.out/mantine/specs/field_view_hooks.stories.js +61 -0
  16. package/.out/mantine/specs/field_view_hooks.tests.d.ts +1 -0
  17. package/.out/mantine/specs/field_view_hooks.tests.js +12 -0
  18. package/.out/tsconfig.tsbuildinfo +1 -1
  19. package/.turbo/turbo-build.log +8 -8
  20. package/.turbo/turbo-check-types.log +1 -1
  21. package/core/mobx/field_adapter_builder.ts +2 -2
  22. package/core/mobx/form_model.ts +89 -47
  23. package/core/mobx/specs/form_model.tests.ts +131 -11
  24. package/core/mobx/types.ts +4 -4
  25. package/core/props.ts +4 -0
  26. package/dist/index.cjs +165 -89
  27. package/dist/index.d.cts +45 -40
  28. package/dist/index.d.ts +45 -40
  29. package/dist/index.js +162 -81
  30. package/index.ts +0 -1
  31. package/mantine/create_field_view.tsx +94 -0
  32. package/mantine/create_list.tsx +9 -2
  33. package/mantine/hooks.tsx +19 -2
  34. package/mantine/specs/__snapshots__/field_view_hooks.tests.tsx.snap +153 -0
  35. package/mantine/specs/field_view_hooks.stories.tsx +112 -0
  36. package/mantine/specs/field_view_hooks.tests.tsx +15 -0
  37. package/package.json +1 -1
  38. package/.out/mantine/field_view.d.ts +0 -18
  39. package/.out/mantine/field_view.js +0 -16
  40. package/mantine/field_view.tsx +0 -39
@@ -7,12 +7,12 @@ $ tsup
7
7
  CLI Target: es6
8
8
  CJS Build start
9
9
  ESM Build start
10
- ESM dist/index.js 55.85 KB
11
- ESM ⚡️ Build success in 136ms
12
- CJS dist/index.cjs 59.76 KB
13
- CJS ⚡️ Build success in 141ms
10
+ CJS dist/index.cjs 62.28 KB
11
+ CJS ⚡️ Build success in 117ms
12
+ ESM dist/index.js 58.27 KB
13
+ ESM ⚡️ Build success in 140ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 9792ms
16
- DTS dist/index.d.cts 37.63 KB
17
- DTS dist/index.d.ts 37.63 KB
18
- Done in 10.91s.
15
+ DTS ⚡️ Build success in 9749ms
16
+ DTS dist/index.d.cts 38.08 KB
17
+ DTS dist/index.d.ts 38.08 KB
18
+ Done in 10.88s.
@@ -1,3 +1,3 @@
1
1
  yarn run v1.22.22
2
2
  $ tsc
3
- Done in 7.51s.
3
+ Done in 7.55s.
@@ -336,8 +336,8 @@ export function trimmingStringAdapter<
336
336
 
337
337
  export function listAdapter<
338
338
  E,
339
- ValuePath extends string,
340
- Context,
339
+ ValuePath extends string = string,
340
+ Context = unknown,
341
341
  >() {
342
342
  return new FieldAdapterBuilder<readonly E[], readonly E[], never, ValuePath, Context>(
343
343
  annotatedIdentityConverter<readonly E[], ValuePath, Context>(false),
@@ -26,6 +26,7 @@ import {
26
26
  type ValueOfType,
27
27
  valuePathToTypePath,
28
28
  } from '@strictly/define'
29
+ import { type FormMode } from 'core/props'
29
30
  import {
30
31
  computed,
31
32
  observable,
@@ -105,11 +106,18 @@ export type ValuePathsToAdaptersOf<
105
106
  : never
106
107
 
107
108
  export type ContextOf<TypePathsToAdapters extends Partial<Readonly<Record<string, FieldAdapter>>>> =
108
- UnionToIntersection<{
109
- readonly [
110
- K in keyof TypePathsToAdapters
111
- ]: TypePathsToAdapters[K] extends undefined ? undefined : ContextOfFieldAdapter<NonNullable<TypePathsToAdapters[K]>>
112
- }[keyof TypePathsToAdapters]>
109
+ UnionToIntersection<
110
+ | {
111
+ readonly [
112
+ K in keyof TypePathsToAdapters
113
+ ]: TypePathsToAdapters[K] extends undefined ? undefined
114
+ // ignore unspecified values
115
+ : unknown extends ContextOfFieldAdapter<NonNullable<TypePathsToAdapters[K]>> ? never
116
+ : ContextOfFieldAdapter<NonNullable<TypePathsToAdapters[K]>>
117
+ }[keyof TypePathsToAdapters]
118
+ // ensure we have at least one thing to intersect (can end up with a `never` context otherwise)
119
+ | {}
120
+ >
113
121
 
114
122
  export abstract class FormModel<
115
123
  T extends Type,
@@ -135,12 +143,12 @@ export abstract class FormModel<
135
143
 
136
144
  constructor(
137
145
  readonly type: T,
138
- value: ValueOfType<ReadonlyTypeOfType<T>>,
146
+ private readonly originalValue: ValueOfType<ReadonlyTypeOfType<T>>,
139
147
  protected readonly adapters: TypePathsToAdapters,
148
+ protected readonly mode: FormMode,
140
149
  ) {
141
- this.value = mobxCopy(type, value)
150
+ this.value = mobxCopy(type, originalValue)
142
151
  this.flattenedTypeDefs = flattenTypesOfType(type)
143
- const contextValue = this.toContext(value)
144
152
  // pre-populate field overrides for consistent behavior when default information is overwritten
145
153
  // then returned to
146
154
  const conversions = flattenValueTo(
@@ -149,11 +157,14 @@ export abstract class FormModel<
149
157
  () => {},
150
158
  (
151
159
  _t: StrictTypeDef,
152
- value: AnyValueType,
160
+ fieldValue: AnyValueType,
153
161
  _setter,
154
162
  typePath,
155
163
  valuePath,
156
164
  ): AnnotatedFieldConversion<FieldOverride> | undefined => {
165
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
166
+ const contextValue = this.toContext(originalValue, valuePath as keyof ValuePathsToAdapters)
167
+
157
168
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
158
169
  const adapter = this.adapters[typePath as keyof TypePathsToAdapters]
159
170
  if (adapter == null) {
@@ -168,7 +179,7 @@ export abstract class FormModel<
168
179
  return
169
180
  }
170
181
  // cannot call this.context yet as the "this" pointer has not been fully created
171
- return convert(value, valuePath, contextValue)
182
+ return convert(fieldValue, valuePath, contextValue)
172
183
  },
173
184
  )
174
185
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -177,12 +188,21 @@ export abstract class FormModel<
177
188
  }) as FlattenedFieldOverrides<ValuePathsToAdapters>
178
189
  }
179
190
 
180
- @computed.struct
181
- get context() {
182
- return this.toContext(this.value)
183
- }
191
+ protected abstract toContext(
192
+ value: ValueOfType<ReadonlyTypeOfType<T>>,
193
+ valuePath: keyof ValuePathsToAdapters,
194
+ ): ContextType
184
195
 
185
- protected abstract toContext(value: ValueOfType<ReadonlyTypeOfType<T>>): ContextType
196
+ get forceMutableFields() {
197
+ switch (this.mode) {
198
+ case 'create':
199
+ return true
200
+ case 'edit':
201
+ return false
202
+ default:
203
+ return this.mode satisfies never
204
+ }
205
+ }
186
206
 
187
207
  @computed
188
208
  get fields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>> {
@@ -256,6 +276,7 @@ export abstract class FormModel<
256
276
  const accessor = this.getAccessorForValuePath(valuePath)
257
277
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
258
278
  const fieldTypeDef = this.flattenedTypeDefs[typePath as string]
279
+ const context = this.toContext(this.value, valuePath)
259
280
  const {
260
281
  value,
261
282
  required,
@@ -267,20 +288,20 @@ export abstract class FormModel<
267
288
  ? mobxCopy(
268
289
  fieldTypeDef,
269
290
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
270
- create(valuePath as string, this.context),
291
+ create(valuePath as string, context),
271
292
  )
272
293
  // fake values can't be copied
273
294
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
274
- : create(valuePath as string, this.context),
295
+ : create(valuePath as string, context),
275
296
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
276
297
  valuePath as string,
277
- this.context,
298
+ context,
278
299
  )
279
300
  const error = this.errors[valuePath]
280
301
  return {
281
302
  value: fieldOverride != null ? fieldOverride[0] : value,
282
303
  error,
283
- readonly,
304
+ readonly: readonly && !this.forceMutableFields,
284
305
  required,
285
306
  }
286
307
  }
@@ -360,7 +381,9 @@ export abstract class FormModel<
360
381
  : elementAdapter.create(
361
382
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
362
383
  elementTypePath as string,
363
- this.context,
384
+ // TODO what can we use for the value path here?
385
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
386
+ this.toContext(this.value, valuePath as unknown as keyof ValuePathsToAdapters),
364
387
  )
365
388
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
366
389
  const originalList: any[] = accessor.value
@@ -505,7 +528,7 @@ export abstract class FormModel<
505
528
  assertExists(revert, 'setting value not supported {}', valuePath)
506
529
 
507
530
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
508
- const conversion = revert(value, valuePath as any, this.context)
531
+ const conversion = revert(value, valuePath as any, this.toContext(this.value, valuePath))
509
532
  const accessor = this.getAccessorForValuePath(valuePath)
510
533
  return runInAction(() => {
511
534
  this.fieldOverrides[valuePath] = [value]
@@ -548,10 +571,12 @@ export abstract class FormModel<
548
571
  convert,
549
572
  create,
550
573
  } = adapter
551
- const value = create(valuePath, this.context)
574
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
575
+ const context = this.toContext(this.value, valuePath as unknown as keyof ValuePathsToAdapters)
576
+ const value = create(valuePath, context)
552
577
  const {
553
578
  value: displayValue,
554
- } = convert(value, valuePath, this.context)
579
+ } = convert(value, valuePath, context)
555
580
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
556
581
  const key = valuePath as unknown as keyof ValuePathsToAdapters
557
582
  runInAction(() => {
@@ -586,16 +611,18 @@ export abstract class FormModel<
586
611
  } = this.getAdapterForValuePath(valuePath)
587
612
  const fieldOverride = this.fieldOverrides[valuePath]
588
613
  const accessor = this.getAccessorForValuePath(valuePath)
614
+ const context = this.toContext(this.value, valuePath)
615
+
589
616
  const {
590
617
  value: storedValue,
591
618
  } = convert(
592
619
  accessor != null
593
620
  ? accessor.value
594
621
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
595
- : create(valuePath as string, this.context),
622
+ : create(valuePath as string, context),
596
623
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
597
624
  valuePath as string,
598
- this.context,
625
+ context,
599
626
  )
600
627
  const value = fieldOverride != null
601
628
  ? fieldOverride[0]
@@ -605,13 +632,13 @@ export abstract class FormModel<
605
632
  if (ignoreDefaultValue) {
606
633
  const {
607
634
  value: defaultDisplayValue,
608
- } = convert(create(valuePath, this.context), valuePath, this.context)
635
+ } = convert(create(valuePath, context), valuePath, context)
609
636
  if (defaultDisplayValue === value) {
610
637
  return true
611
638
  }
612
639
  }
613
640
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
614
- const conversion = revert(value, valuePath as string, this.context)
641
+ const conversion = revert(value, valuePath as string, context)
615
642
  return runInAction(() => {
616
643
  switch (conversion.type) {
617
644
  case UnreliableFieldConversionType.Failure:
@@ -632,11 +659,14 @@ export abstract class FormModel<
632
659
  })
633
660
  }
634
661
 
635
- validateAll(): boolean {
662
+ validateAll(force: boolean = this.mode === 'create'): boolean {
636
663
  // sort keys shortest to longest so parent changes don't overwrite child changes
637
664
  const accessors = toArray(this.accessors).toSorted(function ([a], [b]) {
638
665
  return a.length - b.length
639
666
  })
667
+
668
+ const flattenedOriginalValues = flattenValuesOfType(this.type, this.originalValue)
669
+
640
670
  return runInAction(() => {
641
671
  return accessors.reduce(
642
672
  (
@@ -662,32 +692,44 @@ export abstract class FormModel<
662
692
  return success
663
693
  }
664
694
  const fieldOverride = this.fieldOverrides[adapterPath]
695
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
696
+ const context = this.toContext(this.value, valuePath as keyof ValuePathsToAdapters)
665
697
  const {
666
698
  value: storedValue,
667
- } = convert(accessor.value, valuePath, this.context)
699
+ } = convert(accessor.value, valuePath, context)
668
700
  const value = fieldOverride != null
669
701
  ? fieldOverride[0]
670
702
  : storedValue
671
- // TODO more nuanced comparison
703
+ // TODO customizable comparisons
672
704
  const dirty = fieldOverride != null && fieldOverride[0] !== storedValue
673
-
674
- const conversion = revert(value, valuePath, this.context)
675
- switch (conversion.type) {
676
- case UnreliableFieldConversionType.Failure:
677
- this.errors[adapterPath] = conversion.error
678
- if (conversion.value != null && dirty) {
679
- accessor.set(conversion.value[0])
680
- }
681
- return false
682
- case UnreliableFieldConversionType.Success:
683
- if (dirty) {
684
- accessor.set(conversion.value)
685
- }
686
- delete this.errors[adapterPath]
687
- return success
688
- default:
689
- throw new UnreachableError(conversion)
705
+ const needsValidation = force
706
+ || !(valuePath in flattenedOriginalValues)
707
+ || storedValue !== convert(
708
+ flattenedOriginalValues[valuePath],
709
+ valuePath,
710
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
711
+ this.toContext(this.originalValue, valuePath as keyof ValuePathsToAdapters),
712
+ ).value
713
+ if (needsValidation) {
714
+ const conversion = revert(value, valuePath, context)
715
+ switch (conversion.type) {
716
+ case UnreliableFieldConversionType.Failure:
717
+ this.errors[adapterPath] = conversion.error
718
+ if (conversion.value != null && dirty) {
719
+ accessor.set(conversion.value[0])
720
+ }
721
+ return false
722
+ case UnreliableFieldConversionType.Success:
723
+ if (dirty) {
724
+ accessor.set(conversion.value)
725
+ }
726
+ delete this.errors[adapterPath]
727
+ return success
728
+ default:
729
+ throw new UnreachableError(conversion)
730
+ }
690
731
  }
732
+ return success
691
733
  },
692
734
  true,
693
735
  )
@@ -2,6 +2,7 @@ import { expectDefinedAndReturn } from '@strictly/base'
2
2
  import {
3
3
  booleanType,
4
4
  type FlattenedValuesOfType,
5
+ flattenValidatorsOfValidatingType,
5
6
  list,
6
7
  nullType,
7
8
  numberType,
@@ -13,7 +14,9 @@ import {
13
14
  type ValueOfType,
14
15
  type ValueToTypePathsOfType,
15
16
  } from '@strictly/define'
16
- import { type FieldAdapter } from 'core/mobx/field_adapter'
17
+ import {
18
+ type FieldAdapter,
19
+ } from 'core/mobx/field_adapter'
17
20
  import {
18
21
  adapterFromTwoWayConverter,
19
22
  identityAdapter,
@@ -23,11 +26,14 @@ import {
23
26
  FormModel,
24
27
  type ValuePathsToAdaptersOf,
25
28
  } from 'core/mobx/form_model'
29
+ import { mergeAdaptersWithValidators } from 'core/mobx/merge_field_adapters_with_validators'
26
30
  import { IntegerToStringConverter } from 'field_converters/integer_to_string_converter'
27
31
  import { NullableToBooleanConverter } from 'field_converters/nullable_to_boolean_converter'
28
32
  import { SelectDiscriminatedUnionConverter } from 'field_converters/select_value_type_converter'
29
33
  import { prototypingFieldValueFactory } from 'field_value_factories/prototyping_field_value_factory'
30
- import { type Simplify } from 'type-fest'
34
+ import {
35
+ type Simplify,
36
+ } from 'type-fest'
31
37
  import { type Field } from 'types/field'
32
38
  import {
33
39
  UnreliableFieldConversionType,
@@ -54,9 +60,13 @@ class TestFormModel<
54
60
  {}
55
61
  >,
56
62
  > extends FormModel<T, ValueToTypePaths, TypePathsToAdapters, {}> {
57
- override toContext() {
63
+ override toContext(
64
+ value: ValueOfType<T>,
65
+ valuePath: keyof ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths>,
66
+ ) {
58
67
  return {
59
- ctx: true,
68
+ value,
69
+ valuePath,
60
70
  }
61
71
  }
62
72
  }
@@ -195,6 +205,7 @@ describe('all', function () {
195
205
  typeDef,
196
206
  originalValue,
197
207
  adapters,
208
+ 'create',
198
209
  )
199
210
  })
200
211
 
@@ -255,6 +266,7 @@ describe('all', function () {
255
266
  typeDef,
256
267
  originalValue,
257
268
  adapters,
269
+ 'create',
258
270
  )
259
271
  })
260
272
 
@@ -296,6 +308,7 @@ describe('all', function () {
296
308
  typeDef,
297
309
  value,
298
310
  adapters,
311
+ 'create',
299
312
  )
300
313
  })
301
314
 
@@ -374,6 +387,7 @@ describe('all', function () {
374
387
  typeDef,
375
388
  value,
376
389
  converters,
390
+ 'create',
377
391
  )
378
392
  })
379
393
 
@@ -444,6 +458,7 @@ describe('all', function () {
444
458
  typeDef,
445
459
  value,
446
460
  converters,
461
+ 'create',
447
462
  )
448
463
  })
449
464
 
@@ -509,6 +524,7 @@ describe('all', function () {
509
524
  typeDef,
510
525
  originalValue,
511
526
  adapters,
527
+ 'create',
512
528
  )
513
529
  })
514
530
 
@@ -635,6 +651,7 @@ describe('all', function () {
635
651
  typeDef,
636
652
  originalValue,
637
653
  converters,
654
+ 'create',
638
655
  )
639
656
  })
640
657
 
@@ -740,10 +757,10 @@ describe('all', function () {
740
757
 
741
758
  // no longer passes context, but will pass context eventually again
742
759
  describe('passes context', function () {
743
- let contextCopy: number[]
760
+ let contextCopy: string
744
761
  beforeEach(function () {
745
762
  integerToStringAdapter.revert.mockImplementationOnce(function (_value, _path, context) {
746
- contextCopy = { ...context }
763
+ contextCopy = JSON.stringify(context)
747
764
  return {
748
765
  type: UnreliableFieldConversionType.Success,
749
766
  value: 1,
@@ -751,7 +768,7 @@ describe('all', function () {
751
768
  })
752
769
  })
753
770
 
754
- it('supplies the full, previous context when converting', function () {
771
+ it('supplies the context when converting', function () {
755
772
  model.setFieldValueAndValidate('$.2', '4')
756
773
 
757
774
  expect(integerToStringAdapter.revert).toHaveBeenCalledOnce()
@@ -759,14 +776,23 @@ describe('all', function () {
759
776
  '4',
760
777
  '$.2',
761
778
  {
762
- ctx: true,
779
+ // the supplied value isn't a copy, so it will be the model value, even
780
+ // if the value has since changed
781
+ value: model.value,
782
+ valuePath: '$.2',
763
783
  },
764
784
  )
765
785
  })
766
786
 
767
- it('supplies the context', function () {
768
- expect(contextCopy).toEqual({
769
- ctx: true,
787
+ it('supplies the correct context value at the time it is being checked', function () {
788
+ // the copy will show the supplied value however
789
+ expect(JSON.parse(contextCopy)).toEqual({
790
+ value: [
791
+ 1,
792
+ 3,
793
+ 7,
794
+ ],
795
+ valuePath: '$.2',
770
796
  })
771
797
  })
772
798
  })
@@ -976,6 +1002,7 @@ describe('all', function () {
976
1002
  type,
977
1003
  originalValue,
978
1004
  adapters,
1005
+ 'create',
979
1006
  )
980
1007
  })
981
1008
 
@@ -1044,6 +1071,7 @@ describe('all', function () {
1044
1071
  a: 1,
1045
1072
  },
1046
1073
  adapters,
1074
+ 'create',
1047
1075
  )
1048
1076
  it.each([
1049
1077
  [
@@ -1076,6 +1104,7 @@ describe('all', function () {
1076
1104
  b: false,
1077
1105
  },
1078
1106
  adapters,
1107
+ 'create',
1079
1108
  )
1080
1109
  it.each([
1081
1110
  [
@@ -1125,6 +1154,7 @@ describe('all', function () {
1125
1154
  typeDef,
1126
1155
  originalValue,
1127
1156
  converters,
1157
+ 'create',
1128
1158
  )
1129
1159
  })
1130
1160
 
@@ -1150,5 +1180,95 @@ describe('all', function () {
1150
1180
  })
1151
1181
  })
1152
1182
  })
1183
+
1184
+ describe('interaction with create and edit modes', () => {
1185
+ const typeDef = object().readonlyField('n', numberType.enforce(n => n < 10 ? 'err' : null))
1186
+ const adapters = mergeAdaptersWithValidators(
1187
+ {
1188
+ $: identityAdapter({ n: 0 }),
1189
+ '$.n': integerToStringAdapter,
1190
+ } as const,
1191
+ flattenValidatorsOfValidatingType(typeDef),
1192
+ )
1193
+ type JsonPaths = {
1194
+ $: '$',
1195
+ '$.n': '$.n',
1196
+ }
1197
+ let originalValue: ValueOfType<typeof typeDef>
1198
+ beforeEach(() => {
1199
+ originalValue = {
1200
+ n: 1,
1201
+ }
1202
+ })
1203
+ describe('create mode', () => {
1204
+ let model: FormModel<
1205
+ typeof typeDef,
1206
+ JsonPaths,
1207
+ typeof adapters
1208
+ >
1209
+ beforeEach(() => {
1210
+ model = new TestFormModel<
1211
+ typeof typeDef,
1212
+ JsonPaths,
1213
+ typeof adapters
1214
+ >(
1215
+ typeDef,
1216
+ originalValue,
1217
+ adapters,
1218
+ 'create',
1219
+ )
1220
+ })
1221
+
1222
+ it('makes the field editable', () => {
1223
+ expect(model.fields['$.n'].readonly).toBeFalsy()
1224
+ })
1225
+
1226
+ it('fails validation', () => {
1227
+ expect(model.validateAll()).toBeFalsy()
1228
+ })
1229
+
1230
+ it('passes validation with valid data', () => {
1231
+ model.setFieldValue('$.n', '10')
1232
+ expect(model.validateAll()).toBeTruthy()
1233
+ })
1234
+ })
1235
+ describe('edit model', () => {
1236
+ let model: FormModel<
1237
+ typeof typeDef,
1238
+ JsonPaths,
1239
+ typeof adapters
1240
+ >
1241
+ beforeEach(function () {
1242
+ model = new TestFormModel<
1243
+ typeof typeDef,
1244
+ JsonPaths,
1245
+ typeof adapters
1246
+ >(
1247
+ typeDef,
1248
+ originalValue,
1249
+ adapters,
1250
+ 'edit',
1251
+ )
1252
+ })
1253
+
1254
+ it('respects the field being readonly', () => {
1255
+ expect(model.fields['$.n'].readonly).toBeTruthy()
1256
+ })
1257
+
1258
+ it('validates successfully with clean, but invalid data', () => {
1259
+ expect(model.validateAll()).toBeTruthy()
1260
+ })
1261
+
1262
+ it('fails validation with invalid, dirty data', () => {
1263
+ model.setFieldValue('$.n', '2')
1264
+ expect(model.validateAll()).toBeFalsy()
1265
+ })
1266
+
1267
+ it('passes validation with valid, dirty data', () => {
1268
+ model.setFieldValue('$.n', '10')
1269
+ expect(model.validateAll()).toBeTruthy()
1270
+ })
1271
+ })
1272
+ })
1153
1273
  })
1154
1274
  })
@@ -6,7 +6,7 @@ import {
6
6
  } from './form_model'
7
7
 
8
8
  /**
9
- * Used to extract the supported value paths from a presenter
9
+ * Used to extract the supported value paths from a model
10
10
  */
11
11
  export type ValuePathsOfModel<
12
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -22,7 +22,7 @@ export type ValuePathsOfModel<
22
22
 
23
23
  /**
24
24
  * Used to extract the render type (so the value that is passed to the view) of a given value path
25
- * from a presenter
25
+ * from a model
26
26
  */
27
27
  export type ToValueOfModelValuePath<
28
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -38,9 +38,9 @@ export type ToValueOfModelValuePath<
38
38
  : never
39
39
 
40
40
  /**
41
- * Extracts the form fields from the presenter. The recommended way is to
41
+ * Extracts the form fields from a form model. The recommended way is to
42
42
  * define the form fields explicitly and use that type to enforce the types
43
- * of your converters, but generating the FormFields from your presenter
43
+ * of your converters, but generating the FormFields from your model
44
44
  * is less typing, albeit at the cost of potentially getting type errors
45
45
  * reported a long way away from the source
46
46
  */
package/core/props.ts CHANGED
@@ -14,8 +14,12 @@ export type FieldsViewProps<F extends Fields> = {
14
14
  onFieldSubmit?(this: void, key: keyof F): boolean | void,
15
15
  }
16
16
 
17
+ export type FormMode = 'edit' | 'create'
18
+
17
19
  export type FormProps<O> = {
18
20
  value: O,
19
21
 
20
22
  onValueChange: (value: O) => void,
23
+
24
+ mode: FormMode,
21
25
  }