@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,425 @@
1
+ import { assertExists, assertExistsAndReturn, assertState, checkValidNumber, toArray, UnreachableError, } from '@strictly/base';
2
+ import { flattenAccessorsOf, flattenTypeDefsOf, flattenValueTypeTo, jsonPathPop, mobxCopy, valuePathToTypePath, } from '@strictly/define';
3
+ import { computed, observable, runInAction, } from 'mobx';
4
+ import { FieldConversionResult, } from 'types/field_converters';
5
+ export class FormPresenter {
6
+ typeDef;
7
+ adapters;
8
+ constructor(typeDef, adapters) {
9
+ this.typeDef = typeDef;
10
+ this.adapters = adapters;
11
+ }
12
+ maybeGetAdapterForValuePath(valuePath) {
13
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
14
+ const typePath = valuePathToTypePath(this.typeDef, valuePath, true);
15
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16
+ return this.adapters[typePath];
17
+ }
18
+ getAdapterForValuePath(valuePath) {
19
+ return assertExistsAndReturn(this.maybeGetAdapterForValuePath(valuePath), 'expected adapter to be defined {}', valuePath);
20
+ }
21
+ typePath(valuePath) {
22
+ return valuePathToTypePath(this.typeDef, valuePath, true);
23
+ }
24
+ setFieldValueAndValidate(model, valuePath, value) {
25
+ return this.internalSetFieldValue(model, valuePath, value, true);
26
+ }
27
+ setFieldValue(model, valuePath, value) {
28
+ return this.internalSetFieldValue(model, valuePath, value, false);
29
+ }
30
+ addListItem(model, valuePath, elementValue, index) {
31
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
32
+ const listValuePath = valuePath;
33
+ const accessor = model.accessors[valuePath];
34
+ const listTypePath = this.typePath(valuePath);
35
+ const definedIndex = index ?? accessor.value.length;
36
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
37
+ const elementTypePath = `${listTypePath}.*`;
38
+ const elementAdapter = assertExistsAndReturn(this.adapters[elementTypePath], 'no adapter specified for list {} ({})', elementTypePath, valuePath);
39
+ // TODO validation on new elements
40
+ const element = elementValue != null
41
+ ? elementValue[0]
42
+ : elementAdapter.create(
43
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
44
+ elementTypePath, model.value);
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ const originalList = accessor.value;
47
+ const newList = [
48
+ ...originalList.slice(0, definedIndex),
49
+ element,
50
+ ...originalList.slice(definedIndex),
51
+ ];
52
+ // shuffle the overrides around to account for new indices
53
+ // to so this we need to sort the array indices in descending order
54
+ const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
55
+ return v.startsWith(`${listValuePath}.`);
56
+ }).map(function (v) {
57
+ const parts = v.substring(listValuePath.length + 1).split('.');
58
+ const index = parseInt(parts[0]);
59
+ return [
60
+ index,
61
+ parts.slice(1),
62
+ ];
63
+ }).filter(function ([index]) {
64
+ return index >= definedIndex;
65
+ }).sort(function ([a], [b]) {
66
+ // descending
67
+ return b - a;
68
+ });
69
+ runInAction(function () {
70
+ targetPaths.forEach(function ([index, postfix,]) {
71
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
72
+ const fromJsonPath = [
73
+ listValuePath,
74
+ `${index}`,
75
+ ...postfix,
76
+ ].join('.');
77
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
78
+ const toJsonPath = [
79
+ listValuePath,
80
+ `${index + 1}`,
81
+ ...postfix,
82
+ ].join('.');
83
+ const fieldOverride = model.fieldOverrides[fromJsonPath];
84
+ delete model.fieldOverrides[fromJsonPath];
85
+ model.fieldOverrides[toJsonPath] = fieldOverride;
86
+ const error = model.errors[fromJsonPath];
87
+ delete model.errors[fromJsonPath];
88
+ model.errors[toJsonPath] = error;
89
+ });
90
+ accessor.set(newList);
91
+ // delete any value overrides so the new list isn't shadowed
92
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
93
+ delete model.fieldOverrides[listValuePath];
94
+ });
95
+ }
96
+ removeListItem(model, elementValuePath) {
97
+ const [listValuePath, elementIndexString,] = assertExistsAndReturn(jsonPathPop(elementValuePath), 'expected a path with two or more segments {}', elementValuePath);
98
+ const accessor = model.accessors[listValuePath];
99
+ const elementIndex = checkValidNumber(parseInt(elementIndexString), 'unexpected index {} ({})', elementIndexString, elementValuePath);
100
+ const newList = [...accessor.value];
101
+ assertState(elementIndex >= 0 && elementIndex < newList.length, 'invalid index from path {} ({})', elementIndex, elementValuePath);
102
+ newList.splice(elementIndex, 1);
103
+ // shuffle the overrides around to account for new indices
104
+ // to so this we need to sort the array indices in descending order
105
+ const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
106
+ return v.startsWith(`${listValuePath}.`);
107
+ }).map(function (v) {
108
+ const parts = v.substring(listValuePath.length + 1).split('.');
109
+ const index = parseInt(parts[0]);
110
+ return [
111
+ index,
112
+ parts.slice(1),
113
+ ];
114
+ }).filter(function ([index]) {
115
+ return index > elementIndex;
116
+ }).sort(function ([a], [b]) {
117
+ // ascending
118
+ return a - b;
119
+ });
120
+ runInAction(function () {
121
+ targetPaths.forEach(function ([index, postfix,]) {
122
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
123
+ const fromJsonPath = [
124
+ listValuePath,
125
+ `${index}`,
126
+ ...postfix,
127
+ ].join('.');
128
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
129
+ const toJsonPath = [
130
+ listValuePath,
131
+ `${index - 1}`,
132
+ ...postfix,
133
+ ].join('.');
134
+ const fieldOverride = model.fieldOverrides[fromJsonPath];
135
+ delete model.fieldOverrides[fromJsonPath];
136
+ model.fieldOverrides[toJsonPath] = fieldOverride;
137
+ const error = model.errors[fromJsonPath];
138
+ delete model.errors[fromJsonPath];
139
+ model.errors[toJsonPath] = error;
140
+ });
141
+ accessor.set(newList);
142
+ // delete any value overrides so the new list isn't shadowed
143
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
144
+ delete model.fieldOverrides[listValuePath];
145
+ });
146
+ }
147
+ internalSetFieldValue(model, valuePath, value, displayValidation) {
148
+ const { revert } = this.getAdapterForValuePath(valuePath);
149
+ assertExists(revert, 'setting value not supported {}', valuePath);
150
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
151
+ const conversion = revert(value, valuePath, model.value);
152
+ const accessor = model.getAccessorForValuePath(valuePath);
153
+ return runInAction(() => {
154
+ model.fieldOverrides[valuePath] = {
155
+ value,
156
+ };
157
+ switch (conversion.type) {
158
+ case FieldConversionResult.Failure:
159
+ if (displayValidation) {
160
+ model.errors[valuePath] = conversion.error;
161
+ }
162
+ if (conversion.value != null && accessor != null) {
163
+ accessor.set(conversion.value[0]);
164
+ }
165
+ return false;
166
+ case FieldConversionResult.Success:
167
+ delete model.errors[valuePath];
168
+ accessor?.set(conversion.value);
169
+ return true;
170
+ default:
171
+ throw new UnreachableError(conversion);
172
+ }
173
+ });
174
+ }
175
+ clearFieldError(model, valuePath) {
176
+ const fieldOverride = model.fieldOverrides[valuePath];
177
+ if (fieldOverride != null) {
178
+ runInAction(function () {
179
+ delete model.errors[valuePath];
180
+ });
181
+ }
182
+ }
183
+ clearFieldValue(model, valuePath) {
184
+ const typePath = this.typePath(valuePath);
185
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
186
+ const adapter = this.adapters[typePath];
187
+ if (adapter == null) {
188
+ return;
189
+ }
190
+ const { convert, create, } = adapter;
191
+ const accessor = model.accessors[valuePath];
192
+ const value = accessor == null ? create(valuePath, model.value) : accessor.value;
193
+ const displayValue = convert(value, valuePath, model.value);
194
+ runInAction(function () {
195
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
196
+ model.fieldOverrides[valuePath] = {
197
+ value: displayValue,
198
+ };
199
+ });
200
+ }
201
+ clearAll(model, value) {
202
+ runInAction(() => {
203
+ model.errors = {};
204
+ // TODO this isn't correct, should reload from value
205
+ model.fieldOverrides = {};
206
+ model.value = mobxCopy(this.typeDef, value);
207
+ });
208
+ }
209
+ validateField(model, valuePath) {
210
+ const { convert, revert, create, } = this.getAdapterForValuePath(valuePath);
211
+ const fieldOverride = model.fieldOverrides[valuePath];
212
+ const accessor = model.getAccessorForValuePath(valuePath);
213
+ const storedValue = convert(accessor != null
214
+ ? accessor.value
215
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
216
+ : create(valuePath, model.value),
217
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
218
+ valuePath, model.value);
219
+ const value = fieldOverride != null
220
+ ? fieldOverride.value
221
+ : storedValue;
222
+ const dirty = storedValue !== value;
223
+ assertExists(revert, 'changing field directly not supported {}', valuePath);
224
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
225
+ const conversion = revert(value, valuePath, model.value);
226
+ return runInAction(function () {
227
+ switch (conversion.type) {
228
+ case FieldConversionResult.Failure:
229
+ model.errors[valuePath] = conversion.error;
230
+ if (conversion.value != null && accessor != null && dirty) {
231
+ accessor.set(conversion.value[0]);
232
+ }
233
+ return false;
234
+ case FieldConversionResult.Success:
235
+ delete model.errors[valuePath];
236
+ if (accessor != null && dirty) {
237
+ accessor.set(conversion.value);
238
+ }
239
+ return true;
240
+ default:
241
+ throw new UnreachableError(conversion);
242
+ }
243
+ });
244
+ }
245
+ validateAll(model) {
246
+ // sort keys shortest to longest so parent changes don't overwrite child changes
247
+ const accessors = toArray(model.accessors).toSorted(function ([a], [b]) {
248
+ return a.length - b.length;
249
+ });
250
+ return runInAction(() => {
251
+ return accessors.reduce((success, [valuePath, accessor,]) => {
252
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
253
+ const adapterPath = valuePath;
254
+ const adapter = this.maybeGetAdapterForValuePath(adapterPath);
255
+ if (adapter == null) {
256
+ // no adapter == there should be nothing specified for this field
257
+ return success;
258
+ }
259
+ const { convert, revert, } = adapter;
260
+ if (revert == null) {
261
+ // no convert method means this field is immutable
262
+ return success;
263
+ }
264
+ const fieldOverride = model.fieldOverrides[adapterPath];
265
+ const storedValue = convert(accessor.value, valuePath, model.value);
266
+ const value = fieldOverride != null
267
+ ? fieldOverride.value
268
+ : storedValue;
269
+ // TODO more nuanced comparison
270
+ const dirty = fieldOverride != null && fieldOverride.value !== storedValue;
271
+ const conversion = revert(value, valuePath, model.value);
272
+ switch (conversion.type) {
273
+ case FieldConversionResult.Failure:
274
+ model.errors[adapterPath] = conversion.error;
275
+ if (conversion.value != null && dirty) {
276
+ accessor.set(conversion.value[0]);
277
+ }
278
+ return false;
279
+ case FieldConversionResult.Success:
280
+ if (dirty) {
281
+ accessor.set(conversion.value);
282
+ }
283
+ delete model.errors[adapterPath];
284
+ return success;
285
+ default:
286
+ throw new UnreachableError(conversion);
287
+ }
288
+ }, true);
289
+ });
290
+ }
291
+ createModel(value) {
292
+ return new FormModel(this.typeDef, value, this.adapters);
293
+ }
294
+ }
295
+ export class FormModel {
296
+ typeDef;
297
+ adapters;
298
+ @observable.ref
299
+ accessor value;
300
+ @observable.shallow
301
+ accessor fieldOverrides;
302
+ @observable.shallow
303
+ accessor errors = {};
304
+ flattenedTypeDefs;
305
+ constructor(typeDef, value, adapters) {
306
+ this.typeDef = typeDef;
307
+ this.adapters = adapters;
308
+ this.value = mobxCopy(typeDef, value);
309
+ this.flattenedTypeDefs = flattenTypeDefsOf(typeDef);
310
+ // pre-populate field overrides for consistent behavior when default information is overwritten
311
+ // then returned to
312
+ this.fieldOverrides = flattenValueTypeTo(typeDef, this.value, () => { }, (_t, value, _setter, typePath, valuePath) => {
313
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
314
+ const adapter = this.adapters[typePath];
315
+ if (adapter == null) {
316
+ return;
317
+ }
318
+ const { convert, revert, } = adapter;
319
+ if (revert == null) {
320
+ // no need to store a temporary value if the value cannot be written back
321
+ return;
322
+ }
323
+ const displayValue = convert(value, valuePath, this.value);
324
+ return {
325
+ value: displayValue,
326
+ };
327
+ });
328
+ }
329
+ @computed
330
+ get fields() {
331
+ return new Proxy(this.knownFields, {
332
+ get: (target, prop) => {
333
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
334
+ const field = target[prop];
335
+ if (field != null) {
336
+ return field;
337
+ }
338
+ if (typeof prop === 'string') {
339
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
340
+ return this.maybeSynthesizeFieldByValuePath(prop);
341
+ }
342
+ },
343
+ });
344
+ }
345
+ @computed
346
+ get knownFields() {
347
+ return flattenValueTypeTo(this.typeDef, this.value, () => { },
348
+ // TODO swap these to valuePath, typePath in flatten
349
+ (_t, _v, _setter, typePath, valuePath) => {
350
+ return this.synthesizeFieldByPaths(
351
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
352
+ valuePath,
353
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
354
+ typePath);
355
+ });
356
+ }
357
+ maybeSynthesizeFieldByValuePath(valuePath) {
358
+ let typePath;
359
+ try {
360
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
361
+ typePath = valuePathToTypePath(this.typeDef,
362
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
363
+ valuePath, true);
364
+ }
365
+ catch (e) {
366
+ // TODO make jsonValuePathToTypePath return null in the event of an invalid
367
+ // value path instead of throwing an exception
368
+ // assume that the path was invalid
369
+ return;
370
+ }
371
+ return this.synthesizeFieldByPaths(valuePath, typePath);
372
+ }
373
+ synthesizeFieldByPaths(valuePath, typePath) {
374
+ const adapter = this.adapters[typePath];
375
+ if (adapter == null) {
376
+ // invalid path, which can happen
377
+ return;
378
+ }
379
+ const { convert, create, } = adapter;
380
+ const fieldOverride = this.fieldOverrides[valuePath];
381
+ const accessor = this.getAccessorForValuePath(valuePath);
382
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
383
+ const fieldTypeDef = this.flattenedTypeDefs[typePath];
384
+ const value = fieldOverride
385
+ ? fieldOverride.value
386
+ : convert(accessor != null
387
+ ? accessor.value
388
+ : fieldTypeDef != null
389
+ ? mobxCopy(fieldTypeDef,
390
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
391
+ create(valuePath, this.value))
392
+ // fake values can't be copied
393
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
394
+ : create(valuePath, this.value),
395
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
396
+ valuePath, this.value);
397
+ const error = this.errors[valuePath];
398
+ return {
399
+ value,
400
+ error,
401
+ // if we can't write it back, then we have to disable it
402
+ disabled: this.isDisabled(valuePath),
403
+ required: this.isRequired(valuePath),
404
+ };
405
+ }
406
+ getAccessorForValuePath(valuePath) {
407
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
408
+ return this.accessors[valuePath];
409
+ }
410
+ @computed
411
+ // should only be referenced internally, so loosely typed
412
+ get accessors() {
413
+ return flattenAccessorsOf(this.typeDef, this.value, (value) => {
414
+ this.value = mobxCopy(this.typeDef, value);
415
+ });
416
+ }
417
+ isDisabled(_valuePath) {
418
+ // TODO infer from types
419
+ return false;
420
+ }
421
+ isRequired(_valuePath) {
422
+ // TODO infer from types
423
+ return false;
424
+ }
425
+ }
@@ -0,0 +1,13 @@
1
+ const error = Symbol();
2
+ describe('FlattenedAdaptersOfFIelds', function () {
3
+ it('maps the converter types', function () {
4
+ expectTypeOf().toEqualTypeOf();
5
+ });
6
+ it('ignores extraneous types not listed in the fields', function () {
7
+ expectTypeOf().toEqualTypeOf();
8
+ });
9
+ it('handles multiple fields fields', function () {
10
+ expectTypeOf().toEqualTypeOf();
11
+ });
12
+ });
13
+ export {};
@@ -0,0 +1,16 @@
1
+ import { list, nullType, numberType, object, record, stringType, union, } from '@strictly/define';
2
+ describe('FlattenedListTypeDefsOf', function () {
3
+ it('filters lists types', function () {
4
+ const listTypeDef = list(numberType);
5
+ const recordTypeDef = record(stringType);
6
+ const objectTypeDef = object();
7
+ const unionTypeDef = union().add('0', numberType).add('1', nullType);
8
+ const typeDef = object()
9
+ .set('literal', numberType)
10
+ .set('list', listTypeDef)
11
+ .set('record', recordTypeDef)
12
+ .set('object', objectTypeDef)
13
+ .set('union', unionTypeDef);
14
+ expectTypeOf().toEqualTypeOf();
15
+ });
16
+ });
@@ -0,0 +1 @@
1
+ export {};