@strictly/react-form 0.0.1 → 0.0.2

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 (173) hide show
  1. package/.out/core/mobx/field_adapter.d.ts +7 -6
  2. package/.out/core/mobx/field_adapter_builder.d.ts +12 -13
  3. package/.out/core/mobx/field_adapter_builder.js +8 -12
  4. package/.out/core/mobx/field_adapters_of_values.d.ts +4 -0
  5. package/.out/core/mobx/flattened_adapters_of_fields.d.ts +2 -2
  6. package/.out/core/mobx/flattened_list_types_of_type.d.ts +8 -0
  7. package/.out/core/mobx/form_fields_of_field_adapters.d.ts +8 -0
  8. package/.out/core/mobx/form_presenter.d.ts +21 -24
  9. package/.out/core/mobx/form_presenter.js +64 -69
  10. package/.out/core/mobx/merge_field_adapters_with_two_way_converter.d.ts +13 -0
  11. package/.out/core/mobx/merge_field_adapters_with_two_way_converter.js +11 -0
  12. package/.out/core/mobx/merge_field_adapters_with_validators.d.ts +11 -0
  13. package/.out/core/mobx/merge_field_adapters_with_validators.js +45 -0
  14. package/.out/core/mobx/specs/fixtures.d.ts +7 -0
  15. package/.out/core/mobx/specs/fixtures.js +20 -0
  16. package/.out/core/mobx/specs/flattened_adapters_of_fields.tests.js +5 -2
  17. package/.out/core/mobx/specs/{flattened_list_type_defs_of.tests.js → flattened_list_types_of_types.tests.js} +7 -7
  18. package/.out/core/mobx/specs/form_presenter.tests.js +162 -60
  19. package/.out/core/mobx/specs/merge_field_adapters_with_two_way_converter.js +89 -0
  20. package/.out/core/mobx/specs/merge_field_adapters_with_validators.tests.js +172 -0
  21. package/.out/core/mobx/types.d.ts +2 -2
  22. package/.out/field_converters/chain_field_converter.d.ts +3 -3
  23. package/.out/field_converters/chain_field_converter.js +17 -12
  24. package/.out/field_converters/identity_converter.d.ts +3 -3
  25. package/.out/field_converters/identity_converter.js +10 -6
  26. package/.out/field_converters/integer_to_string_converter.d.ts +5 -4
  27. package/.out/field_converters/integer_to_string_converter.js +13 -6
  28. package/.out/field_converters/list_converter.d.ts +2 -2
  29. package/.out/field_converters/list_converter.js +6 -1
  30. package/.out/field_converters/maybe_identity_converter.d.ts +3 -3
  31. package/.out/field_converters/maybe_identity_converter.js +3 -1
  32. package/.out/field_converters/nullable_to_boolean_converter.d.ts +9 -8
  33. package/.out/field_converters/nullable_to_boolean_converter.js +13 -7
  34. package/.out/field_converters/select_value_type_converter.d.ts +20 -15
  35. package/.out/field_converters/select_value_type_converter.js +29 -14
  36. package/.out/field_converters/specs/chain_field_converter.tests.d.ts +1 -0
  37. package/.out/field_converters/specs/chain_field_converter.tests.js +251 -0
  38. package/.out/field_converters/trimming_string_converter.d.ts +3 -3
  39. package/.out/field_converters/trimming_string_converter.js +7 -3
  40. package/.out/field_converters/validating_converter.d.ts +3 -3
  41. package/.out/field_converters/validating_converter.js +7 -5
  42. package/.out/index.d.ts +9 -2
  43. package/.out/index.js +9 -2
  44. package/.out/mantine/create_checkbox.d.ts +2 -3
  45. package/.out/mantine/create_checkbox.js +6 -5
  46. package/.out/mantine/create_pill.js +2 -2
  47. package/.out/mantine/create_radio.js +1 -1
  48. package/.out/mantine/create_radio_group.d.ts +2 -3
  49. package/.out/mantine/create_radio_group.js +4 -3
  50. package/.out/mantine/create_text_input.d.ts +2 -3
  51. package/.out/mantine/create_text_input.js +6 -5
  52. package/.out/mantine/create_value_input.d.ts +2 -3
  53. package/.out/mantine/create_value_input.js +6 -5
  54. package/.out/mantine/error_renderer.d.ts +6 -0
  55. package/.out/mantine/error_renderer.js +5 -0
  56. package/.out/mantine/hooks.d.ts +9 -13
  57. package/.out/mantine/hooks.js +10 -15
  58. package/.out/mantine/specs/checkbox_hooks.stories.d.ts +7 -2
  59. package/.out/mantine/specs/checkbox_hooks.stories.js +33 -6
  60. package/.out/mantine/specs/list_hooks.stories.js +2 -2
  61. package/.out/mantine/specs/radio_group_hooks.stories.d.ts +7 -2
  62. package/.out/mantine/specs/radio_group_hooks.stories.js +33 -6
  63. package/.out/mantine/specs/select_hooks.stories.d.ts +8 -2
  64. package/.out/mantine/specs/select_hooks.stories.js +45 -8
  65. package/.out/mantine/specs/text_input_hooks.stories.d.ts +5 -1
  66. package/.out/mantine/specs/text_input_hooks.stories.js +23 -8
  67. package/.out/mantine/specs/value_input_hooks.stories.d.ts +7 -2
  68. package/.out/mantine/specs/value_input_hooks.stories.js +49 -15
  69. package/.out/mantine/types.d.ts +4 -1
  70. package/.out/tsconfig.tsbuildinfo +1 -1
  71. package/.out/types/error_of_field.d.ts +2 -0
  72. package/.out/types/error_of_field.js +1 -0
  73. package/.out/types/field.d.ts +1 -1
  74. package/.out/types/field_converters.d.ts +17 -10
  75. package/.out/types/field_converters.js +5 -5
  76. package/.out/types/flattened_validators_of_fields.d.ts +8 -0
  77. package/.out/types/flattened_validators_of_fields.js +1 -0
  78. package/.out/types/merge_validators.d.ts +7 -0
  79. package/.out/types/merge_validators.js +38 -0
  80. package/.out/types/specs/flattened_validators_of_fields.tests.d.ts +1 -0
  81. package/.out/types/specs/flattened_validators_of_fields.tests.js +16 -0
  82. package/.out/types/specs/merge_validators.tests.d.ts +1 -0
  83. package/.out/types/specs/merge_validators.tests.js +192 -0
  84. package/.out/util/partial.d.ts +11 -5
  85. package/.out/util/partial.js +55 -15
  86. package/.turbo/turbo-build.log +9 -9
  87. package/.turbo/turbo-check-types.log +1 -1
  88. package/.turbo/turbo-release$colon$exports.log +1 -1
  89. package/README.md +5 -1
  90. package/core/mobx/field_adapter.ts +15 -7
  91. package/core/mobx/field_adapter_builder.ts +39 -75
  92. package/core/mobx/field_adapters_of_values.ts +17 -0
  93. package/core/mobx/flattened_adapters_of_fields.ts +3 -3
  94. package/core/mobx/flattened_list_types_of_type.ts +17 -0
  95. package/core/mobx/form_fields_of_field_adapters.ts +16 -0
  96. package/core/mobx/form_presenter.ts +117 -104
  97. package/core/mobx/merge_field_adapters_with_two_way_converter.ts +68 -0
  98. package/core/mobx/merge_field_adapters_with_validators.ts +99 -0
  99. package/core/mobx/specs/fixtures.ts +73 -0
  100. package/core/mobx/specs/flattened_adapters_of_fields.tests.ts +23 -2
  101. package/core/mobx/specs/flattened_list_types_of_types.tests.ts +35 -0
  102. package/core/mobx/specs/form_presenter.tests.ts +248 -124
  103. package/core/mobx/specs/merge_field_adapters_with_two_way_converter.ts +140 -0
  104. package/core/mobx/specs/merge_field_adapters_with_validators.tests.ts +259 -0
  105. package/core/mobx/types.ts +3 -3
  106. package/dist/index.cjs +459 -211
  107. package/dist/index.d.cts +153 -111
  108. package/dist/index.d.ts +153 -111
  109. package/dist/index.js +453 -200
  110. package/field_converters/chain_field_converter.ts +37 -23
  111. package/field_converters/identity_converter.ts +14 -10
  112. package/field_converters/integer_to_string_converter.ts +15 -9
  113. package/field_converters/list_converter.ts +8 -3
  114. package/field_converters/maybe_identity_converter.ts +7 -4
  115. package/field_converters/nullable_to_boolean_converter.ts +23 -16
  116. package/field_converters/select_value_type_converter.ts +86 -26
  117. package/field_converters/specs/chain_field_converter.tests.ts +302 -0
  118. package/field_converters/trimming_string_converter.ts +11 -6
  119. package/field_converters/validating_converter.ts +21 -11
  120. package/index.ts +9 -2
  121. package/mantine/create_checkbox.tsx +15 -8
  122. package/mantine/create_list.tsx +1 -4
  123. package/mantine/create_pill.tsx +2 -2
  124. package/mantine/create_radio.tsx +1 -1
  125. package/mantine/create_radio_group.tsx +8 -6
  126. package/mantine/create_text_input.tsx +20 -8
  127. package/mantine/create_value_input.tsx +17 -8
  128. package/mantine/error_renderer.ts +15 -0
  129. package/mantine/hooks.tsx +25 -51
  130. package/mantine/specs/__snapshots__/checkbox_hooks.tests.tsx.snap +126 -0
  131. package/mantine/specs/__snapshots__/radio_group_hooks.tests.tsx.snap +356 -0
  132. package/mantine/specs/__snapshots__/select_hooks.tests.tsx.snap +208 -12
  133. package/mantine/specs/__snapshots__/text_input_hooks.tests.tsx.snap +45 -0
  134. package/mantine/specs/__snapshots__/value_input_hooks.tests.tsx.snap +194 -8
  135. package/mantine/specs/checkbox_hooks.stories.tsx +47 -7
  136. package/mantine/specs/list_hooks.stories.tsx +2 -2
  137. package/mantine/specs/radio_group_hooks.stories.tsx +47 -7
  138. package/mantine/specs/select_hooks.stories.tsx +55 -8
  139. package/mantine/specs/text_input_hooks.stories.tsx +32 -7
  140. package/mantine/specs/value_input_hooks.stories.tsx +57 -16
  141. package/mantine/types.ts +5 -1
  142. package/package.json +16 -4
  143. package/tsconfig.json +1 -0
  144. package/types/error_of_field.ts +3 -0
  145. package/types/field.ts +1 -1
  146. package/types/field_converters.ts +21 -10
  147. package/types/flattened_validators_of_fields.ts +34 -0
  148. package/types/merge_validators.ts +80 -0
  149. package/types/specs/error_type_of_field.tests.ts +2 -2
  150. package/types/specs/flattened_validators_of_fields.tests.ts +93 -0
  151. package/types/specs/merge_validators.tests.ts +267 -0
  152. package/util/partial.tsx +200 -16
  153. package/.out/core/mobx/flattened_list_type_defs_of.d.ts +0 -8
  154. package/.out/field_validators/minimum_string_length_field_validator.d.ts +0 -2
  155. package/.out/field_validators/minimum_string_length_field_validator.js +0 -8
  156. package/.out/types/error_type_of_field.d.ts +0 -2
  157. package/.out/types/field_validator.d.ts +0 -3
  158. package/.out/types/flattened_form_fields_of.d.ts +0 -9
  159. package/.out/types/specs/flattened_form_fields_of.tests.js +0 -13
  160. package/core/mobx/flattened_list_type_defs_of.ts +0 -17
  161. package/core/mobx/specs/flattened_list_type_defs_of.tests.ts +0 -35
  162. package/field_validators/minimum_string_length_field_validator.ts +0 -13
  163. package/mantine/specs/__snapshots__/check_box_hooks.tests.tsx.snap +0 -227
  164. package/types/error_type_of_field.ts +0 -3
  165. package/types/field_validator.ts +0 -7
  166. package/types/flattened_form_fields_of.ts +0 -16
  167. package/types/specs/flattened_form_fields_of.tests.ts +0 -43
  168. /package/.out/core/mobx/{flattened_list_type_defs_of.js → field_adapters_of_values.js} +0 -0
  169. /package/.out/core/mobx/{specs/flattened_list_type_defs_of.tests.d.ts → flattened_list_types_of_type.js} +0 -0
  170. /package/.out/{types/error_type_of_field.js → core/mobx/form_fields_of_field_adapters.js} +0 -0
  171. /package/.out/{types/field_validator.js → core/mobx/specs/flattened_list_types_of_types.tests.d.ts} +0 -0
  172. /package/.out/{types/flattened_form_fields_of.js → core/mobx/specs/merge_field_adapters_with_two_way_converter.d.ts} +0 -0
  173. /package/.out/{types/specs/flattened_form_fields_of.tests.d.ts → core/mobx/specs/merge_field_adapters_with_validators.tests.d.ts} +0 -0
@@ -15,6 +15,7 @@ import {
15
15
  } from '@storybook/react'
16
16
  import { type FormProps } from 'core/props'
17
17
  import { type SuppliedValueInputProps } from 'mantine/create_value_input'
18
+ import { type ErrorRenderer } from 'mantine/error_renderer'
18
19
  import { useMantineForm } from 'mantine/hooks'
19
20
  import {
20
21
  type ComponentType,
@@ -33,14 +34,17 @@ function Component<
33
34
  P extends StoryValueInputProps<V>,
34
35
  >({
35
36
  ValueInput,
36
- inputs,
37
+ ErrorRenderer,
38
+ inputProps,
37
39
  ...props
38
40
  }: FormProps<{
39
41
  $: Field<V, string>,
40
42
  }> & {
41
43
  ValueInput: ComponentType<P>,
42
44
  } & {
43
- inputs: P,
45
+ inputProps: P,
46
+ } & {
47
+ ErrorRenderer?: ErrorRenderer,
44
48
  }) {
45
49
  const form = useMantineForm(props)
46
50
  const ValueInputComponent = form.valueInput<'$', P>('$', ValueInput)
@@ -48,8 +52,9 @@ function Component<
48
52
  <ValueInputComponent
49
53
  {
50
54
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
51
- ...inputs as any
55
+ ...inputProps as any
52
56
  }
57
+ ErrorRenderer={ErrorRenderer}
53
58
  />
54
59
  )
55
60
  }
@@ -75,13 +80,13 @@ export const EmptyNumberInput: Story<number | string, NumberInputProps> = {
75
80
  args: {
76
81
  fields: {
77
82
  $: {
78
- disabled: false,
79
- required: true,
83
+ readonly: false,
84
+ required: false,
80
85
  value: '',
81
86
  },
82
87
  },
83
88
  ValueInput: NumberInput,
84
- inputs: {
89
+ inputProps: {
85
90
  label: NUMBER_INPUT_LABEL,
86
91
  },
87
92
  },
@@ -91,13 +96,49 @@ export const PopulatedNumberInput: Story<number | string, NumberInputProps> = {
91
96
  args: {
92
97
  fields: {
93
98
  $: {
94
- disabled: false,
99
+ readonly: false,
100
+ required: false,
101
+ value: 3,
102
+ },
103
+ },
104
+ ValueInput: NumberInput,
105
+ inputProps: {
106
+ label: NUMBER_INPUT_LABEL,
107
+ },
108
+ },
109
+ }
110
+
111
+ export const RequiredNumberInput: Story<number | string, NumberInputProps> = {
112
+ args: {
113
+ fields: {
114
+ $: {
115
+ readonly: false,
116
+ required: true,
117
+ value: 3,
118
+ },
119
+ },
120
+ ValueInput: NumberInput,
121
+ inputProps: {
122
+ label: NUMBER_INPUT_LABEL,
123
+ },
124
+ },
125
+ }
126
+
127
+ export const CustomErrorNumberInput: Story<number | string, NumberInputProps> = {
128
+ args: {
129
+ fields: {
130
+ $: {
131
+ readonly: false,
95
132
  required: false,
96
133
  value: 3,
134
+ error: 'an error',
97
135
  },
98
136
  },
99
137
  ValueInput: NumberInput,
100
- inputs: {
138
+ ErrorRenderer: function () {
139
+ return 'a custom error'
140
+ },
141
+ inputProps: {
101
142
  label: NUMBER_INPUT_LABEL,
102
143
  },
103
144
  },
@@ -107,13 +148,13 @@ export const DisabledNumberInput: Story<number | string, NumberInputProps> = {
107
148
  args: {
108
149
  fields: {
109
150
  $: {
110
- disabled: true,
151
+ readonly: true,
111
152
  required: false,
112
153
  value: 3,
113
154
  },
114
155
  },
115
156
  ValueInput: NumberInput,
116
- inputs: {
157
+ inputProps: {
117
158
  label: NUMBER_INPUT_LABEL,
118
159
  },
119
160
  },
@@ -123,13 +164,13 @@ export const AnSlider: Story<number, SliderProps> = {
123
164
  args: {
124
165
  fields: {
125
166
  $: {
126
- disabled: false,
167
+ readonly: false,
127
168
  required: false,
128
169
  value: 3,
129
170
  },
130
171
  },
131
172
  ValueInput: Slider,
132
- inputs: {
173
+ inputProps: {
133
174
  label: SLIDER_LABEL,
134
175
  min: 1,
135
176
  max: 10,
@@ -155,13 +196,13 @@ export const AnRating: Story<number, RatingProps> = {
155
196
  args: {
156
197
  fields: {
157
198
  $: {
158
- disabled: false,
199
+ readonly: false,
159
200
  required: false,
160
201
  value: 2,
161
202
  },
162
203
  },
163
204
  ValueInput: Rating,
164
- inputs: {},
205
+ inputProps: {},
165
206
  },
166
207
  }
167
208
 
@@ -169,13 +210,13 @@ export const AnJsonInput: Story<string, JsonInputProps> = {
169
210
  args: {
170
211
  fields: {
171
212
  $: {
172
- disabled: false,
213
+ readonly: false,
173
214
  required: false,
174
215
  value: '{}',
175
216
  },
176
217
  },
177
218
  ValueInput: JsonInput,
178
- inputs: {
219
+ inputProps: {
179
220
  rows: 8,
180
221
  },
181
222
  },
package/mantine/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type ComponentType } from 'react'
2
2
  import { type Fields } from 'types/field'
3
3
  import { type UnsafePartialComponent } from 'util/partial'
4
+ import { type ErrorRenderer } from './error_renderer'
4
5
 
5
6
  export type MantineForm<F extends Fields> = {
6
7
  fields: F,
@@ -10,4 +11,7 @@ export type MantineForm<F extends Fields> = {
10
11
  onFieldSubmit: ((this: void, key: keyof F) => boolean | void) | undefined,
11
12
  }
12
13
 
13
- export type MantineFieldComponent<T, P = T> = UnsafePartialComponent<ComponentType<P>, T>
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ export type MantineFieldComponent<T, P = T, E = any> = UnsafePartialComponent<ComponentType<P>, T, {
16
+ ErrorRenderer?: ErrorRenderer<E>,
17
+ }>
package/package.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "author": "Chris <chris.glover@gmail.com> (@madmaw)",
2
3
  "dependencies": {
3
4
  "@strictly/base": "*",
4
5
  "@strictly/define": "*",
@@ -6,6 +7,7 @@
6
7
  "mobx-react": "^9.1.1",
7
8
  "react": "^19.0.0"
8
9
  },
10
+ "description": "Types and utilities for creating React forms",
9
11
  "devDependencies": {
10
12
  "@babel/plugin-proposal-decorators": "^7.25.9",
11
13
  "@babel/plugin-transform-class-static-block": "^7.26.0",
@@ -18,13 +20,14 @@
18
20
  "@storybook/builder-vite": "^8.4.5",
19
21
  "@storybook/react": "^8.4.5",
20
22
  "@storybook/react-vite": "^8.4.5",
21
- "@storybook/test-runner": "^0.19.1",
23
+ "@storybook/test-runner": "^0.21.0",
22
24
  "@strictly/support-vite": "*",
23
25
  "@testing-library/dom": "^10.4.0",
24
26
  "@testing-library/react": "^16.0.1",
25
27
  "@types/react": "^18.3.12",
26
28
  "@types/react-dom": "^18.3.1",
27
29
  "@vitejs/plugin-react": "^4.3.3",
30
+ "concurrently": "^9.1.2",
28
31
  "jsdom": "^25.0.1",
29
32
  "react-dom": "^19.0.0",
30
33
  "resize-observer-polyfill": "^1.5.1",
@@ -33,6 +36,12 @@
33
36
  "vite": "^6.0.5",
34
37
  "vitest-matchmedia-mock": "^1.0.6"
35
38
  },
39
+ "homepage": "https://madmaw.github.io/strictly/form",
40
+ "keywords": [
41
+ "react",
42
+ "state management",
43
+ "types"
44
+ ],
36
45
  "license": "MIT",
37
46
  "name": "@strictly/react-form",
38
47
  "publishConfig": {
@@ -41,22 +50,25 @@
41
50
  "repository": {
42
51
  "directory": "packages/react-form",
43
52
  "type": "git",
44
- "url": "git+https://github.com/madmaw/de.git"
53
+ "url": "git+https://github.com/madmaw/strictly.git"
45
54
  },
46
55
  "scripts": {
47
56
  "build": "tsup",
48
57
  "check-types": "tsc",
49
- "clean": "del-cli dist",
58
+ "clean": "del-cli dist .out .turbo",
50
59
  "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --max-warnings=0",
51
60
  "lint:fix": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --fix",
52
61
  "release:exports": "json -f package.json -f package.exports.json --merge > package.release.json",
53
62
  "storybook": "storybook dev",
63
+ "storybook:build": "storybook build",
64
+ "storybook:test": "yarn playwright install && yarn storybook:build && concurrently -k -s first -n \"SB,TEST\" \"npx http-server storybook-static --port 6006 --silent\" \"npx wait-on tcp:127.0.0.1:6006 && yarn storybook:test:client\"",
65
+ "storybook:test:client": "test-storybook",
54
66
  "test": "vitest run",
55
67
  "test:update": "vitest run -u",
56
68
  "test:watch": "vitest"
57
69
  },
58
70
  "type": "module",
59
- "version": "0.0.1",
71
+ "version": "0.0.2",
60
72
  "exports": {
61
73
  ".": {
62
74
  "import": {
package/tsconfig.json CHANGED
@@ -12,6 +12,7 @@
12
12
  ".vitest/**/*.ts",
13
13
  "babel.config.cjs",
14
14
  "tsconfig.json"
15
+
15
16
  ],
16
17
  "references": [
17
18
  {
@@ -0,0 +1,3 @@
1
+ import { type Field } from './field'
2
+
3
+ export type ErrorOfField<F extends Field> = F extends Field<infer _V, infer E> ? E : never
package/types/field.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export type Field<V = any, E = any> = {
3
3
  readonly value: V,
4
4
  readonly error?: E | undefined,
5
- readonly disabled: boolean,
5
+ readonly readonly: boolean,
6
6
  readonly required: boolean,
7
7
  }
8
8
 
@@ -1,14 +1,15 @@
1
1
  import { type Maybe } from '@strictly/base'
2
2
 
3
- export enum FieldConversionResult {
3
+ export enum UnreliableFieldConversionType {
4
4
  Success = 0,
5
5
  Failure = 1,
6
6
  }
7
- export type FieldConversion<V, E> = {
8
- type: FieldConversionResult.Success,
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ export type UnreliableFieldConversion<V = any, E = any> = {
9
+ type: UnreliableFieldConversionType.Success,
9
10
  value: V,
10
11
  } | {
11
- type: FieldConversionResult.Failure,
12
+ type: UnreliableFieldConversionType.Failure,
12
13
  error: E,
13
14
  value: Maybe<V>,
14
15
  }
@@ -18,23 +19,33 @@ export type FieldConversion<V, E> = {
18
19
  // a `from` type of `string`, and a `to` type of `number`
19
20
 
20
21
  // TODO converter can also have the allowable value path as a parameter
21
- export type FieldConverter<
22
+ export type UnreliableFieldConverter<
22
23
  From,
23
24
  To,
24
25
  E,
25
26
  ValuePath extends string,
26
27
  Context,
27
28
  > = {
28
- (from: From, valuePath: ValuePath, context: Context): FieldConversion<To, E>,
29
+ (from: From, valuePath: ValuePath, context: Context): UnreliableFieldConversion<To, E>,
29
30
  }
30
31
 
31
- export type SafeFieldConverter<
32
+ export type Annotation = {
33
+ readonly required: boolean,
34
+ readonly readonly: boolean,
35
+ }
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ export type AnnotatedFieldConversion<V = any> = {
39
+ value: V,
40
+ } & Annotation
41
+
42
+ export type AnnotatedFieldConverter<
32
43
  From,
33
44
  To,
34
45
  ValuePath extends string,
35
46
  Context,
36
47
  > = {
37
- (from: From, valuePath: ValuePath, context: Context): To,
48
+ (from: From, valuePath: ValuePath, context: Context): AnnotatedFieldConversion<To>,
38
49
  }
39
50
 
40
51
  export type TwoWayFieldConverter<
@@ -44,9 +55,9 @@ export type TwoWayFieldConverter<
44
55
  ValuePath extends string,
45
56
  Context,
46
57
  > = {
47
- convert: SafeFieldConverter<From, To, ValuePath, Context>,
58
+ convert: AnnotatedFieldConverter<From, To, ValuePath, Context>,
48
59
 
49
- revert: FieldConverter<To, From, E, ValuePath, Context>,
60
+ revert: UnreliableFieldConverter<To, From, E, ValuePath, Context>,
50
61
  }
51
62
 
52
63
  export type FieldValueFactory<V, ValuePath extends string, Context> = {
@@ -0,0 +1,34 @@
1
+ import {
2
+ type ReadonlyTypeOfType,
3
+ type Type,
4
+ type Validator,
5
+ type ValueOfType,
6
+ } from '@strictly/define'
7
+ import {
8
+ type SimplifyDeep,
9
+ type ValueOf,
10
+ } from 'type-fest'
11
+ import { type Field } from 'types/field'
12
+
13
+ export type FlattenedValidatorsOfFields<
14
+ ValuePathsToTypePaths extends Readonly<Record<string, string>>,
15
+ FlattenedTypeDefs extends Partial<Readonly<Record<ValueOf<ValuePathsToTypePaths>, Type>>>,
16
+ FormFields extends Partial<Readonly<Record<keyof ValuePathsToTypePaths, Field>>>,
17
+ > = SimplifyDeep<{
18
+ readonly [
19
+ K in keyof ValuePathsToTypePaths as FormFields[K] extends Field ? ValuePathsToTypePaths[K] : never
20
+ ]: ValidatorOfField<
21
+ NonNullable<FormFields[K]>,
22
+ FlattenedTypeDefs[ValuePathsToTypePaths[K]],
23
+ K
24
+ >
25
+ }>
26
+
27
+ type ValidatorOfField<
28
+ F extends Field,
29
+ T extends Type | undefined,
30
+ ValuePath extends string | number | symbol,
31
+ > = ValuePath extends string ? F extends Field<infer V, infer E> ? undefined extends T ? Validator<V, E, ValuePath>
32
+ : Validator<ValueOfType<ReadonlyTypeOfType<NonNullable<T>>>, E, ValuePath>
33
+ : never
34
+ : never
@@ -0,0 +1,80 @@
1
+ import {
2
+ annotations,
3
+ validate,
4
+ type Validator,
5
+ } from '@strictly/define'
6
+ import { type Simplify } from 'type-fest'
7
+
8
+ export type MergedOfValidators<
9
+ Validators1 extends Partial<Readonly<Record<Keys, Validator>>>,
10
+ Validators2 extends Partial<Readonly<Record<Keys, Validator>>>,
11
+ Keys extends string = Extract<keyof Validators1 | keyof Validators2, string>,
12
+ > = Simplify<{
13
+ readonly [K in Keys]: undefined extends Validators1[K] ? undefined extends Validators2[K]
14
+ // validator1 and validator 2 are undefined
15
+ ? never
16
+ // validator 1 is undefined
17
+ : Validators2[K]
18
+ : undefined extends Validators2[K]
19
+ // validator 2 is undefined
20
+ ? Validators1[K]
21
+ // validator 2 and validator 2 are defined
22
+ : MergedOfValidator<NonNullable<Validators1[K]>, NonNullable<Validators2[K]>>
23
+ }>
24
+
25
+ export type MergedOfValidator<
26
+ Validator1 extends Validator,
27
+ Validator2 extends Validator,
28
+ > = Validator1 extends Validator<infer V, infer E1, infer P, infer C>
29
+ ? Validator2 extends Validator<V, infer E2, P, C> ? Validator<V, E1 | E2, P, C>
30
+ : never
31
+ : never
32
+
33
+ export function mergeValidators<
34
+ Validators1 extends Partial<Readonly<Record<Keys, Validator>>>,
35
+ Validators2 extends Partial<Readonly<Record<Keys, Validator>>>,
36
+ Keys extends string = Extract<keyof Validators1 | keyof Validators2, string>,
37
+ >(
38
+ validators1: Validators1,
39
+ validators2: Validators2,
40
+ ): MergedOfValidators<Validators1, Validators2, Keys> {
41
+ const validators = {
42
+ ...validators1,
43
+ ...validators2,
44
+ }
45
+ const keys1 = new Set(Object.keys(validators1))
46
+ const keys2 = new Set(Object.keys(validators2))
47
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
48
+ return Array.from(keys1.intersection(keys2)).reduce(
49
+ function (validators, key) {
50
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
51
+ const validator1 = validators1[key as keyof Validators1]
52
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
53
+ const validator2 = validators2[key as keyof Validators2]
54
+
55
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
56
+ validators[key as Keys] = {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ validate: function (value: any, valuePath: string, context: any) {
59
+ const error = validate(validator1!, value, valuePath, context)
60
+ if (error != null) {
61
+ return error
62
+ }
63
+ return validate(validator2!, value, valuePath, context)
64
+ },
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ annotations: function (valuePath: string, context: any) {
67
+ const annotations1 = annotations(validator1!, valuePath, context)
68
+ const annotations2 = annotations(validator2!, valuePath, context)
69
+ return {
70
+ readonly: annotations1.readonly || annotations2.readonly,
71
+ required: annotations1.required || annotations2.required,
72
+ }
73
+ },
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ } as any
76
+ return validators
77
+ },
78
+ validators,
79
+ ) as unknown as MergedOfValidators<Validators1, Validators2, Keys>
80
+ }
@@ -1,10 +1,10 @@
1
- import { type ErrorTypeOfField } from 'types/error_type_of_field'
1
+ import { type ErrorOfField } from 'types/error_of_field'
2
2
  import { type Field } from 'types/field'
3
3
 
4
4
  describe('ErrorTypeOfField', function () {
5
5
  it('equals expected type', function () {
6
6
  const e = Symbol()
7
7
  type E = typeof e
8
- expectTypeOf<ErrorTypeOfField<Field<unknown, E>>>().toEqualTypeOf<E>()
8
+ expectTypeOf<ErrorOfField<Field<unknown, E>>>().toEqualTypeOf<E>()
9
9
  })
10
10
  })
@@ -0,0 +1,93 @@
1
+ import {
2
+ type booleanType,
3
+ type numberType,
4
+ type Validator,
5
+ } from '@strictly/define'
6
+ import { type Field } from 'types/field'
7
+ import { type FlattenedValidatorsOfFields } from 'types/flattened_validators_of_fields'
8
+
9
+ const error = Symbol()
10
+ type Error = typeof error
11
+
12
+ describe('FlattenedValidatorsOfFields', function () {
13
+ it('maps the converter types', function () {
14
+ type Fields = {
15
+ a: Field<string, Error>,
16
+ }
17
+ type T = FlattenedValidatorsOfFields<
18
+ {
19
+ a: 'b',
20
+ },
21
+ {
22
+ b: typeof numberType,
23
+ },
24
+ Fields
25
+ >
26
+ expectTypeOf<T>().toEqualTypeOf<{
27
+ readonly b: Validator<number, Error, 'a'>,
28
+ }>()
29
+ })
30
+
31
+ it('ignores extraneous types not listed in the fields', function () {
32
+ type FormFields = {
33
+ a: Field<string, Error>,
34
+ }
35
+ type T = FlattenedValidatorsOfFields<
36
+ {
37
+ a: 'b',
38
+ c: 'd',
39
+ },
40
+ {
41
+ b: typeof numberType,
42
+ d: typeof booleanType,
43
+ },
44
+ FormFields
45
+ >
46
+ expectTypeOf<T>().toEqualTypeOf<{
47
+ readonly b: Validator<number, Error, 'a'>,
48
+ }>()
49
+ })
50
+
51
+ it('handles multiple fields', function () {
52
+ type FormFields = {
53
+ a: Field<string, Error>,
54
+ c: Field<boolean, never>,
55
+ }
56
+ type T = FlattenedValidatorsOfFields<
57
+ {
58
+ a: 'b',
59
+ c: 'd',
60
+ },
61
+ {
62
+ b: typeof numberType,
63
+ d: typeof booleanType,
64
+ },
65
+ FormFields
66
+ >
67
+ expectTypeOf<T>().toEqualTypeOf<{
68
+ readonly b: Validator<number, Error, 'a'>,
69
+ readonly d: Validator<boolean, never, 'c'>,
70
+ }>()
71
+ })
72
+
73
+ it('allows synthesized fields', function () {
74
+ type FormFields = {
75
+ a: Field<string, Error>,
76
+ c: Field<number, never>,
77
+ }
78
+ type T = FlattenedValidatorsOfFields<
79
+ {
80
+ a: 'b',
81
+ c: 'd',
82
+ },
83
+ {
84
+ b: typeof numberType,
85
+ },
86
+ FormFields
87
+ >
88
+ expectTypeOf<T>().toEqualTypeOf<{
89
+ readonly b: Validator<number, Error, 'a'>,
90
+ readonly d: Validator<number, never, 'c'>,
91
+ }>()
92
+ })
93
+ })