@strictly/react-form 0.0.1

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 (239) hide show
  1. package/.eslintrc.cjs +26 -0
  2. package/.out/.storybook/main.d.ts +3 -0
  3. package/.out/.storybook/main.js +32 -0
  4. package/.out/.storybook/preview.d.ts +4 -0
  5. package/.out/.storybook/preview.js +20 -0
  6. package/.out/.vitest/install_deterministic_random.d.ts +2 -0
  7. package/.out/.vitest/install_deterministic_random.js +15 -0
  8. package/.out/.vitest/install_storybook_preview.d.ts +1 -0
  9. package/.out/.vitest/install_storybook_preview.js +7 -0
  10. package/.out/.vitest/match_media.d.ts +1 -0
  11. package/.out/.vitest/match_media.js +5 -0
  12. package/.out/.vitest/resize_observer.d.ts +1 -0
  13. package/.out/.vitest/resize_observer.js +4 -0
  14. package/.out/core/mobx/field_adapter.d.ts +9 -0
  15. package/.out/core/mobx/field_adapter.js +1 -0
  16. package/.out/core/mobx/field_adapter_builder.d.ts +22 -0
  17. package/.out/core/mobx/field_adapter_builder.js +56 -0
  18. package/.out/core/mobx/flattened_adapters_of_fields.d.ts +9 -0
  19. package/.out/core/mobx/flattened_adapters_of_fields.js +1 -0
  20. package/.out/core/mobx/flattened_list_type_defs_of.d.ts +8 -0
  21. package/.out/core/mobx/flattened_list_type_defs_of.js +1 -0
  22. package/.out/core/mobx/form_presenter.d.ts +61 -0
  23. package/.out/core/mobx/form_presenter.js +425 -0
  24. package/.out/core/mobx/specs/flattened_adapters_of_fields.tests.d.ts +1 -0
  25. package/.out/core/mobx/specs/flattened_adapters_of_fields.tests.js +13 -0
  26. package/.out/core/mobx/specs/flattened_list_type_defs_of.tests.d.ts +1 -0
  27. package/.out/core/mobx/specs/flattened_list_type_defs_of.tests.js +16 -0
  28. package/.out/core/mobx/specs/form_presenter.tests.d.ts +1 -0
  29. package/.out/core/mobx/specs/form_presenter.tests.js +697 -0
  30. package/.out/core/mobx/types.d.ts +19 -0
  31. package/.out/core/mobx/types.js +1 -0
  32. package/.out/core/props.d.ts +12 -0
  33. package/.out/core/props.js +1 -0
  34. package/.out/field_converters/chain_field_converter.d.ts +3 -0
  35. package/.out/field_converters/chain_field_converter.js +46 -0
  36. package/.out/field_converters/identity_converter.d.ts +3 -0
  37. package/.out/field_converters/identity_converter.js +14 -0
  38. package/.out/field_converters/integer_to_string_converter.d.ts +7 -0
  39. package/.out/field_converters/integer_to_string_converter.js +26 -0
  40. package/.out/field_converters/list_converter.d.ts +2 -0
  41. package/.out/field_converters/list_converter.js +8 -0
  42. package/.out/field_converters/maybe_identity_converter.d.ts +8 -0
  43. package/.out/field_converters/maybe_identity_converter.js +15 -0
  44. package/.out/field_converters/nullable_to_boolean_converter.d.ts +11 -0
  45. package/.out/field_converters/nullable_to_boolean_converter.js +31 -0
  46. package/.out/field_converters/select_value_type_converter.d.ts +23 -0
  47. package/.out/field_converters/select_value_type_converter.js +60 -0
  48. package/.out/field_converters/trimming_string_converter.d.ts +6 -0
  49. package/.out/field_converters/trimming_string_converter.js +14 -0
  50. package/.out/field_converters/validating_converter.d.ts +3 -0
  51. package/.out/field_converters/validating_converter.js +21 -0
  52. package/.out/field_validators/minimum_string_length_field_validator.d.ts +2 -0
  53. package/.out/field_validators/minimum_string_length_field_validator.js +8 -0
  54. package/.out/field_value_factories/prototyping_field_value_factory.d.ts +2 -0
  55. package/.out/field_value_factories/prototyping_field_value_factory.js +5 -0
  56. package/.out/index.d.ts +16 -0
  57. package/.out/index.js +16 -0
  58. package/.out/mantine/create_checkbox.d.ts +9 -0
  59. package/.out/mantine/create_checkbox.js +37 -0
  60. package/.out/mantine/create_list.d.ts +15 -0
  61. package/.out/mantine/create_list.js +16 -0
  62. package/.out/mantine/create_pill.d.ts +7 -0
  63. package/.out/mantine/create_pill.js +15 -0
  64. package/.out/mantine/create_radio.d.ts +8 -0
  65. package/.out/mantine/create_radio.js +10 -0
  66. package/.out/mantine/create_radio_group.d.ts +9 -0
  67. package/.out/mantine/create_radio_group.js +34 -0
  68. package/.out/mantine/create_text_input.d.ts +19 -0
  69. package/.out/mantine/create_text_input.js +38 -0
  70. package/.out/mantine/create_value_input.d.ts +17 -0
  71. package/.out/mantine/create_value_input.js +38 -0
  72. package/.out/mantine/hooks.d.ts +56 -0
  73. package/.out/mantine/hooks.js +135 -0
  74. package/.out/mantine/specs/checkbox_constants.d.ts +1 -0
  75. package/.out/mantine/specs/checkbox_constants.js +1 -0
  76. package/.out/mantine/specs/checkbox_hooks.stories.d.ts +13 -0
  77. package/.out/mantine/specs/checkbox_hooks.stories.js +63 -0
  78. package/.out/mantine/specs/checkbox_hooks.tests.d.ts +1 -0
  79. package/.out/mantine/specs/checkbox_hooks.tests.js +74 -0
  80. package/.out/mantine/specs/list_hooks.stories.d.ts +11 -0
  81. package/.out/mantine/specs/list_hooks.stories.js +48 -0
  82. package/.out/mantine/specs/list_hooks.tests.d.ts +1 -0
  83. package/.out/mantine/specs/list_hooks.tests.js +12 -0
  84. package/.out/mantine/specs/radio_group_constants.d.ts +4 -0
  85. package/.out/mantine/specs/radio_group_constants.js +11 -0
  86. package/.out/mantine/specs/radio_group_hooks.stories.d.ts +14 -0
  87. package/.out/mantine/specs/radio_group_hooks.stories.js +68 -0
  88. package/.out/mantine/specs/radio_group_hooks.tests.d.ts +1 -0
  89. package/.out/mantine/specs/radio_group_hooks.tests.js +62 -0
  90. package/.out/mantine/specs/select_hooks.stories.d.ts +12 -0
  91. package/.out/mantine/specs/select_hooks.stories.js +57 -0
  92. package/.out/mantine/specs/select_hooks.tests.d.ts +1 -0
  93. package/.out/mantine/specs/select_hooks.tests.js +12 -0
  94. package/.out/mantine/specs/select_hooks_constant.d.ts +1 -0
  95. package/.out/mantine/specs/select_hooks_constant.js +1 -0
  96. package/.out/mantine/specs/text_input_constants.d.ts +1 -0
  97. package/.out/mantine/specs/text_input_constants.js +1 -0
  98. package/.out/mantine/specs/text_input_hooks.stories.d.ts +21 -0
  99. package/.out/mantine/specs/text_input_hooks.stories.js +88 -0
  100. package/.out/mantine/specs/text_input_hooks.tests.d.ts +1 -0
  101. package/.out/mantine/specs/text_input_hooks.tests.js +79 -0
  102. package/.out/mantine/specs/value_input_constants.d.ts +2 -0
  103. package/.out/mantine/specs/value_input_constants.js +2 -0
  104. package/.out/mantine/specs/value_input_hooks.stories.d.ts +23 -0
  105. package/.out/mantine/specs/value_input_hooks.stories.js +124 -0
  106. package/.out/mantine/specs/value_input_hooks.tests.d.ts +1 -0
  107. package/.out/mantine/specs/value_input_hooks.tests.js +12 -0
  108. package/.out/mantine/types.d.ts +11 -0
  109. package/.out/mantine/types.js +1 -0
  110. package/.out/tsconfig.json +27 -0
  111. package/.out/tsconfig.tsbuildinfo +1 -0
  112. package/.out/tsup.config.d.ts +3 -0
  113. package/.out/tsup.config.js +12 -0
  114. package/.out/types/all_fields_of_fields.d.ts +5 -0
  115. package/.out/types/all_fields_of_fields.js +1 -0
  116. package/.out/types/boolean_fields_of_fields.d.ts +5 -0
  117. package/.out/types/boolean_fields_of_fields.js +1 -0
  118. package/.out/types/error_type_of_field.d.ts +2 -0
  119. package/.out/types/error_type_of_field.js +1 -0
  120. package/.out/types/field.d.ts +7 -0
  121. package/.out/types/field.js +1 -0
  122. package/.out/types/field_converters.d.ts +29 -0
  123. package/.out/types/field_converters.js +5 -0
  124. package/.out/types/field_validator.d.ts +3 -0
  125. package/.out/types/field_validator.js +1 -0
  126. package/.out/types/flattened_form_fields_of.d.ts +9 -0
  127. package/.out/types/flattened_form_fields_of.js +1 -0
  128. package/.out/types/list_fields_of_fields.d.ts +5 -0
  129. package/.out/types/list_fields_of_fields.js +1 -0
  130. package/.out/types/specs/boolean_fields_of_fields.tests.d.ts +1 -0
  131. package/.out/types/specs/boolean_fields_of_fields.tests.js +11 -0
  132. package/.out/types/specs/error_type_of_field.tests.d.ts +1 -0
  133. package/.out/types/specs/error_type_of_field.tests.js +7 -0
  134. package/.out/types/specs/flattened_form_fields_of.tests.d.ts +1 -0
  135. package/.out/types/specs/flattened_form_fields_of.tests.js +13 -0
  136. package/.out/types/specs/string_fields_of_fields.tests.d.ts +1 -0
  137. package/.out/types/specs/string_fields_of_fields.tests.js +19 -0
  138. package/.out/types/specs/value_type_of_field.tests.d.ts +1 -0
  139. package/.out/types/specs/value_type_of_field.tests.js +7 -0
  140. package/.out/types/string_fields_of_fields.d.ts +5 -0
  141. package/.out/types/string_fields_of_fields.js +1 -0
  142. package/.out/types/value_type_of_field.d.ts +2 -0
  143. package/.out/types/value_type_of_field.js +1 -0
  144. package/.out/util/partial.d.ts +11 -0
  145. package/.out/util/partial.js +74 -0
  146. package/.out/vitest.workspace.d.ts +2 -0
  147. package/.out/vitest.workspace.js +22 -0
  148. package/.storybook/main.ts +40 -0
  149. package/.storybook/preview.tsx +28 -0
  150. package/.storybook/vite.config.mts +38 -0
  151. package/.turbo/turbo-build.log +18 -0
  152. package/.turbo/turbo-check-types.log +3 -0
  153. package/.turbo/turbo-release$colon$exports.log +3 -0
  154. package/.vitest/install_deterministic_random.ts +17 -0
  155. package/.vitest/install_storybook_preview.ts +9 -0
  156. package/.vitest/match_media.ts +7 -0
  157. package/.vitest/resize_observer.ts +5 -0
  158. package/README.md +2 -0
  159. package/core/mobx/field_adapter.ts +32 -0
  160. package/core/mobx/field_adapter_builder.ts +313 -0
  161. package/core/mobx/flattened_adapters_of_fields.ts +35 -0
  162. package/core/mobx/flattened_list_type_defs_of.ts +17 -0
  163. package/core/mobx/form_presenter.ts +705 -0
  164. package/core/mobx/specs/flattened_adapters_of_fields.tests.ts +72 -0
  165. package/core/mobx/specs/flattened_list_type_defs_of.tests.ts +35 -0
  166. package/core/mobx/specs/form_presenter.tests.ts +989 -0
  167. package/core/mobx/types.ts +54 -0
  168. package/core/props.ts +21 -0
  169. package/dist/index.cjs +11479 -0
  170. package/dist/index.d.cts +345 -0
  171. package/dist/index.d.ts +345 -0
  172. package/dist/index.js +11486 -0
  173. package/field_converters/chain_field_converter.ts +74 -0
  174. package/field_converters/identity_converter.ts +39 -0
  175. package/field_converters/integer_to_string_converter.ts +32 -0
  176. package/field_converters/list_converter.ts +15 -0
  177. package/field_converters/maybe_identity_converter.ts +23 -0
  178. package/field_converters/nullable_to_boolean_converter.ts +56 -0
  179. package/field_converters/select_value_type_converter.ts +141 -0
  180. package/field_converters/trimming_string_converter.ts +23 -0
  181. package/field_converters/validating_converter.ts +35 -0
  182. package/field_validators/minimum_string_length_field_validator.ts +13 -0
  183. package/field_value_factories/prototyping_field_value_factory.ts +11 -0
  184. package/index.ts +16 -0
  185. package/mantine/create_checkbox.tsx +79 -0
  186. package/mantine/create_list.tsx +58 -0
  187. package/mantine/create_pill.tsx +43 -0
  188. package/mantine/create_radio.tsx +36 -0
  189. package/mantine/create_radio_group.tsx +71 -0
  190. package/mantine/create_text_input.tsx +80 -0
  191. package/mantine/create_value_input.tsx +81 -0
  192. package/mantine/hooks.tsx +394 -0
  193. package/mantine/specs/__snapshots__/check_box_hooks.tests.tsx.snap +227 -0
  194. package/mantine/specs/__snapshots__/checkbox_hooks.tests.tsx.snap +227 -0
  195. package/mantine/specs/__snapshots__/list_hooks.tests.tsx.snap +68 -0
  196. package/mantine/specs/__snapshots__/radio_group_hooks.tests.tsx.snap +695 -0
  197. package/mantine/specs/__snapshots__/select_hooks.tests.tsx.snap +225 -0
  198. package/mantine/specs/__snapshots__/text_input_hooks.tests.tsx.snap +202 -0
  199. package/mantine/specs/__snapshots__/value_input_hooks.tests.tsx.snap +613 -0
  200. package/mantine/specs/checkbox_constants.ts +1 -0
  201. package/mantine/specs/checkbox_hooks.stories.tsx +79 -0
  202. package/mantine/specs/checkbox_hooks.tests.tsx +100 -0
  203. package/mantine/specs/list_hooks.stories.tsx +83 -0
  204. package/mantine/specs/list_hooks.tests.tsx +15 -0
  205. package/mantine/specs/radio_group_constants.ts +12 -0
  206. package/mantine/specs/radio_group_hooks.stories.tsx +103 -0
  207. package/mantine/specs/radio_group_hooks.tests.tsx +92 -0
  208. package/mantine/specs/select_hooks.stories.tsx +77 -0
  209. package/mantine/specs/select_hooks.tests.tsx +14 -0
  210. package/mantine/specs/select_hooks_constant.ts +1 -0
  211. package/mantine/specs/text_input_constants.ts +1 -0
  212. package/mantine/specs/text_input_hooks.stories.tsx +124 -0
  213. package/mantine/specs/text_input_hooks.tests.tsx +106 -0
  214. package/mantine/specs/value_input_constants.ts +2 -0
  215. package/mantine/specs/value_input_hooks.stories.tsx +182 -0
  216. package/mantine/specs/value_input_hooks.tests.tsx +14 -0
  217. package/mantine/types.ts +13 -0
  218. package/package.exports.json +18 -0
  219. package/package.json +74 -0
  220. package/tsconfig.build.json +13 -0
  221. package/tsconfig.json +27 -0
  222. package/tsup.config.ts +16 -0
  223. package/types/all_fields_of_fields.ts +9 -0
  224. package/types/boolean_fields_of_fields.ts +8 -0
  225. package/types/error_type_of_field.ts +3 -0
  226. package/types/field.ts +9 -0
  227. package/types/field_converters.ts +64 -0
  228. package/types/field_validator.ts +7 -0
  229. package/types/flattened_form_fields_of.ts +16 -0
  230. package/types/list_fields_of_fields.ts +7 -0
  231. package/types/specs/boolean_fields_of_fields.tests.ts +23 -0
  232. package/types/specs/error_type_of_field.tests.ts +10 -0
  233. package/types/specs/flattened_form_fields_of.tests.ts +43 -0
  234. package/types/specs/string_fields_of_fields.tests.ts +40 -0
  235. package/types/specs/value_type_of_field.tests.ts +10 -0
  236. package/types/string_fields_of_fields.ts +6 -0
  237. package/types/value_type_of_field.ts +3 -0
  238. package/util/partial.tsx +200 -0
  239. package/vitest.workspace.ts +26 -0
@@ -0,0 +1,705 @@
1
+ import {
2
+ assertExists,
3
+ assertExistsAndReturn,
4
+ assertState,
5
+ checkValidNumber,
6
+ type ElementOfArray,
7
+ type Maybe,
8
+ toArray,
9
+ UnreachableError,
10
+ } from '@strictly/base'
11
+ import {
12
+ type Accessor,
13
+ type AnyValueType,
14
+ flattenAccessorsOf,
15
+ type FlattenedValueTypesOf,
16
+ flattenTypeDefsOf,
17
+ flattenValueTypeTo,
18
+ jsonPathPop,
19
+ mobxCopy,
20
+ type MobxValueTypeOf,
21
+ type ReadonlyTypeDefOf,
22
+ type StrictTypeDef,
23
+ type Type,
24
+ valuePathToTypePath,
25
+ type ValueTypeOf,
26
+ } from '@strictly/define'
27
+ import {
28
+ computed,
29
+ observable,
30
+ runInAction,
31
+ } from 'mobx'
32
+ import {
33
+ type ReadonlyDeep,
34
+ type SimplifyDeep,
35
+ type StringKeyOf,
36
+ type ValueOf,
37
+ } from 'type-fest'
38
+ import {
39
+ type Field,
40
+ } from 'types/field'
41
+ import {
42
+ FieldConversionResult,
43
+ } from 'types/field_converters'
44
+ import {
45
+ type ErrorTypeOfFieldAdapter,
46
+ type FieldAdapter,
47
+ type ToTypeOfFieldAdapter,
48
+ } from './field_adapter'
49
+ import {
50
+ type FlattenedListTypeDefsOf,
51
+ } from './flattened_list_type_defs_of'
52
+
53
+ export type FlattenedConvertedFieldsOf<
54
+ ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>,
55
+ > = {
56
+ readonly [K in keyof ValuePathsToAdapters]: Field<
57
+ ToTypeOfFieldAdapter<ValuePathsToAdapters[K]>,
58
+ ErrorTypeOfFieldAdapter<ValuePathsToAdapters[K]>
59
+ >
60
+ }
61
+
62
+ export type FlattenedTypePathsToAdaptersOf<
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ FlattenedValues extends Readonly<Record<string, any>>,
65
+ Context,
66
+ > = {
67
+ readonly [
68
+ K in keyof FlattenedValues
69
+ // TODO would be better to use the equivalent readonly typedef, but it causes typescript to
70
+ // infinitely recurse
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ ]?: FieldAdapter<ReadonlyDeep<FlattenedValues[K]>, any, any, any, Context>
73
+ }
74
+
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ type FieldOverride<V = any> = {
77
+ value: V,
78
+ }
79
+
80
+ type FlattenedFieldOverrides<
81
+ ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>,
82
+ > = {
83
+ -readonly [K in keyof ValuePathsToAdapters]?: FieldOverride<
84
+ ToTypeOfFieldAdapter<ValuePathsToAdapters[K]>
85
+ >
86
+ }
87
+
88
+ type FlattenedErrors<
89
+ ValuePathsToAdapters extends Readonly<Record<string, FieldAdapter>>,
90
+ > = {
91
+ -readonly [K in keyof ValuePathsToAdapters]?: ErrorTypeOfFieldAdapter<ValuePathsToAdapters[K]>
92
+ }
93
+
94
+ export type ValuePathsToAdaptersOf<
95
+ TypePathsToAdapters extends Partial<Readonly<Record<string, FieldAdapter>>>,
96
+ ValuePathsToTypePaths extends Readonly<Record<string, string>>,
97
+ > = keyof TypePathsToAdapters extends ValueOf<ValuePathsToTypePaths> ? {
98
+ readonly [
99
+ K in keyof ValuePathsToTypePaths as unknown extends TypePathsToAdapters[ValuePathsToTypePaths[K]] ? never : K
100
+ ]: NonNullable<TypePathsToAdapters[ValuePathsToTypePaths[K]]>
101
+ }
102
+ : never
103
+
104
+ export class FormPresenter<
105
+ T extends Type,
106
+ ValueToTypePaths extends Readonly<Record<string, string>>,
107
+ TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<
108
+ FlattenedValueTypesOf<T, '*'>,
109
+ ValueTypeOf<ReadonlyTypeDefOf<T>>
110
+ >,
111
+ ValuePathsToAdapters extends ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths> = ValuePathsToAdaptersOf<
112
+ TypePathsToAdapters,
113
+ ValueToTypePaths
114
+ >,
115
+ > {
116
+ constructor(
117
+ readonly typeDef: T,
118
+ private readonly adapters: TypePathsToAdapters,
119
+ ) {
120
+ }
121
+
122
+ private maybeGetAdapterForValuePath(valuePath: keyof ValuePathsToAdapters) {
123
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
124
+ const typePath = valuePathToTypePath(this.typeDef, valuePath as string, true)
125
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
126
+ return this.adapters[typePath as keyof TypePathsToAdapters]
127
+ }
128
+
129
+ private getAdapterForValuePath(valuePath: keyof ValuePathsToAdapters) {
130
+ return assertExistsAndReturn(
131
+ this.maybeGetAdapterForValuePath(valuePath),
132
+ 'expected adapter to be defined {}',
133
+ valuePath,
134
+ )
135
+ }
136
+
137
+ typePath<K extends keyof ValueToTypePaths>(valuePath: K): ValueToTypePaths[K] {
138
+ return valuePathToTypePath<ValueToTypePaths, K>(this.typeDef, valuePath, true)
139
+ }
140
+
141
+ setFieldValueAndValidate<K extends keyof ValuePathsToAdapters>(
142
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
143
+ valuePath: K,
144
+ value: ToTypeOfFieldAdapter<ValuePathsToAdapters[K]>,
145
+ ): boolean {
146
+ return this.internalSetFieldValue(model, valuePath, value, true)
147
+ }
148
+
149
+ setFieldValue<K extends keyof ValuePathsToAdapters>(
150
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
151
+ valuePath: K,
152
+ value: ToTypeOfFieldAdapter<ValuePathsToAdapters[K]>,
153
+ ): boolean {
154
+ return this.internalSetFieldValue(model, valuePath, value, false)
155
+ }
156
+
157
+ addListItem<K extends keyof FlattenedListTypeDefsOf<T>>(
158
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
159
+ valuePath: K,
160
+ elementValue: Maybe<ElementOfArray<FlattenedValueTypesOf<T>[K]>>,
161
+ index?: number,
162
+ ) {
163
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
164
+ const listValuePath = valuePath as string
165
+ const accessor = model.accessors[valuePath]
166
+ const listTypePath = this.typePath(valuePath)
167
+ const definedIndex = index ?? accessor.value.length
168
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
169
+ const elementTypePath = `${listTypePath}.*` as keyof TypePathsToAdapters
170
+ const elementAdapter = assertExistsAndReturn(
171
+ this.adapters[elementTypePath],
172
+ 'no adapter specified for list {} ({})',
173
+ elementTypePath,
174
+ valuePath,
175
+ )
176
+ // TODO validation on new elements
177
+ const element = elementValue != null
178
+ ? elementValue[0]
179
+ : elementAdapter.create(
180
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
181
+ elementTypePath as string,
182
+ model.value,
183
+ )
184
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
+ const originalList: any[] = accessor.value
186
+ const newList = [
187
+ ...originalList.slice(0, definedIndex),
188
+ element,
189
+ ...originalList.slice(definedIndex),
190
+ ]
191
+ // shuffle the overrides around to account for new indices
192
+ // to so this we need to sort the array indices in descending order
193
+ const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
194
+ return v.startsWith(`${listValuePath}.`)
195
+ }).map(function (v) {
196
+ const parts = v.substring(listValuePath.length + 1).split('.')
197
+ const index = parseInt(parts[0])
198
+ return [
199
+ index,
200
+ parts.slice(1),
201
+ ] as const
202
+ }).filter(function ([index]) {
203
+ return index >= definedIndex
204
+ }).sort(function ([a], [b]) {
205
+ // descending
206
+ return b - a
207
+ })
208
+ runInAction(function () {
209
+ targetPaths.forEach(function ([
210
+ index,
211
+ postfix,
212
+ ]) {
213
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
214
+ const fromJsonPath = [
215
+ listValuePath,
216
+ `${index}`,
217
+ ...postfix,
218
+ ].join('.') as keyof ValuePathsToAdapters
219
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
220
+ const toJsonPath = [
221
+ listValuePath,
222
+ `${index + 1}`,
223
+ ...postfix,
224
+ ].join('.') as keyof ValuePathsToAdapters
225
+ const fieldOverride = model.fieldOverrides[fromJsonPath]
226
+ delete model.fieldOverrides[fromJsonPath]
227
+ model.fieldOverrides[toJsonPath] = fieldOverride
228
+ const error = model.errors[fromJsonPath]
229
+ delete model.errors[fromJsonPath]
230
+ model.errors[toJsonPath] = error
231
+ })
232
+ accessor.set(newList)
233
+ // delete any value overrides so the new list isn't shadowed
234
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
235
+ delete model.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
236
+ })
237
+ }
238
+
239
+ removeListItem<K extends keyof FlattenedListTypeDefsOf<T>>(
240
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
241
+ elementValuePath: `${K}.${number}`,
242
+ ) {
243
+ const [
244
+ listValuePath,
245
+ elementIndexString,
246
+ ] = assertExistsAndReturn(
247
+ jsonPathPop(elementValuePath),
248
+ 'expected a path with two or more segments {}',
249
+ elementValuePath,
250
+ )
251
+ const accessor = model.accessors[listValuePath]
252
+ const elementIndex = checkValidNumber(
253
+ parseInt(elementIndexString),
254
+ 'unexpected index {} ({})',
255
+ elementIndexString,
256
+ elementValuePath,
257
+ )
258
+ const newList = [...accessor.value]
259
+ assertState(
260
+ elementIndex >= 0 && elementIndex < newList.length,
261
+ 'invalid index from path {} ({})',
262
+ elementIndex,
263
+ elementValuePath,
264
+ )
265
+ newList.splice(elementIndex, 1)
266
+
267
+ // shuffle the overrides around to account for new indices
268
+ // to so this we need to sort the array indices in descending order
269
+ const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
270
+ return v.startsWith(`${listValuePath}.`)
271
+ }).map(function (v) {
272
+ const parts = v.substring(listValuePath.length + 1).split('.')
273
+ const index = parseInt(parts[0])
274
+ return [
275
+ index,
276
+ parts.slice(1),
277
+ ] as const
278
+ }).filter(function ([index]) {
279
+ return index > elementIndex
280
+ }).sort(function ([a], [b]) {
281
+ // ascending
282
+ return a - b
283
+ })
284
+
285
+ runInAction(function () {
286
+ targetPaths.forEach(function ([
287
+ index,
288
+ postfix,
289
+ ]) {
290
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
291
+ const fromJsonPath = [
292
+ listValuePath,
293
+ `${index}`,
294
+ ...postfix,
295
+ ].join('.') as keyof ValuePathsToAdapters
296
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
297
+ const toJsonPath = [
298
+ listValuePath,
299
+ `${index - 1}`,
300
+ ...postfix,
301
+ ].join('.') as keyof ValuePathsToAdapters
302
+ const fieldOverride = model.fieldOverrides[fromJsonPath]
303
+ delete model.fieldOverrides[fromJsonPath]
304
+ model.fieldOverrides[toJsonPath] = fieldOverride
305
+ const error = model.errors[fromJsonPath]
306
+ delete model.errors[fromJsonPath]
307
+ model.errors[toJsonPath] = error
308
+ })
309
+ accessor.set(newList)
310
+ // delete any value overrides so the new list isn't shadowed
311
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
312
+ delete model.fieldOverrides[listValuePath as keyof ValuePathsToAdapters]
313
+ })
314
+ }
315
+
316
+ private internalSetFieldValue<K extends keyof ValuePathsToAdapters>(
317
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
318
+ valuePath: K,
319
+ value: ToTypeOfFieldAdapter<ValuePathsToAdapters[K]>,
320
+ displayValidation: boolean,
321
+ ): boolean {
322
+ const { revert } = this.getAdapterForValuePath(valuePath)
323
+
324
+ assertExists(revert, 'setting value not supported {}', valuePath)
325
+
326
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
327
+ const conversion = revert(value, valuePath as any, model.value)
328
+ const accessor = model.getAccessorForValuePath(valuePath)
329
+ return runInAction(() => {
330
+ model.fieldOverrides[valuePath] = {
331
+ value,
332
+ }
333
+ switch (conversion.type) {
334
+ case FieldConversionResult.Failure:
335
+ if (displayValidation) {
336
+ model.errors[valuePath] = conversion.error
337
+ }
338
+ if (conversion.value != null && accessor != null) {
339
+ accessor.set(conversion.value[0])
340
+ }
341
+ return false
342
+ case FieldConversionResult.Success:
343
+ delete model.errors[valuePath]
344
+ accessor?.set(conversion.value)
345
+ return true
346
+ default:
347
+ throw new UnreachableError(conversion)
348
+ }
349
+ })
350
+ }
351
+
352
+ clearFieldError<K extends keyof ValuePathsToAdapters>(
353
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
354
+ valuePath: K,
355
+ ) {
356
+ const fieldOverride = model.fieldOverrides[valuePath]
357
+ if (fieldOverride != null) {
358
+ runInAction(function () {
359
+ delete model.errors[valuePath]
360
+ })
361
+ }
362
+ }
363
+
364
+ clearFieldValue<K extends StringKeyOf<ValuePathsToAdapters>>(
365
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
366
+ valuePath: K,
367
+ ) {
368
+ const typePath = this.typePath(valuePath)
369
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
370
+ const adapter = this.adapters[typePath as keyof TypePathsToAdapters]
371
+ if (adapter == null) {
372
+ return
373
+ }
374
+ const {
375
+ convert,
376
+ create,
377
+ } = adapter
378
+ const accessor = model.accessors[valuePath]
379
+ const value = accessor == null ? create(valuePath, model.value) : accessor.value
380
+ const displayValue = convert(value, valuePath, model.value)
381
+ runInAction(function () {
382
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
383
+ model.fieldOverrides[valuePath as unknown as keyof ValuePathsToAdapters] = {
384
+ value: displayValue,
385
+ }
386
+ })
387
+ }
388
+
389
+ clearAll(
390
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
391
+ value: ValueTypeOf<T>,
392
+ ): void {
393
+ runInAction(() => {
394
+ model.errors = {}
395
+ // TODO this isn't correct, should reload from value
396
+ model.fieldOverrides = {}
397
+ model.value = mobxCopy(this.typeDef, value)
398
+ })
399
+ }
400
+
401
+ validateField<K extends keyof ValuePathsToAdapters>(
402
+ model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>,
403
+ valuePath: K,
404
+ ): boolean {
405
+ const {
406
+ convert,
407
+ revert,
408
+ create,
409
+ } = this.getAdapterForValuePath(valuePath)
410
+ const fieldOverride = model.fieldOverrides[valuePath]
411
+ const accessor = model.getAccessorForValuePath(valuePath)
412
+ const storedValue = convert(
413
+ accessor != null
414
+ ? accessor.value
415
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
416
+ : create(valuePath as string, model.value),
417
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
418
+ valuePath as string,
419
+ model.value,
420
+ )
421
+ const value = fieldOverride != null
422
+ ? fieldOverride.value
423
+ : storedValue
424
+ const dirty = storedValue !== value
425
+ assertExists(revert, 'changing field directly not supported {}', valuePath)
426
+
427
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
428
+ const conversion = revert(value, valuePath as string, model.value)
429
+ return runInAction(function () {
430
+ switch (conversion.type) {
431
+ case FieldConversionResult.Failure:
432
+ model.errors[valuePath] = conversion.error
433
+ if (conversion.value != null && accessor != null && dirty) {
434
+ accessor.set(conversion.value[0])
435
+ }
436
+ return false
437
+ case FieldConversionResult.Success:
438
+ delete model.errors[valuePath]
439
+ if (accessor != null && dirty) {
440
+ accessor.set(conversion.value)
441
+ }
442
+ return true
443
+ default:
444
+ throw new UnreachableError(conversion)
445
+ }
446
+ })
447
+ }
448
+
449
+ validateAll(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>): boolean {
450
+ // sort keys shortest to longest so parent changes don't overwrite child changes
451
+ const accessors = toArray(model.accessors).toSorted(function ([a], [b]) {
452
+ return a.length - b.length
453
+ })
454
+ return runInAction(() => {
455
+ return accessors.reduce(
456
+ (
457
+ success,
458
+ [
459
+ valuePath,
460
+ accessor,
461
+ ],
462
+ ): boolean => {
463
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
464
+ const adapterPath = valuePath as keyof ValuePathsToAdapters
465
+ const adapter = this.maybeGetAdapterForValuePath(adapterPath)
466
+ if (adapter == null) {
467
+ // no adapter == there should be nothing specified for this field
468
+ return success
469
+ }
470
+ const {
471
+ convert,
472
+ revert,
473
+ } = adapter
474
+ if (revert == null) {
475
+ // no convert method means this field is immutable
476
+ return success
477
+ }
478
+ const fieldOverride = model.fieldOverrides[adapterPath]
479
+ const storedValue = convert(accessor.value, valuePath, model.value)
480
+ const value = fieldOverride != null
481
+ ? fieldOverride.value
482
+ : storedValue
483
+ // TODO more nuanced comparison
484
+ const dirty = fieldOverride != null && fieldOverride.value !== storedValue
485
+
486
+ const conversion = revert(value, valuePath, model.value)
487
+ switch (conversion.type) {
488
+ case FieldConversionResult.Failure:
489
+ model.errors[adapterPath] = conversion.error
490
+ if (conversion.value != null && dirty) {
491
+ accessor.set(conversion.value[0])
492
+ }
493
+ return false
494
+ case FieldConversionResult.Success:
495
+ if (dirty) {
496
+ accessor.set(conversion.value)
497
+ }
498
+ delete model.errors[adapterPath]
499
+ return success
500
+ default:
501
+ throw new UnreachableError(conversion)
502
+ }
503
+ },
504
+ true,
505
+ )
506
+ })
507
+ }
508
+
509
+ createModel(value: ValueTypeOf<ReadonlyTypeDefOf<T>>): FormModel<
510
+ T,
511
+ ValueToTypePaths,
512
+ TypePathsToAdapters,
513
+ ValuePathsToAdapters
514
+ > {
515
+ return new FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>(
516
+ this.typeDef,
517
+ value,
518
+ this.adapters,
519
+ )
520
+ }
521
+ }
522
+
523
+ export class FormModel<
524
+ T extends Type,
525
+ ValueToTypePaths extends Readonly<Record<string, string>>,
526
+ TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<
527
+ FlattenedValueTypesOf<T, '*'>,
528
+ ValueTypeOf<ReadonlyTypeDefOf<T>>
529
+ >,
530
+ ValuePathsToAdapters extends ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths> = ValuePathsToAdaptersOf<
531
+ TypePathsToAdapters,
532
+ ValueToTypePaths
533
+ >,
534
+ > {
535
+ @observable.ref
536
+ accessor value: MobxValueTypeOf<T>
537
+ @observable.shallow
538
+ accessor fieldOverrides: FlattenedFieldOverrides<ValuePathsToAdapters>
539
+ @observable.shallow
540
+ accessor errors: FlattenedErrors<ValuePathsToAdapters> = {}
541
+
542
+ private readonly flattenedTypeDefs: Readonly<Record<string, Type>>
543
+
544
+ constructor(
545
+ private readonly typeDef: T,
546
+ value: ValueTypeOf<ReadonlyTypeDefOf<T>>,
547
+ private readonly adapters: TypePathsToAdapters,
548
+ ) {
549
+ this.value = mobxCopy(typeDef, value)
550
+ this.flattenedTypeDefs = flattenTypeDefsOf(typeDef)
551
+ // pre-populate field overrides for consistent behavior when default information is overwritten
552
+ // then returned to
553
+ this.fieldOverrides = flattenValueTypeTo(
554
+ typeDef,
555
+ this.value,
556
+ () => {},
557
+ (_t: StrictTypeDef, value: AnyValueType, _setter, typePath, valuePath): FieldOverride | undefined => {
558
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
559
+ const adapter = this.adapters[typePath as keyof TypePathsToAdapters]
560
+ if (adapter == null) {
561
+ return
562
+ }
563
+ const {
564
+ convert,
565
+ revert,
566
+ } = adapter
567
+ if (revert == null) {
568
+ // no need to store a temporary value if the value cannot be written back
569
+ return
570
+ }
571
+ const displayValue = convert(value, valuePath, this.value)
572
+ return {
573
+ value: displayValue,
574
+ }
575
+ },
576
+ )
577
+ }
578
+
579
+ @computed
580
+ get fields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>> {
581
+ return new Proxy<SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>>>(
582
+ this.knownFields,
583
+ {
584
+ get: (target, prop) => {
585
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
586
+ const field = (target as any)[prop]
587
+ if (field != null) {
588
+ return field
589
+ }
590
+ if (typeof prop === 'string') {
591
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
592
+ return this.maybeSynthesizeFieldByValuePath(prop as keyof ValuePathsToAdapters)
593
+ }
594
+ },
595
+ },
596
+ )
597
+ }
598
+
599
+ @computed
600
+ private get knownFields(): SimplifyDeep<FlattenedConvertedFieldsOf<ValuePathsToAdapters>> {
601
+ return flattenValueTypeTo(
602
+ this.typeDef,
603
+ this.value,
604
+ () => {},
605
+ // TODO swap these to valuePath, typePath in flatten
606
+ (_t: StrictTypeDef, _v: AnyValueType, _setter, typePath, valuePath): Field | undefined => {
607
+ return this.synthesizeFieldByPaths(
608
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
609
+ valuePath as keyof ValuePathsToAdapters,
610
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
611
+ typePath as keyof TypePathsToAdapters,
612
+ )
613
+ },
614
+ )
615
+ }
616
+
617
+ private maybeSynthesizeFieldByValuePath(valuePath: keyof ValuePathsToAdapters): Field | undefined {
618
+ let typePath: keyof TypePathsToAdapters
619
+ try {
620
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
621
+ typePath = valuePathToTypePath<ValueToTypePaths, keyof ValueToTypePaths>(
622
+ this.typeDef,
623
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
624
+ valuePath as keyof ValueToTypePaths,
625
+ true,
626
+ ) as keyof TypePathsToAdapters
627
+ } catch (e) {
628
+ // TODO make jsonValuePathToTypePath return null in the event of an invalid
629
+ // value path instead of throwing an exception
630
+ // assume that the path was invalid
631
+ return
632
+ }
633
+ return this.synthesizeFieldByPaths(valuePath, typePath)
634
+ }
635
+
636
+ private synthesizeFieldByPaths(valuePath: keyof ValuePathsToAdapters, typePath: keyof TypePathsToAdapters) {
637
+ const adapter = this.adapters[typePath]
638
+ if (adapter == null) {
639
+ // invalid path, which can happen
640
+ return
641
+ }
642
+ const {
643
+ convert,
644
+ create,
645
+ } = adapter
646
+
647
+ const fieldOverride = this.fieldOverrides[valuePath]
648
+ const accessor = this.getAccessorForValuePath(valuePath)
649
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
650
+ const fieldTypeDef = this.flattenedTypeDefs[typePath as string]
651
+ const value = fieldOverride
652
+ ? fieldOverride.value
653
+ : convert(
654
+ accessor != null
655
+ ? accessor.value
656
+ : fieldTypeDef != null
657
+ ? mobxCopy(
658
+ fieldTypeDef,
659
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
660
+ create(valuePath as string, this.value),
661
+ )
662
+ // fake values can't be copied
663
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
664
+ : create(valuePath as string, this.value),
665
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
666
+ valuePath as string,
667
+ this.value,
668
+ )
669
+ const error = this.errors[valuePath]
670
+ return {
671
+ value,
672
+ error,
673
+ // if we can't write it back, then we have to disable it
674
+ disabled: this.isDisabled(valuePath),
675
+ required: this.isRequired(valuePath),
676
+ }
677
+ }
678
+
679
+ getAccessorForValuePath(valuePath: keyof ValuePathsToAdapters): Accessor | undefined {
680
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
681
+ return this.accessors[valuePath as string]
682
+ }
683
+
684
+ @computed
685
+ // should only be referenced internally, so loosely typed
686
+ get accessors(): Readonly<Record<string, Accessor>> {
687
+ return flattenAccessorsOf<T, Readonly<Record<string, Accessor>>>(
688
+ this.typeDef,
689
+ this.value,
690
+ (value: ValueTypeOf<T>): void => {
691
+ this.value = mobxCopy(this.typeDef, value)
692
+ },
693
+ )
694
+ }
695
+
696
+ protected isDisabled(_valuePath: keyof ValuePathsToAdapters): boolean {
697
+ // TODO infer from types
698
+ return false
699
+ }
700
+
701
+ protected isRequired(_valuePath: keyof ValuePathsToAdapters): boolean {
702
+ // TODO infer from types
703
+ return false
704
+ }
705
+ }