@strictly/react-form 0.0.27 → 0.0.29

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.
@@ -34,6 +34,7 @@ export type ContextOf<TypePathsToAdapters extends Partial<Readonly<Record<string
34
34
  export type FormMode = 'edit' | 'create';
35
35
  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>> {
36
36
  readonly type: T;
37
+ private readonly originalValue;
37
38
  protected readonly adapters: TypePathsToAdapters;
38
39
  protected readonly mode: FormMode;
39
40
  accessor value: MobxValueOfType<T>;
@@ -42,6 +43,7 @@ export declare abstract class FormModel<T extends Type, ValueToTypePaths extends
42
43
  accessor validation: FlattenedValidation<ValuePathsToAdapters>;
43
44
  private readonly flattenedTypeDefs;
44
45
  private readonly originalValues;
46
+ private readonly listIndicesToKeys;
45
47
  constructor(type: T, originalValue: ValueOfType<ReadonlyTypeOfType<T>>, adapters: TypePathsToAdapters, mode: FormMode);
46
48
  protected abstract toContext(value: ValueOfType<ReadonlyTypeOfType<T>>, valuePath: keyof ValuePathsToAdapters): ContextType;
47
49
  get forceMutableFields(): boolean;
@@ -55,11 +57,11 @@ export declare abstract class FormModel<T extends Type, ValueToTypePaths extends
55
57
  private maybeGetAdapterForValuePath;
56
58
  private getAdapterForValuePath;
57
59
  get dirty(): boolean;
60
+ get valueChanged(): boolean;
58
61
  typePath<K extends keyof ValueToTypePaths>(valuePath: K): ValueToTypePaths[K];
59
62
  setFieldValue<K extends keyof ValuePathsToAdapters>(valuePath: K, value: ToOfFieldAdapter<ValuePathsToAdapters[K]>, validation?: Validation): boolean;
60
63
  addListItem<K extends keyof FlattenedListTypesOfType<T>>(valuePath: K, elementValue?: Maybe<ElementOfArray<FlattenedValuesOfType<T>[K]>>, index?: number): void;
61
64
  removeListItem<K extends keyof FlattenedListTypesOfType<T>>(...elementValuePaths: readonly `${K}.${number}`[]): void;
62
- protected moveListItem<K extends keyof FlattenedListTypesOfType<T>>(fromValuePath: K, toValuePath: K): void;
63
65
  private internalSetFieldValue;
64
66
  /**
65
67
  * Forces an error onto a field. Error will be removed if the field value changes
@@ -43,7 +43,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
43
43
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
44
44
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
45
45
  };
46
- import { assertExists, assertExistsAndReturn, assertState, checkValidNumber, map, toArray, UnreachableError, } from '@strictly/base';
46
+ import { assertExists, assertExistsAndReturn, checkValidNumber, map, toArray, UnreachableError, } from '@strictly/base';
47
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';
@@ -73,6 +73,7 @@ let FormModel = (() => {
73
73
  let _get_knownFields_decorators;
74
74
  let _get_accessors_decorators;
75
75
  let _get_dirty_decorators;
76
+ let _get_valueChanged_decorators;
76
77
  return _a = class FormModel {
77
78
  get value() { return __classPrivateFieldGet(this, _FormModel_value_accessor_storage, "f"); }
78
79
  set value(value) { __classPrivateFieldSet(this, _FormModel_value_accessor_storage, value, "f"); }
@@ -89,6 +90,12 @@ let FormModel = (() => {
89
90
  writable: true,
90
91
  value: (__runInitializers(this, _instanceExtraInitializers), type)
91
92
  });
93
+ Object.defineProperty(this, "originalValue", {
94
+ enumerable: true,
95
+ configurable: true,
96
+ writable: true,
97
+ value: originalValue
98
+ });
92
99
  Object.defineProperty(this, "adapters", {
93
100
  enumerable: true,
94
101
  configurable: true,
@@ -119,7 +126,14 @@ let FormModel = (() => {
119
126
  writable: true,
120
127
  value: void 0
121
128
  });
122
- this.originalValues = flattenValuesOfType(type, originalValue);
129
+ // maintains the value paths of lists when the original order is destroyed by deletes or reordering
130
+ Object.defineProperty(this, "listIndicesToKeys", {
131
+ enumerable: true,
132
+ configurable: true,
133
+ writable: true,
134
+ value: {}
135
+ });
136
+ this.originalValues = flattenValuesOfType(type, originalValue, this.listIndicesToKeys);
123
137
  this.value = mobxCopy(type, originalValue);
124
138
  this.flattenedTypeDefs = flattenTypesOfType(type);
125
139
  // pre-populate field overrides for consistent behavior when default information is overwritten
@@ -139,7 +153,7 @@ let FormModel = (() => {
139
153
  }
140
154
  // cannot call this.context yet as the "this" pointer has not been fully created
141
155
  return convert(fieldValue, valuePath, contextValue);
142
- });
156
+ }, this.listIndicesToKeys);
143
157
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
144
158
  this.fieldOverrides = map(conversions, function (_k, v) {
145
159
  return v && [v.value];
@@ -179,7 +193,7 @@ let FormModel = (() => {
179
193
  valuePath,
180
194
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
181
195
  typePath);
182
- });
196
+ }, this.listIndicesToKeys);
183
197
  }
184
198
  maybeSynthesizeFieldByValuePath(valuePath) {
185
199
  let typePath;
@@ -233,7 +247,7 @@ let FormModel = (() => {
233
247
  };
234
248
  }
235
249
  synthesizeFieldByPaths(valuePath, typePath) {
236
- var _b;
250
+ var _b, _c;
237
251
  const field = this.getField(valuePath, typePath);
238
252
  if (field == null) {
239
253
  return;
@@ -282,6 +296,9 @@ let FormModel = (() => {
282
296
  error,
283
297
  readonly: readonly && !this.forceMutableFields,
284
298
  required,
299
+ // make a copy of the index mapping and remove the final value (next id)
300
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
301
+ listIndexToKey: (_c = this.listIndicesToKeys[valuePath]) === null || _c === void 0 ? void 0 : _c.slice(0, -1),
285
302
  };
286
303
  }
287
304
  getAccessorForValuePath(valuePath) {
@@ -291,7 +308,7 @@ let FormModel = (() => {
291
308
  get accessors() {
292
309
  return flattenAccessorsOfType(this.type, this.value, (value) => {
293
310
  this.value = mobxCopy(this.type, value);
294
- });
311
+ }, this.listIndicesToKeys);
295
312
  }
296
313
  maybeGetAdapterForValuePath(valuePath) {
297
314
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -308,6 +325,10 @@ let FormModel = (() => {
308
325
  return this.isFieldDirty(valuePath);
309
326
  });
310
327
  }
328
+ get valueChanged() {
329
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
330
+ return !equals(this.type, this.value, this.originalValue);
331
+ }
311
332
  typePath(valuePath) {
312
333
  return valuePathToTypePath(this.type, valuePath, true);
313
334
  }
@@ -341,113 +362,40 @@ let FormModel = (() => {
341
362
  element,
342
363
  ...originalList.slice(definedIndex),
343
364
  ];
344
- // shuffle the overrides around to account for new indices
345
- // to so this we need to sort the array indices in descending order
346
- const targetPaths = Object.keys(this.fieldOverrides).filter(function (v) {
347
- return v.startsWith(`${listValuePath}.`);
348
- }).map(function (v) {
349
- const parts = v.substring(listValuePath.length + 1).split('.');
350
- const index = parseInt(parts[0]);
351
- return [
352
- index,
353
- parts.slice(1),
354
- ];
355
- }).filter(function ([index]) {
356
- return index >= definedIndex;
357
- }).sort(function ([a], [b]) {
358
- // descending
359
- return b - a;
360
- });
361
365
  runInAction(() => {
362
- targetPaths.forEach(([index, postfix,]) => {
363
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
364
- const fromJsonPath = [
365
- listValuePath,
366
- `${index}`,
367
- ...postfix,
368
- ].join('.');
369
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
370
- const toJsonPath = [
371
- listValuePath,
372
- `${index + 1}`,
373
- ...postfix,
374
- ].join('.');
375
- const fieldOverride = this.fieldOverrides[fromJsonPath];
376
- delete this.fieldOverrides[fromJsonPath];
377
- this.fieldOverrides[toJsonPath] = fieldOverride;
378
- const validation = this.validation[fromJsonPath];
379
- delete this.validation[fromJsonPath];
380
- this.validation[toJsonPath] = validation;
381
- });
382
366
  accessor.set(newList);
383
367
  // delete any value overrides so the new list isn't shadowed
384
368
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
385
369
  delete this.fieldOverrides[listValuePath];
370
+ const indicesToKeys = assertExistsAndReturn(this.listIndicesToKeys[listValuePath], 'no index to key mapping for list {}', listValuePath);
371
+ const nextKey = indicesToKeys[indicesToKeys.length - 1];
372
+ // insert the next key
373
+ indicesToKeys.splice(definedIndex, 0, nextKey);
374
+ // create the new next key
375
+ indicesToKeys[indicesToKeys.length - 1] = nextKey + 1;
386
376
  });
387
377
  }
388
378
  removeListItem(...elementValuePaths) {
389
- // sort and reverse so we delete last to first so indices of sequential deletions are preserved
390
- const orderedElementValuePaths = elementValuePaths.toSorted().reverse();
391
379
  runInAction(() => {
392
- orderedElementValuePaths.forEach(elementValuePath => {
393
- const [listValuePath, elementIndexString,] = assertExistsAndReturn(jsonPathPop(elementValuePath), 'expected a path with two or more segments {}', elementValuePath);
380
+ elementValuePaths.forEach(elementValuePath => {
381
+ var _b;
382
+ const [listValuePath, elementKeyString,] = assertExistsAndReturn(jsonPathPop(elementValuePath), 'expected a path with two or more segments {}', elementValuePath);
394
383
  const accessor = this.accessors[listValuePath];
395
- const elementIndex = checkValidNumber(parseInt(elementIndexString), 'unexpected index {} ({})', elementIndexString, elementValuePath);
396
- const newList = [...accessor.value];
397
- assertState(elementIndex >= 0 && elementIndex < newList.length, 'invalid index from path {} ({})', elementIndex, elementValuePath);
398
- newList.splice(elementIndex, 1);
399
- // shuffle the overrides around to account for new indices
400
- // to so this we need to sort the array indices in descending order
401
- const targetPaths = Object.keys(this.fieldOverrides).filter(function (v) {
402
- return v.startsWith(`${listValuePath}.`);
403
- }).map(function (v) {
404
- const parts = v.substring(listValuePath.length + 1).split('.');
405
- const index = parseInt(parts[0]);
406
- return [
407
- index,
408
- parts.slice(1),
409
- ];
410
- }).filter(function ([index]) {
411
- return index > elementIndex;
412
- }).sort(function ([a], [b]) {
413
- // descending
414
- return a - b;
415
- });
416
- targetPaths.forEach(([index, postfix,]) => {
417
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
418
- const fromJsonPath = [
419
- listValuePath,
420
- `${index}`,
421
- ...postfix,
422
- ].join('.');
384
+ const elementKey = checkValidNumber(parseInt(elementKeyString), 'unexpected id {} ({})', elementKeyString, elementValuePath);
385
+ const indicesToKeys = this.listIndicesToKeys[listValuePath];
386
+ const elementIndex = (_b = indicesToKeys === null || indicesToKeys === void 0 ? void 0 : indicesToKeys.indexOf(elementKey)) !== null && _b !== void 0 ? _b : -1;
387
+ if (elementIndex >= 0) {
388
+ const newList = [...accessor.value];
389
+ newList.splice(elementIndex, 1);
390
+ accessor.set(newList);
391
+ // delete any value overrides so the new list isn't shadowed
423
392
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
424
- const toJsonPath = [
425
- listValuePath,
426
- `${index - 1}`,
427
- ...postfix,
428
- ].join('.');
429
- const fieldOverride = this.fieldOverrides[fromJsonPath];
430
- delete this.fieldOverrides[fromJsonPath];
431
- this.fieldOverrides[toJsonPath] = fieldOverride;
432
- const validation = this.validation[fromJsonPath];
433
- delete this.validation[fromJsonPath];
434
- this.validation[toJsonPath] = validation;
435
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
436
- this.moveListItem(fromJsonPath, toJsonPath);
437
- });
438
- accessor.set(newList);
439
- // delete any value overrides so the new list isn't shadowed
440
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
441
- delete this.fieldOverrides[listValuePath];
393
+ delete this.fieldOverrides[listValuePath];
394
+ indicesToKeys.splice(elementIndex, 1);
395
+ }
442
396
  });
443
397
  });
444
398
  }
445
- moveListItem(fromValuePath, toValuePath) {
446
- // do nothing, this is for subclasses to override
447
- // put in some nonsense so TS doesn't complain about the parameters not being used
448
- fromValuePath;
449
- toValuePath;
450
- }
451
399
  internalSetFieldValue(valuePath, value, validation) {
452
400
  const { revert } = this.getAdapterForValuePath(valuePath);
453
401
  assertExists(revert, 'setting value not supported {}', valuePath);
@@ -528,7 +476,7 @@ let FormModel = (() => {
528
476
  });
529
477
  }
530
478
  isValuePathActive(valuePath) {
531
- const values = flattenValuesOfType(this.type, this.value);
479
+ const values = flattenValuesOfType(this.type, this.value, this.listIndicesToKeys);
532
480
  const keys = new Set(Object.keys(values));
533
481
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
534
482
  return keys.has(valuePath);
@@ -606,6 +554,7 @@ let FormModel = (() => {
606
554
  _get_knownFields_decorators = [computed];
607
555
  _get_accessors_decorators = [computed];
608
556
  _get_dirty_decorators = [computed];
557
+ _get_valueChanged_decorators = [computed];
609
558
  __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);
610
559
  __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);
611
560
  __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);
@@ -614,6 +563,7 @@ let FormModel = (() => {
614
563
  __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);
615
564
  __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);
616
565
  __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);
566
+ __esDecorate(_a, null, _get_valueChanged_decorators, { kind: "getter", name: "valueChanged", static: false, private: false, access: { has: obj => "valueChanged" in obj, get: obj => obj.valueChanged }, metadata: _metadata }, null, _instanceExtraInitializers);
617
567
  if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
618
568
  })(),
619
569
  _a;
@@ -546,19 +546,20 @@ describe('all', function () {
546
546
  });
547
547
  it.each([
548
548
  [
549
- '$.0',
549
+ // new
550
+ '$.3',
550
551
  '0',
551
552
  ],
552
553
  [
553
- '$.1',
554
+ '$.0',
554
555
  'x',
555
556
  ],
556
557
  [
557
- '$.2',
558
+ '$.1',
558
559
  '3',
559
560
  ],
560
561
  [
561
- '$.3',
562
+ '$.2',
562
563
  'z',
563
564
  ],
564
565
  ])('it reports the value of field %s as %s', function (path, fieldValue) {
@@ -567,19 +568,20 @@ describe('all', function () {
567
568
  });
568
569
  it.each([
569
570
  [
570
- '$.0',
571
+ // new
572
+ '$.3',
571
573
  undefined,
572
574
  ],
573
575
  [
574
- '$.1',
576
+ '$.0',
575
577
  IS_NAN_ERROR,
576
578
  ],
577
579
  [
578
- '$.2',
580
+ '$.1',
579
581
  undefined,
580
582
  ],
581
583
  [
582
- '$.3',
584
+ '$.2',
583
585
  IS_NAN_ERROR,
584
586
  ],
585
587
  ])('it reports the error of field %s', function (path, error) {
@@ -636,11 +638,11 @@ describe('all', function () {
636
638
  });
637
639
  it('updates the field values and errors', function () {
638
640
  expect(model.fields).toEqual({
639
- '$.0': expect.objectContaining({
641
+ '$.1': expect.objectContaining({
640
642
  value: '3',
641
643
  error: undefined,
642
644
  }),
643
- '$.1': expect.objectContaining({
645
+ '$.2': expect.objectContaining({
644
646
  value: 'z',
645
647
  error: IS_NAN_ERROR,
646
648
  }),
@@ -663,7 +665,7 @@ describe('all', function () {
663
665
  value: 'x',
664
666
  error: IS_NAN_ERROR,
665
667
  }),
666
- '$.1': expect.objectContaining({
668
+ '$.2': expect.objectContaining({
667
669
  value: 'z',
668
670
  error: IS_NAN_ERROR,
669
671
  }),
@@ -679,7 +681,7 @@ describe('all', function () {
679
681
  });
680
682
  it('updates the field values and errors', function () {
681
683
  expect(model.fields).toEqual({
682
- '$.0': expect.objectContaining({
684
+ '$.2': expect.objectContaining({
683
685
  value: 'z',
684
686
  error: IS_NAN_ERROR,
685
687
  }),
@@ -6,11 +6,12 @@ import { type ValueTypeOfField } from 'types/value_type_of_field';
6
6
  import { type MantineFieldComponent, type MantineForm } from './types';
7
7
  export type SuppliedListProps<Value = any, ListPath extends string = string> = {
8
8
  values: readonly Value[];
9
+ indexKeys: number[];
9
10
  listPath: ListPath;
10
11
  };
11
12
  export declare function createList<F extends Fields, K extends keyof ListFieldsOfFields<F>, Props extends SuppliedListProps<ElementOfArray<ValueTypeOfField<F[K]>>> & {
12
13
  children: (valuePath: `${K}.${number}`, value: ElementOfArray<ValueTypeOfField<F[K]>>, index: number) => React.ReactNode;
13
14
  }>(this: MantineForm<F>, valuePath: K, List: ComponentType<Props>): MantineFieldComponent<SuppliedListProps<ElementOfArray<ValueTypeOfField<F[K]>>>, Props, never>;
14
- export declare function DefaultList<Value, ListPath extends string>({ values, listPath, children, }: SuppliedListProps<Value, ListPath> & {
15
+ export declare function DefaultList<Value, ListPath extends string>({ values, indexKeys, listPath, children, }: SuppliedListProps<Value, ListPath> & {
15
16
  children: (valuePath: `${ListPath}.${number}`, value: Value, index: number) => React.ReactNode;
16
17
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,19 +1,31 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { assertExistsAndReturn, } from '@strictly/base';
2
3
  import { Fragment, } from 'react';
3
4
  import { createUnsafePartialObserverComponent } from 'util/partial';
4
5
  export function createList(valuePath, List) {
5
6
  const propSource = () => {
6
- const values = [...this.fields[valuePath].value];
7
+ const field = this.fields[valuePath];
8
+ const values = [...field.value];
7
9
  return {
8
10
  values,
9
11
  listPath: valuePath,
12
+ indexKeys: assertExistsAndReturn(field.listIndexToKey, 'list index to key mapping missing in {}', valuePath),
10
13
  };
11
14
  };
12
15
  return createUnsafePartialObserverComponent(List, propSource);
13
16
  }
14
- export function DefaultList({ values, listPath, children, }) {
17
+ export function DefaultList({ values, indexKeys, listPath, children, }) {
15
18
  return (_jsx(_Fragment, { children: values.map(function (value, index) {
16
- const valuePath = `${listPath}.${index}`;
19
+ return [
20
+ value,
21
+ index,
22
+ indexKeys[index],
23
+ ];
24
+ }).filter(function ([_value, _index, key,]) {
25
+ // omit entries without keys
26
+ return key != null;
27
+ }).map(function ([value, index, key,]) {
28
+ const valuePath = `${listPath}.${key}`;
17
29
  return (_jsx(Fragment, { children: children(valuePath, value, index) }, valuePath));
18
30
  }) }));
19
31
  }
@@ -26,6 +26,7 @@ export const Empty = {
26
26
  readonly: false,
27
27
  required: false,
28
28
  value: [],
29
+ listIndexToKey: [0],
29
30
  },
30
31
  },
31
32
  },
@@ -42,6 +43,13 @@ export const Populated = {
42
43
  'C',
43
44
  'D',
44
45
  ],
46
+ listIndexToKey: [
47
+ 100,
48
+ 1,
49
+ 33,
50
+ 5,
51
+ 101,
52
+ ],
45
53
  },
46
54
  },
47
55
  },