@strictly/react-form 0.0.11 → 0.0.13

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.
@@ -102,7 +102,7 @@ export type ValuePathsToAdaptersOf<
102
102
  }
103
103
  : never
104
104
 
105
- export abstract class FormPresenter<
105
+ export class FormModel<
106
106
  T extends Type,
107
107
  ValueToTypePaths extends Readonly<Record<string, string>>,
108
108
  TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<
@@ -114,10 +114,173 @@ export abstract class FormPresenter<
114
114
  ValueToTypePaths
115
115
  >,
116
116
  > {
117
+ @observable.ref
118
+ accessor value: MobxValueOfType<T>
119
+ @observable.shallow
120
+ accessor fieldOverrides: FlattenedFieldOverrides<ValuePathsToAdapters>
121
+ @observable.shallow
122
+ accessor errors: FlattenedErrors<ValuePathsToAdapters> = {}
123
+
124
+ private readonly flattenedTypeDefs: Readonly<Record<string, Type>>
125
+
117
126
  constructor(
118
127
  readonly type: T,
128
+ value: ValueOfType<ReadonlyTypeOfType<T>>,
119
129
  protected readonly adapters: TypePathsToAdapters,
120
130
  ) {
131
+ this.value = mobxCopy(type, value)
132
+ this.flattenedTypeDefs = flattenTypesOfType(type)
133
+ // pre-populate field overrides for consistent behavior when default information is overwritten
134
+ // then returned to
135
+ const conversions = flattenValueTo(
136
+ type,
137
+ this.value,
138
+ () => {},
139
+ (
140
+ _t: StrictTypeDef,
141
+ value: AnyValueType,
142
+ _setter,
143
+ typePath,
144
+ valuePath,
145
+ ): AnnotatedFieldConversion<FieldOverride> | undefined => {
146
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
147
+ const adapter = this.adapters[typePath as keyof TypePathsToAdapters]
148
+ if (adapter == null) {
149
+ return
150
+ }
151
+ const {
152
+ convert,
153
+ revert,
154
+ } = adapter
155
+ if (revert == null) {
156
+ // no need to store a temporary value if the value cannot be written back
157
+ return
158
+ }
159
+ return convert(value, valuePath, this.value)
160
+ },
161
+ )
162
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
163
+ this.fieldOverrides = map(conversions, function (_k, v) {
164
+ return v && [v.value]
165
+ }) as FlattenedFieldOverrides<ValuePathsToAdapters>
166
+ }
167
+
168
+ @computed
169
+ get fields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>> {
170
+ return new Proxy<SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>>>(
171
+ this.knownFields,
172
+ {
173
+ get: (target, prop) => {
174
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
175
+ const field = (target as any)[prop]
176
+ if (field != null) {
177
+ return field
178
+ }
179
+ if (typeof prop === 'string') {
180
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
181
+ return this.maybeSynthesizeFieldByValuePath(prop as keyof ValuePathsToAdapters)
182
+ }
183
+ },
184
+ },
185
+ )
186
+ }
187
+
188
+ @computed
189
+ private get knownFields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>> {
190
+ return flattenValueTo(
191
+ this.type,
192
+ this.value,
193
+ () => {},
194
+ // TODO swap these to valuePath, typePath in flatten
195
+ (_t: StrictTypeDef, _v: AnyValueType, _setter, typePath, valuePath): Field | undefined => {
196
+ return this.synthesizeFieldByPaths(
197
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
198
+ valuePath as keyof ValuePathsToAdapters,
199
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
200
+ typePath as keyof TypePathsToAdapters,
201
+ )
202
+ },
203
+ )
204
+ }
205
+
206
+ private maybeSynthesizeFieldByValuePath(valuePath: keyof ValuePathsToAdapters): Field | undefined {
207
+ let typePath: keyof TypePathsToAdapters
208
+ try {
209
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
210
+ typePath = valuePathToTypePath<ValueToTypePaths, keyof ValueToTypePaths>(
211
+ this.type,
212
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
213
+ valuePath as keyof ValueToTypePaths,
214
+ true,
215
+ ) as keyof TypePathsToAdapters
216
+ } catch (e) {
217
+ // TODO make jsonValuePathToTypePath return null in the event of an invalid
218
+ // value path instead of throwing an exception
219
+ // assume that the path was invalid
220
+ return
221
+ }
222
+ return this.synthesizeFieldByPaths(valuePath, typePath)
223
+ }
224
+
225
+ private synthesizeFieldByPaths(valuePath: keyof ValuePathsToAdapters, typePath: keyof TypePathsToAdapters) {
226
+ const adapter = this.adapters[typePath]
227
+ if (adapter == null) {
228
+ // invalid path, which can happen
229
+ return
230
+ }
231
+ const {
232
+ convert,
233
+ create,
234
+ } = adapter
235
+
236
+ const fieldOverride = this.fieldOverrides[valuePath]
237
+ const accessor = this.getAccessorForValuePath(valuePath)
238
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
239
+ const fieldTypeDef = this.flattenedTypeDefs[typePath as string]
240
+ const {
241
+ value,
242
+ required,
243
+ readonly,
244
+ } = convert(
245
+ accessor != null
246
+ ? accessor.value
247
+ : fieldTypeDef != null
248
+ ? mobxCopy(
249
+ fieldTypeDef,
250
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
251
+ create(valuePath as string, this.value),
252
+ )
253
+ // fake values can't be copied
254
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
255
+ : create(valuePath as string, this.value),
256
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
257
+ valuePath as string,
258
+ this.value,
259
+ )
260
+ const error = this.errors[valuePath]
261
+ return {
262
+ value: fieldOverride != null ? fieldOverride[0] : value,
263
+ error,
264
+ readonly,
265
+ required,
266
+ }
267
+ }
268
+
269
+ getAccessorForValuePath(valuePath: keyof ValuePathsToAdapters): Accessor | undefined {
270
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
271
+ return this.accessors[valuePath as string]
272
+ }
273
+
274
+ @computed
275
+ // should only be referenced internally, so loosely typed
276
+ get accessors(): Readonly<Record<string, Accessor>> {
277
+ return flattenAccessorsOfType<T, Readonly<Record<string, Accessor>>>(
278
+ this.type,
279
+ this.value,
280
+ (value: ValueOfType<T>): void => {
281
+ this.value = mobxCopy(this.type, value)
282
+ },
283
+ )
121
284
  }
122
285
 
123
286
  private maybeGetAdapterForValuePath(valuePath: keyof ValuePathsToAdapters) {
@@ -140,23 +303,20 @@ export abstract class FormPresenter<
140
303
  }
141
304
 
142
305
  setFieldValueAndValidate<K extends keyof ValuePathsToAdapters>(
143
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
144
306
  valuePath: K,
145
307
  value: ToOfFieldAdapter<ValuePathsToAdapters[K]>,
146
308
  ): boolean {
147
- return this.internalSetFieldValue(model, valuePath, value, true)
309
+ return this.internalSetFieldValue(valuePath, value, true)
148
310
  }
149
311
 
150
312
  setFieldValue<K extends keyof ValuePathsToAdapters>(
151
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
152
313
  valuePath: K,
153
314
  value: ToOfFieldAdapter<ValuePathsToAdapters[K]>,
154
315
  ): boolean {
155
- return this.internalSetFieldValue(model, valuePath, value, false)
316
+ return this.internalSetFieldValue(valuePath, value, false)
156
317
  }
157
318
 
158
319
  addListItem<K extends keyof FlattenedListTypesOfType<T>>(
159
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
160
320
  valuePath: K,
161
321
  // TODO can this type be simplified?
162
322
  elementValue: Maybe<ElementOfArray<FlattenedValuesOfType<T>[K]>> = null,
@@ -164,7 +324,7 @@ export abstract class FormPresenter<
164
324
  ) {
165
325
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
166
326
  const listValuePath = valuePath as string
167
- const accessor = model.accessors[valuePath]
327
+ const accessor = this.accessors[valuePath]
168
328
  const listTypePath = this.typePath(valuePath)
169
329
  const definedIndex = index ?? accessor.value.length
170
330
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -181,7 +341,7 @@ export abstract class FormPresenter<
181
341
  : elementAdapter.create(
182
342
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
183
343
  elementTypePath as string,
184
- model.value,
344
+ this.value,
185
345
  )
186
346
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
187
347
  const originalList: any[] = accessor.value
@@ -192,7 +352,7 @@ export abstract class FormPresenter<
192
352
  ]
193
353
  // shuffle the overrides around to account for new indices
194
354
  // to so this we need to sort the array indices in descending order
195
- const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
355
+ const targetPaths = Object.keys(this.fieldOverrides).filter(function (v) {
196
356
  return v.startsWith(`${listValuePath}.`)
197
357
  }).map(function (v) {
198
358
  const parts = v.substring(listValuePath.length + 1).split('.')
@@ -207,11 +367,11 @@ export abstract class FormPresenter<
207
367
  // descending
208
368
  return b - a
209
369
  })
210
- runInAction(function () {
211
- targetPaths.forEach(function ([
370
+ runInAction(() => {
371
+ targetPaths.forEach(([
212
372
  index,
213
373
  postfix,
214
- ]) {
374
+ ]) => {
215
375
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
216
376
  const fromJsonPath = [
217
377
  listValuePath,
@@ -224,99 +384,99 @@ export abstract class FormPresenter<
224
384
  `${index + 1}`,
225
385
  ...postfix,
226
386
  ].join('.') as keyof ValuePathsToAdapters
227
- const fieldOverride = model.fieldOverrides[fromJsonPath]
228
- delete model.fieldOverrides[fromJsonPath]
229
- model.fieldOverrides[toJsonPath] = fieldOverride
230
- const error = model.errors[fromJsonPath]
231
- delete model.errors[fromJsonPath]
232
- model.errors[toJsonPath] = error
387
+ const fieldOverride = this.fieldOverrides[fromJsonPath]
388
+ delete this.fieldOverrides[fromJsonPath]
389
+ this.fieldOverrides[toJsonPath] = fieldOverride
390
+ const error = this.errors[fromJsonPath]
391
+ delete this.errors[fromJsonPath]
392
+ this.errors[toJsonPath] = error
233
393
  })
234
394
  accessor.set(newList)
235
395
  // delete any value overrides so the new list isn't shadowed
236
396
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
237
- delete model.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
397
+ delete this.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
238
398
  })
239
399
  }
240
400
 
241
- removeListItem<K extends keyof FlattenedListTypesOfType<T>>(
242
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
243
- elementValuePath: `${K}.${number}`,
244
- ) {
245
- const [
246
- listValuePath,
247
- elementIndexString,
248
- ] = assertExistsAndReturn(
249
- jsonPathPop(elementValuePath),
250
- 'expected a path with two or more segments {}',
251
- elementValuePath,
252
- )
253
- const accessor = model.accessors[listValuePath]
254
- const elementIndex = checkValidNumber(
255
- parseInt(elementIndexString),
256
- 'unexpected index {} ({})',
257
- elementIndexString,
258
- elementValuePath,
259
- )
260
- const newList = [...accessor.value]
261
- assertState(
262
- elementIndex >= 0 && elementIndex < newList.length,
263
- 'invalid index from path {} ({})',
264
- elementIndex,
265
- elementValuePath,
266
- )
267
- newList.splice(elementIndex, 1)
268
-
269
- // shuffle the overrides around to account for new indices
270
- // to so this we need to sort the array indices in descending order
271
- const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
272
- return v.startsWith(`${listValuePath}.`)
273
- }).map(function (v) {
274
- const parts = v.substring(listValuePath.length + 1).split('.')
275
- const index = parseInt(parts[0])
276
- return [
277
- index,
278
- parts.slice(1),
279
- ] as const
280
- }).filter(function ([index]) {
281
- return index > elementIndex
282
- }).sort(function ([a], [b]) {
283
- // ascending
284
- return a - b
285
- })
286
-
287
- runInAction(function () {
288
- targetPaths.forEach(function ([
289
- index,
290
- postfix,
291
- ]) {
292
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
293
- const fromJsonPath = [
401
+ removeListItem<K extends keyof FlattenedListTypesOfType<T>>(...elementValuePaths: readonly `${K}.${number}`[]) {
402
+ // sort and reverse so we delete last to first so indices of sequential deletions are preserved
403
+ const orderedElementValuePaths = elementValuePaths.toSorted().reverse()
404
+ runInAction(() => {
405
+ orderedElementValuePaths.forEach(elementValuePath => {
406
+ const [
294
407
  listValuePath,
295
- `${index}`,
296
- ...postfix,
297
- ].join('.') as keyof ValuePathsToAdapters
408
+ elementIndexString,
409
+ ] = assertExistsAndReturn(
410
+ jsonPathPop(elementValuePath),
411
+ 'expected a path with two or more segments {}',
412
+ elementValuePath,
413
+ )
414
+ const accessor = this.accessors[listValuePath]
415
+ const elementIndex = checkValidNumber(
416
+ parseInt(elementIndexString),
417
+ 'unexpected index {} ({})',
418
+ elementIndexString,
419
+ elementValuePath,
420
+ )
421
+ const newList = [...accessor.value]
422
+ assertState(
423
+ elementIndex >= 0 && elementIndex < newList.length,
424
+ 'invalid index from path {} ({})',
425
+ elementIndex,
426
+ elementValuePath,
427
+ )
428
+ newList.splice(elementIndex, 1)
429
+
430
+ // shuffle the overrides around to account for new indices
431
+ // to so this we need to sort the array indices in descending order
432
+ const targetPaths = Object.keys(this.fieldOverrides).filter(function (v) {
433
+ return v.startsWith(`${listValuePath}.`)
434
+ }).map(function (v) {
435
+ const parts = v.substring(listValuePath.length + 1).split('.')
436
+ const index = parseInt(parts[0])
437
+ return [
438
+ index,
439
+ parts.slice(1),
440
+ ] as const
441
+ }).filter(function ([index]) {
442
+ return index > elementIndex
443
+ }).sort(function ([a], [b]) {
444
+ // descending
445
+ return a - b
446
+ })
447
+
448
+ targetPaths.forEach(([
449
+ index,
450
+ postfix,
451
+ ]) => {
452
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
453
+ const fromJsonPath = [
454
+ listValuePath,
455
+ `${index}`,
456
+ ...postfix,
457
+ ].join('.') as keyof ValuePathsToAdapters
458
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
459
+ const toJsonPath = [
460
+ listValuePath,
461
+ `${index - 1}`,
462
+ ...postfix,
463
+ ].join('.') as keyof ValuePathsToAdapters
464
+ const fieldOverride = this.fieldOverrides[fromJsonPath]
465
+ delete this.fieldOverrides[fromJsonPath]
466
+ this.fieldOverrides[toJsonPath] = fieldOverride
467
+ const error = this.errors[fromJsonPath]
468
+ delete this.errors[fromJsonPath]
469
+ this.errors[toJsonPath] = error
470
+ })
471
+ accessor.set(newList)
472
+ // delete any value overrides so the new list isn't shadowed
298
473
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
299
- const toJsonPath = [
300
- listValuePath,
301
- `${index - 1}`,
302
- ...postfix,
303
- ].join('.') as keyof ValuePathsToAdapters
304
- const fieldOverride = model.fieldOverrides[fromJsonPath]
305
- delete model.fieldOverrides[fromJsonPath]
306
- model.fieldOverrides[toJsonPath] = fieldOverride
307
- const error = model.errors[fromJsonPath]
308
- delete model.errors[fromJsonPath]
309
- model.errors[toJsonPath] = error
474
+ delete this.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
310
475
  })
311
- accessor.set(newList)
312
- // delete any value overrides so the new list isn't shadowed
313
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
314
- delete model.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
315
476
  })
316
477
  }
317
478
 
318
479
  private internalSetFieldValue<K extends keyof ValuePathsToAdapters>(
319
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
320
480
  valuePath: K,
321
481
  value: ToOfFieldAdapter<ValuePathsToAdapters[K]>,
322
482
  displayValidation: boolean,
@@ -326,21 +486,21 @@ export abstract class FormPresenter<
326
486
  assertExists(revert, 'setting value not supported {}', valuePath)
327
487
 
328
488
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
329
- const conversion = revert(value, valuePath as any, model.value)
330
- const accessor = model.getAccessorForValuePath(valuePath)
489
+ const conversion = revert(value, valuePath as any, this.value)
490
+ const accessor = this.getAccessorForValuePath(valuePath)
331
491
  return runInAction(() => {
332
- model.fieldOverrides[valuePath] = [value]
492
+ this.fieldOverrides[valuePath] = [value]
333
493
  switch (conversion.type) {
334
494
  case UnreliableFieldConversionType.Failure:
335
495
  if (displayValidation) {
336
- model.errors[valuePath] = conversion.error
496
+ this.errors[valuePath] = conversion.error
337
497
  }
338
498
  if (conversion.value != null && accessor != null) {
339
499
  accessor.set(conversion.value[0])
340
500
  }
341
501
  return false
342
502
  case UnreliableFieldConversionType.Success:
343
- delete model.errors[valuePath]
503
+ delete this.errors[valuePath]
344
504
  accessor?.set(conversion.value)
345
505
  return true
346
506
  default:
@@ -349,22 +509,16 @@ export abstract class FormPresenter<
349
509
  })
350
510
  }
351
511
 
352
- clearFieldError<K extends keyof ValuePathsToAdapters>(
353
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
354
- valuePath: K,
355
- ) {
356
- const fieldOverride = model.fieldOverrides[valuePath]
512
+ clearFieldError<K extends keyof ValuePathsToAdapters>(valuePath: K) {
513
+ const fieldOverride = this.fieldOverrides[valuePath]
357
514
  if (fieldOverride != null) {
358
- runInAction(function () {
359
- delete model.errors[valuePath]
515
+ runInAction(() => {
516
+ delete this.errors[valuePath]
360
517
  })
361
518
  }
362
519
  }
363
520
 
364
- clearFieldValue<K extends StringKeyOf<ValueToTypePaths>>(
365
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
366
- valuePath: K,
367
- ) {
521
+ clearFieldValue<K extends StringKeyOf<ValueToTypePaths>>(valuePath: K) {
368
522
  const typePath = this.typePath(valuePath)
369
523
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
370
524
  const adapter = this.adapters[typePath as keyof TypePathsToAdapters]
@@ -375,41 +529,34 @@ export abstract class FormPresenter<
375
529
  convert,
376
530
  create,
377
531
  } = adapter
378
- const value = create(valuePath, model.value)
532
+ const value = create(valuePath, this.value)
379
533
  const {
380
534
  value: displayValue,
381
- } = convert(value, valuePath, model.value)
535
+ } = convert(value, valuePath, this.value)
382
536
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
383
537
  const key = valuePath as unknown as keyof ValuePathsToAdapters
384
- runInAction(function () {
385
- model.fieldOverrides[key] = [displayValue]
538
+ runInAction(() => {
539
+ this.fieldOverrides[key] = [displayValue]
386
540
  })
387
541
  }
388
542
 
389
- clearAll(
390
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
391
- value: ValueOfType<T>,
392
- ): void {
543
+ clearAll(value: ValueOfType<T>): void {
393
544
  runInAction(() => {
394
- model.errors = {}
545
+ this.errors = {}
395
546
  // TODO this isn't correct, should reload from value
396
- model.fieldOverrides = {}
397
- model.value = mobxCopy(this.type, value)
547
+ this.fieldOverrides = {}
548
+ this.value = mobxCopy(this.type, value)
398
549
  })
399
550
  }
400
551
 
401
- isValuePathActive<K extends keyof ValuePathsToAdapters>(
402
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
403
- valuePath: K,
404
- ): boolean {
405
- const values = flattenValuesOfType(this.type, model.value)
552
+ isValuePathActive<K extends keyof ValuePathsToAdapters>(valuePath: K): boolean {
553
+ const values = flattenValuesOfType(this.type, this.value)
406
554
  const keys = new Set(Object.keys(values))
407
555
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
408
556
  return keys.has(valuePath as string)
409
557
  }
410
558
 
411
559
  validateField<K extends keyof ValuePathsToAdapters>(
412
- model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
413
560
  valuePath: K,
414
561
  ignoreDefaultValue = false,
415
562
  ): boolean {
@@ -418,18 +565,18 @@ export abstract class FormPresenter<
418
565
  revert,
419
566
  create,
420
567
  } = this.getAdapterForValuePath(valuePath)
421
- const fieldOverride = model.fieldOverrides[valuePath]
422
- const accessor = model.getAccessorForValuePath(valuePath)
568
+ const fieldOverride = this.fieldOverrides[valuePath]
569
+ const accessor = this.getAccessorForValuePath(valuePath)
423
570
  const {
424
571
  value: storedValue,
425
572
  } = convert(
426
573
  accessor != null
427
574
  ? accessor.value
428
575
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
429
- : create(valuePath as string, model.value),
576
+ : create(valuePath as string, this.value),
430
577
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
431
578
  valuePath as string,
432
- model.value,
579
+ this.value,
433
580
  )
434
581
  const value = fieldOverride != null
435
582
  ? fieldOverride[0]
@@ -439,23 +586,23 @@ export abstract class FormPresenter<
439
586
  if (ignoreDefaultValue) {
440
587
  const {
441
588
  value: defaultDisplayValue,
442
- } = convert(create(valuePath, model.value), valuePath, model.value)
589
+ } = convert(create(valuePath, this.value), valuePath, this.value)
443
590
  if (defaultDisplayValue === value) {
444
591
  return true
445
592
  }
446
593
  }
447
594
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
448
- const conversion = revert(value, valuePath as string, model.value)
449
- return runInAction(function () {
595
+ const conversion = revert(value, valuePath as string, this.value)
596
+ return runInAction(() => {
450
597
  switch (conversion.type) {
451
598
  case UnreliableFieldConversionType.Failure:
452
- model.errors[valuePath] = conversion.error
599
+ this.errors[valuePath] = conversion.error
453
600
  if (conversion.value != null && accessor != null && dirty) {
454
601
  accessor.set(conversion.value[0])
455
602
  }
456
603
  return false
457
604
  case UnreliableFieldConversionType.Success:
458
- delete model.errors[valuePath]
605
+ delete this.errors[valuePath]
459
606
  if (accessor != null && dirty) {
460
607
  accessor.set(conversion.value)
461
608
  }
@@ -466,9 +613,9 @@ export abstract class FormPresenter<
466
613
  })
467
614
  }
468
615
 
469
- validateAll(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>): boolean {
616
+ validateAll(): boolean {
470
617
  // sort keys shortest to longest so parent changes don't overwrite child changes
471
- const accessors = toArray(model.accessors).toSorted(function ([a], [b]) {
618
+ const accessors = toArray(this.accessors).toSorted(function ([a], [b]) {
472
619
  return a.length - b.length
473
620
  })
474
621
  return runInAction(() => {
@@ -495,20 +642,20 @@ export abstract class FormPresenter<
495
642
  // no convert method means this field is immutable
496
643
  return success
497
644
  }
498
- const fieldOverride = model.fieldOverrides[adapterPath]
645
+ const fieldOverride = this.fieldOverrides[adapterPath]
499
646
  const {
500
647
  value: storedValue,
501
- } = convert(accessor.value, valuePath, model.value)
648
+ } = convert(accessor.value, valuePath, this.value)
502
649
  const value = fieldOverride != null
503
650
  ? fieldOverride[0]
504
651
  : storedValue
505
652
  // TODO more nuanced comparison
506
653
  const dirty = fieldOverride != null && fieldOverride[0] !== storedValue
507
654
 
508
- const conversion = revert(value, valuePath, model.value)
655
+ const conversion = revert(value, valuePath, this.value)
509
656
  switch (conversion.type) {
510
657
  case UnreliableFieldConversionType.Failure:
511
- model.errors[adapterPath] = conversion.error
658
+ this.errors[adapterPath] = conversion.error
512
659
  if (conversion.value != null && dirty) {
513
660
  accessor.set(conversion.value[0])
514
661
  }
@@ -517,7 +664,7 @@ export abstract class FormPresenter<
517
664
  if (dirty) {
518
665
  accessor.set(conversion.value)
519
666
  }
520
- delete model.errors[adapterPath]
667
+ delete this.errors[adapterPath]
521
668
  return success
522
669
  default:
523
670
  throw new UnreachableError(conversion)
@@ -527,193 +674,4 @@ export abstract class FormPresenter<
527
674
  )
528
675
  })
529
676
  }
530
-
531
- abstract createModel(value: ValueOfType<ReadonlyTypeOfType<T>>): FormModel<
532
- T,
533
- ValueToTypePaths,
534
- TypePathsToAdapters,
535
- ValuePathsToAdapters
536
- >
537
- }
538
-
539
- export class FormModel<
540
- T extends Type,
541
- ValueToTypePaths extends Readonly<Record<string, string>>,
542
- TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<
543
- FlattenedValuesOfType<T, '*'>,
544
- ValueOfType<ReadonlyTypeOfType<T>>
545
- >,
546
- ValuePathsToAdapters extends ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths> = ValuePathsToAdaptersOf<
547
- TypePathsToAdapters,
548
- ValueToTypePaths
549
- >,
550
- > {
551
- @observable.ref
552
- accessor value: MobxValueOfType<T>
553
- @observable.shallow
554
- accessor fieldOverrides: FlattenedFieldOverrides<ValuePathsToAdapters>
555
- @observable.shallow
556
- accessor errors: FlattenedErrors<ValuePathsToAdapters> = {}
557
-
558
- private readonly flattenedTypeDefs: Readonly<Record<string, Type>>
559
-
560
- constructor(
561
- private readonly type: T,
562
- value: ValueOfType<ReadonlyTypeOfType<T>>,
563
- private readonly adapters: TypePathsToAdapters,
564
- ) {
565
- this.value = mobxCopy(type, value)
566
- this.flattenedTypeDefs = flattenTypesOfType(type)
567
- // pre-populate field overrides for consistent behavior when default information is overwritten
568
- // then returned to
569
- const conversions = flattenValueTo(
570
- type,
571
- this.value,
572
- () => {},
573
- (
574
- _t: StrictTypeDef,
575
- value: AnyValueType,
576
- _setter,
577
- typePath,
578
- valuePath,
579
- ): AnnotatedFieldConversion<FieldOverride> | undefined => {
580
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
581
- const adapter = this.adapters[typePath as keyof TypePathsToAdapters]
582
- if (adapter == null) {
583
- return
584
- }
585
- const {
586
- convert,
587
- revert,
588
- } = adapter
589
- if (revert == null) {
590
- // no need to store a temporary value if the value cannot be written back
591
- return
592
- }
593
- return convert(value, valuePath, this.value)
594
- },
595
- )
596
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
597
- this.fieldOverrides = map(conversions, function (_k, v) {
598
- return v && [v.value]
599
- }) as FlattenedFieldOverrides<ValuePathsToAdapters>
600
- }
601
-
602
- @computed
603
- get fields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>> {
604
- return new Proxy<SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>>>(
605
- this.knownFields,
606
- {
607
- get: (target, prop) => {
608
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
609
- const field = (target as any)[prop]
610
- if (field != null) {
611
- return field
612
- }
613
- if (typeof prop === 'string') {
614
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
615
- return this.maybeSynthesizeFieldByValuePath(prop as keyof ValuePathsToAdapters)
616
- }
617
- },
618
- },
619
- )
620
- }
621
-
622
- @computed
623
- private get knownFields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>> {
624
- return flattenValueTo(
625
- this.type,
626
- this.value,
627
- () => {},
628
- // TODO swap these to valuePath, typePath in flatten
629
- (_t: StrictTypeDef, _v: AnyValueType, _setter, typePath, valuePath): Field | undefined => {
630
- return this.synthesizeFieldByPaths(
631
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
632
- valuePath as keyof ValuePathsToAdapters,
633
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
634
- typePath as keyof TypePathsToAdapters,
635
- )
636
- },
637
- )
638
- }
639
-
640
- private maybeSynthesizeFieldByValuePath(valuePath: keyof ValuePathsToAdapters): Field | undefined {
641
- let typePath: keyof TypePathsToAdapters
642
- try {
643
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
644
- typePath = valuePathToTypePath<ValueToTypePaths, keyof ValueToTypePaths>(
645
- this.type,
646
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
647
- valuePath as keyof ValueToTypePaths,
648
- true,
649
- ) as keyof TypePathsToAdapters
650
- } catch (e) {
651
- // TODO make jsonValuePathToTypePath return null in the event of an invalid
652
- // value path instead of throwing an exception
653
- // assume that the path was invalid
654
- return
655
- }
656
- return this.synthesizeFieldByPaths(valuePath, typePath)
657
- }
658
-
659
- private synthesizeFieldByPaths(valuePath: keyof ValuePathsToAdapters, typePath: keyof TypePathsToAdapters) {
660
- const adapter = this.adapters[typePath]
661
- if (adapter == null) {
662
- // invalid path, which can happen
663
- return
664
- }
665
- const {
666
- convert,
667
- create,
668
- } = adapter
669
-
670
- const fieldOverride = this.fieldOverrides[valuePath]
671
- const accessor = this.getAccessorForValuePath(valuePath)
672
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
673
- const fieldTypeDef = this.flattenedTypeDefs[typePath as string]
674
- const {
675
- value,
676
- required,
677
- readonly,
678
- } = convert(
679
- accessor != null
680
- ? accessor.value
681
- : fieldTypeDef != null
682
- ? mobxCopy(
683
- fieldTypeDef,
684
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
685
- create(valuePath as string, this.value),
686
- )
687
- // fake values can't be copied
688
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
689
- : create(valuePath as string, this.value),
690
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
691
- valuePath as string,
692
- this.value,
693
- )
694
- const error = this.errors[valuePath]
695
- return {
696
- value: fieldOverride != null ? fieldOverride[0] : value,
697
- error,
698
- readonly,
699
- required,
700
- }
701
- }
702
-
703
- getAccessorForValuePath(valuePath: keyof ValuePathsToAdapters): Accessor | undefined {
704
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
705
- return this.accessors[valuePath as string]
706
- }
707
-
708
- @computed
709
- // should only be referenced internally, so loosely typed
710
- get accessors(): Readonly<Record<string, Accessor>> {
711
- return flattenAccessorsOfType<T, Readonly<Record<string, Accessor>>>(
712
- this.type,
713
- this.value,
714
- (value: ValueOfType<T>): void => {
715
- this.value = mobxCopy(this.type, value)
716
- },
717
- )
718
- }
719
677
  }