@strictly/react-form 0.0.26 → 0.0.28

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.
@@ -3,5 +3,6 @@ export type Field<V = any, E = any> = {
3
3
  readonly error?: E | undefined;
4
4
  readonly readonly: boolean;
5
5
  readonly required: boolean;
6
+ readonly listIndexToKey?: number[];
6
7
  };
7
8
  export type Fields = Readonly<Record<string, Field>>;
@@ -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 59.53 KB
11
- ESM ⚡️ Build success in 105ms
12
- CJS dist/index.cjs 63.62 KB
13
- CJS ⚡️ Build success in 110ms
10
+ CJS dist/index.cjs 62.88 KB
11
+ CJS ⚡️ Build success in 150ms
12
+ ESM dist/index.js 58.76 KB
13
+ ESM ⚡️ Build success in 163ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 30914ms
16
- DTS dist/index.d.cts 38.03 KB
17
- DTS dist/index.d.ts 38.03 KB
18
- Done in 32.12s.
15
+ DTS ⚡️ Build success in 31145ms
16
+ DTS dist/index.d.cts 38.21 KB
17
+ DTS dist/index.d.ts 38.21 KB
18
+ Done in 32.31s.
@@ -1,3 +1,3 @@
1
1
  yarn run v1.22.22
2
2
  $ tsc -b
3
- Done in 0.43s.
3
+ Done in 0.34s.
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  assertExists,
3
3
  assertExistsAndReturn,
4
- assertState,
5
4
  checkValidNumber,
6
5
  type ElementOfArray,
7
6
  map,
@@ -161,13 +160,16 @@ export abstract class FormModel<
161
160
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
161
  private readonly originalValues: Record<string, any>
163
162
 
163
+ // maintains the value paths of lists when the original order is destroyed by deletes or reordering
164
+ private readonly listIndicesToKeys: Record<string, number[]> = {}
165
+
164
166
  constructor(
165
167
  readonly type: T,
166
- originalValue: ValueOfType<ReadonlyTypeOfType<T>>,
168
+ private readonly originalValue: ValueOfType<ReadonlyTypeOfType<T>>,
167
169
  protected readonly adapters: TypePathsToAdapters,
168
170
  protected readonly mode: FormMode,
169
171
  ) {
170
- this.originalValues = flattenValuesOfType<ReadonlyTypeOfType<T>>(type, originalValue)
172
+ this.originalValues = flattenValuesOfType<ReadonlyTypeOfType<T>>(type, originalValue, this.listIndicesToKeys)
171
173
  this.value = mobxCopy(type, originalValue)
172
174
  this.flattenedTypeDefs = flattenTypesOfType(type)
173
175
  // pre-populate field overrides for consistent behavior when default information is overwritten
@@ -202,6 +204,7 @@ export abstract class FormModel<
202
204
  // cannot call this.context yet as the "this" pointer has not been fully created
203
205
  return convert(fieldValue, valuePath, contextValue)
204
206
  },
207
+ this.listIndicesToKeys,
205
208
  )
206
209
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
207
210
  this.fieldOverrides = map(conversions, function (_k, v) {
@@ -260,6 +263,7 @@ export abstract class FormModel<
260
263
  typePath as keyof TypePathsToAdapters,
261
264
  )
262
265
  },
266
+ this.listIndicesToKeys,
263
267
  )
264
268
  }
265
269
 
@@ -335,7 +339,10 @@ export abstract class FormModel<
335
339
  }
336
340
  }
337
341
 
338
- private synthesizeFieldByPaths(valuePath: keyof ValuePathsToAdapters, typePath: keyof TypePathsToAdapters) {
342
+ private synthesizeFieldByPaths(
343
+ valuePath: keyof ValuePathsToAdapters,
344
+ typePath: keyof TypePathsToAdapters,
345
+ ): Field | undefined {
339
346
  const field = this.getField(valuePath, typePath)
340
347
  if (field == null) {
341
348
  return
@@ -391,6 +398,8 @@ export abstract class FormModel<
391
398
  error,
392
399
  readonly: readonly && !this.forceMutableFields,
393
400
  required,
401
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
402
+ listIndexToKey: this.listIndicesToKeys[valuePath as string],
394
403
  }
395
404
  }
396
405
 
@@ -408,6 +417,7 @@ export abstract class FormModel<
408
417
  (value: ValueOfType<T>): void => {
409
418
  this.value = mobxCopy(this.type, value)
410
419
  },
420
+ this.listIndicesToKeys,
411
421
  )
412
422
  }
413
423
 
@@ -434,6 +444,12 @@ export abstract class FormModel<
434
444
  })
435
445
  }
436
446
 
447
+ @computed
448
+ get valueChanged() {
449
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
450
+ return !equals(this.type, this.value, this.originalValue as ValueOfType<T>)
451
+ }
452
+
437
453
  typePath<K extends keyof ValueToTypePaths>(valuePath: K): ValueToTypePaths[K] {
438
454
  return valuePathToTypePath<ValueToTypePaths, K>(this.type, valuePath, true)
439
455
  }
@@ -482,128 +498,53 @@ export abstract class FormModel<
482
498
  element,
483
499
  ...originalList.slice(definedIndex),
484
500
  ]
485
- // shuffle the overrides around to account for new indices
486
- // to so this we need to sort the array indices in descending order
487
- const targetPaths = Object.keys(this.fieldOverrides).filter(function (v) {
488
- return v.startsWith(`${listValuePath}.`)
489
- }).map(function (v) {
490
- const parts = v.substring(listValuePath.length + 1).split('.')
491
- const index = parseInt(parts[0])
492
- return [
493
- index,
494
- parts.slice(1),
495
- ] as const
496
- }).filter(function ([index]) {
497
- return index >= definedIndex
498
- }).sort(function ([a], [b]) {
499
- // descending
500
- return b - a
501
- })
502
501
  runInAction(() => {
503
- targetPaths.forEach(([
504
- index,
505
- postfix,
506
- ]) => {
507
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
508
- const fromJsonPath = [
509
- listValuePath,
510
- `${index}`,
511
- ...postfix,
512
- ].join('.') as keyof ValuePathsToAdapters
513
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
514
- const toJsonPath = [
515
- listValuePath,
516
- `${index + 1}`,
517
- ...postfix,
518
- ].join('.') as keyof ValuePathsToAdapters
519
- const fieldOverride = this.fieldOverrides[fromJsonPath]
520
- delete this.fieldOverrides[fromJsonPath]
521
- this.fieldOverrides[toJsonPath] = fieldOverride
522
- const validation = this.validation[fromJsonPath]
523
- delete this.validation[fromJsonPath]
524
- this.validation[toJsonPath] = validation
525
- })
526
502
  accessor.set(newList)
527
503
  // delete any value overrides so the new list isn't shadowed
528
504
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
529
505
  delete this.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
506
+ const indicesToKeys = assertExistsAndReturn(
507
+ this.listIndicesToKeys[listValuePath],
508
+ 'no index to key mapping for list {}',
509
+ listValuePath,
510
+ )
511
+ const nextKey = indicesToKeys[indicesToKeys.length - 1]
512
+ // insert the next key
513
+ indicesToKeys.splice(definedIndex, 0, nextKey)
514
+ // create the new next key
515
+ indicesToKeys[indicesToKeys.length - 1] = nextKey + 1
530
516
  })
531
517
  }
532
518
 
533
519
  removeListItem<K extends keyof FlattenedListTypesOfType<T>>(...elementValuePaths: readonly `${K}.${number}`[]) {
534
- // sort and reverse so we delete last to first so indices of sequential deletions are preserved
535
- const orderedElementValuePaths = elementValuePaths.toSorted().reverse()
536
520
  runInAction(() => {
537
- orderedElementValuePaths.forEach(elementValuePath => {
521
+ elementValuePaths.forEach(elementValuePath => {
538
522
  const [
539
523
  listValuePath,
540
- elementIndexString,
524
+ elementKeyString,
541
525
  ] = assertExistsAndReturn(
542
526
  jsonPathPop(elementValuePath),
543
527
  'expected a path with two or more segments {}',
544
528
  elementValuePath,
545
529
  )
546
530
  const accessor = this.accessors[listValuePath]
547
- const elementIndex = checkValidNumber(
548
- parseInt(elementIndexString),
549
- 'unexpected index {} ({})',
550
- elementIndexString,
531
+ const elementKey = checkValidNumber(
532
+ parseInt(elementKeyString),
533
+ 'unexpected id {} ({})',
534
+ elementKeyString,
551
535
  elementValuePath,
552
536
  )
553
- const newList = [...accessor.value]
554
- assertState(
555
- elementIndex >= 0 && elementIndex < newList.length,
556
- 'invalid index from path {} ({})',
557
- elementIndex,
558
- elementValuePath,
559
- )
560
- newList.splice(elementIndex, 1)
561
-
562
- // shuffle the overrides around to account for new indices
563
- // to so this we need to sort the array indices in descending order
564
- const targetPaths = Object.keys(this.fieldOverrides).filter(function (v) {
565
- return v.startsWith(`${listValuePath}.`)
566
- }).map(function (v) {
567
- const parts = v.substring(listValuePath.length + 1).split('.')
568
- const index = parseInt(parts[0])
569
- return [
570
- index,
571
- parts.slice(1),
572
- ] as const
573
- }).filter(function ([index]) {
574
- return index > elementIndex
575
- }).sort(function ([a], [b]) {
576
- // descending
577
- return a - b
578
- })
579
-
580
- targetPaths.forEach(([
581
- index,
582
- postfix,
583
- ]) => {
584
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
585
- const fromJsonPath = [
586
- listValuePath,
587
- `${index}`,
588
- ...postfix,
589
- ].join('.') as keyof ValuePathsToAdapters
537
+ const indicesToKeys = this.listIndicesToKeys[listValuePath]
538
+ const elementIndex = indicesToKeys?.indexOf(elementKey) ?? -1
539
+ if (elementIndex >= 0) {
540
+ const newList = [...accessor.value]
541
+ newList.splice(elementIndex, 1)
542
+ accessor.set(newList)
543
+ // delete any value overrides so the new list isn't shadowed
590
544
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
591
- const toJsonPath = [
592
- listValuePath,
593
- `${index - 1}`,
594
- ...postfix,
595
- ].join('.') as keyof ValuePathsToAdapters
596
- const fieldOverride = this.fieldOverrides[fromJsonPath]
597
- delete this.fieldOverrides[fromJsonPath]
598
- this.fieldOverrides[toJsonPath] = fieldOverride
599
- const validation = this.validation[fromJsonPath]
600
- delete this.validation[fromJsonPath]
601
- this.validation[toJsonPath] = validation
602
- })
603
- accessor.set(newList)
604
- // delete any value overrides so the new list isn't shadowed
605
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
606
- delete this.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
545
+ delete this.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
546
+ indicesToKeys.splice(elementIndex, 1)
547
+ }
607
548
  })
608
549
  })
609
550
  }
@@ -706,7 +647,7 @@ export abstract class FormModel<
706
647
  }
707
648
 
708
649
  isValuePathActive<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean {
709
- const values = flattenValuesOfType(this.type, this.value)
650
+ const values = flattenValuesOfType(this.type, this.value, this.listIndicesToKeys)
710
651
  const keys = new Set(Object.keys(values))
711
652
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
712
653
  return keys.has(valuePath as string)
@@ -66,8 +66,8 @@ export function useDefaultMobxFormHooks<
66
66
  // (e.g. changing a discriminator)
67
67
  // TODO debounce?
68
68
  setTimeout(function () {
69
- // only start validation if the user has changed the field
70
- if (model.isValuePathActive(path) && model.isFieldDirty(path)) {
69
+ // only start validation if the user has changed the field and there isn't already an error visible
70
+ if (model.isValuePathActive(path) && model.isFieldDirty(path) && model.fields[path].error == null) {
71
71
  // further workaround to make sure we don't downgrade the existing validation
72
72
  model.validateField(path, Math.max(Validation.Changed, model.getValidation(path)))
73
73
  }
@@ -828,19 +828,20 @@ describe('all', function () {
828
828
 
829
829
  it.each([
830
830
  [
831
- '$.0',
831
+ // new
832
+ '$.3',
832
833
  '0',
833
834
  ],
834
835
  [
835
- '$.1',
836
+ '$.0',
836
837
  'x',
837
838
  ],
838
839
  [
839
- '$.2',
840
+ '$.1',
840
841
  '3',
841
842
  ],
842
843
  [
843
- '$.3',
844
+ '$.2',
844
845
  'z',
845
846
  ],
846
847
  ] as const)('it reports the value of field %s as %s', function (path, fieldValue) {
@@ -849,19 +850,20 @@ describe('all', function () {
849
850
 
850
851
  it.each([
851
852
  [
852
- '$.0',
853
+ // new
854
+ '$.3',
853
855
  undefined,
854
856
  ],
855
857
  [
856
- '$.1',
858
+ '$.0',
857
859
  IS_NAN_ERROR,
858
860
  ],
859
861
  [
860
- '$.2',
862
+ '$.1',
861
863
  undefined,
862
864
  ],
863
865
  [
864
- '$.3',
866
+ '$.2',
865
867
  IS_NAN_ERROR,
866
868
  ],
867
869
  ] as const)('it reports the error of field %s', function (path, error) {
@@ -926,11 +928,11 @@ describe('all', function () {
926
928
 
927
929
  it('updates the field values and errors', function () {
928
930
  expect(model.fields).toEqual({
929
- '$.0': expect.objectContaining({
931
+ '$.1': expect.objectContaining({
930
932
  value: '3',
931
933
  error: undefined,
932
934
  }),
933
- '$.1': expect.objectContaining({
935
+ '$.2': expect.objectContaining({
934
936
  value: 'z',
935
937
  error: IS_NAN_ERROR,
936
938
  }),
@@ -956,7 +958,7 @@ describe('all', function () {
956
958
  value: 'x',
957
959
  error: IS_NAN_ERROR,
958
960
  }),
959
- '$.1': expect.objectContaining({
961
+ '$.2': expect.objectContaining({
960
962
  value: 'z',
961
963
  error: IS_NAN_ERROR,
962
964
  }),
@@ -975,7 +977,7 @@ describe('all', function () {
975
977
 
976
978
  it('updates the field values and errors', function () {
977
979
  expect(model.fields).toEqual({
978
- '$.0': expect.objectContaining({
980
+ '$.2': expect.objectContaining({
979
981
  value: 'z',
980
982
  error: IS_NAN_ERROR,
981
983
  }),