@strictly/react-form 0.0.6 → 0.0.8

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 (77) hide show
  1. package/.out/core/mobx/hooks.d.ts +5 -1
  2. package/.out/core/mobx/hooks.js +13 -3
  3. package/.out/core/mobx/specs/form_presenter.tests.js +3 -6
  4. package/.out/core/mobx/specs/{merge_field_adapters_with_two_way_converter.js → merge_field_adapters_with_two_way_converter.tests.js} +15 -16
  5. package/.out/core/mobx/specs/sub_form_field_adapters.tests.js +127 -12
  6. package/.out/core/mobx/sub_form_field_adapters.d.ts +6 -4
  7. package/.out/core/mobx/sub_form_field_adapters.js +28 -4
  8. package/.out/field_converters/nullable_to_boolean_converter.d.ts +2 -2
  9. package/.out/mantine/create_checkbox.js +1 -0
  10. package/.out/mantine/create_fields_view.d.ts +1 -1
  11. package/.out/mantine/create_fields_view.js +4 -4
  12. package/.out/mantine/create_form.d.ts +1 -1
  13. package/.out/mantine/create_list.d.ts +1 -1
  14. package/.out/mantine/create_pill.d.ts +1 -1
  15. package/.out/mantine/create_radio.d.ts +1 -1
  16. package/.out/mantine/create_radio_group.js +1 -0
  17. package/.out/mantine/create_text_input.js +7 -2
  18. package/.out/mantine/create_value_input.js +1 -0
  19. package/.out/mantine/error_renderer.d.ts +1 -1
  20. package/.out/mantine/error_renderer.js +1 -1
  21. package/.out/mantine/hooks.d.ts +9 -9
  22. package/.out/mantine/specs/checkbox_hooks.stories.d.ts +2 -6
  23. package/.out/mantine/specs/checkbox_hooks.stories.js +4 -16
  24. package/.out/mantine/specs/fields_view_hooks.stories.d.ts +1 -1
  25. package/.out/mantine/specs/fields_view_hooks.stories.js +6 -3
  26. package/.out/mantine/specs/form_hooks.stories.d.ts +2 -2
  27. package/.out/mantine/specs/form_hooks.stories.js +4 -1
  28. package/.out/mantine/specs/radio_group_hooks.stories.d.ts +2 -6
  29. package/.out/mantine/specs/radio_group_hooks.stories.js +5 -17
  30. package/.out/mantine/specs/select_hooks.stories.d.ts +2 -6
  31. package/.out/mantine/specs/select_hooks.stories.js +4 -16
  32. package/.out/mantine/specs/text_input_hooks.stories.d.ts +2 -5
  33. package/.out/mantine/specs/text_input_hooks.stories.js +5 -5
  34. package/.out/mantine/specs/value_input_hooks.stories.d.ts +2 -5
  35. package/.out/mantine/specs/value_input_hooks.stories.js +5 -5
  36. package/.out/mantine/types.d.ts +4 -2
  37. package/.out/tsconfig.tsbuildinfo +1 -1
  38. package/.turbo/turbo-build.log +8 -8
  39. package/.turbo/turbo-check-types.log +1 -1
  40. package/.turbo/turbo-release$colon$exports.log +1 -1
  41. package/core/mobx/hooks.ts +24 -6
  42. package/core/mobx/specs/form_presenter.tests.ts +6 -6
  43. package/core/mobx/specs/{merge_field_adapters_with_two_way_converter.ts → merge_field_adapters_with_two_way_converter.tests.ts} +16 -16
  44. package/core/mobx/specs/sub_form_field_adapters.tests.ts +193 -17
  45. package/core/mobx/sub_form_field_adapters.ts +74 -11
  46. package/dist/index.cjs +77 -32
  47. package/dist/index.d.cts +25 -18
  48. package/dist/index.d.ts +25 -18
  49. package/dist/index.js +68 -21
  50. package/field_converters/nullable_to_boolean_converter.ts +2 -3
  51. package/mantine/create_checkbox.tsx +2 -1
  52. package/mantine/create_fields_view.tsx +17 -14
  53. package/mantine/create_form.tsx +2 -2
  54. package/mantine/create_list.tsx +1 -1
  55. package/mantine/create_pill.tsx +1 -1
  56. package/mantine/create_radio.tsx +1 -1
  57. package/mantine/create_radio_group.tsx +6 -2
  58. package/mantine/create_text_input.tsx +9 -3
  59. package/mantine/create_value_input.tsx +2 -1
  60. package/mantine/error_renderer.ts +1 -1
  61. package/mantine/hooks.tsx +19 -14
  62. package/mantine/specs/__snapshots__/checkbox_hooks.tests.tsx.snap +1 -64
  63. package/mantine/specs/__snapshots__/fields_view_hooks.tests.tsx.snap +52 -52
  64. package/mantine/specs/__snapshots__/radio_group_hooks.tests.tsx.snap +1 -179
  65. package/mantine/specs/__snapshots__/select_hooks.tests.tsx.snap +1 -83
  66. package/mantine/specs/__snapshots__/text_input_hooks.tests.tsx.snap +27 -27
  67. package/mantine/specs/__snapshots__/value_input_hooks.tests.tsx.snap +31 -31
  68. package/mantine/specs/checkbox_hooks.stories.tsx +5 -21
  69. package/mantine/specs/fields_view_hooks.stories.tsx +16 -4
  70. package/mantine/specs/form_hooks.stories.tsx +10 -3
  71. package/mantine/specs/radio_group_hooks.stories.tsx +6 -20
  72. package/mantine/specs/select_hooks.stories.tsx +5 -21
  73. package/mantine/specs/text_input_hooks.stories.tsx +5 -8
  74. package/mantine/specs/value_input_hooks.stories.tsx +5 -8
  75. package/mantine/types.ts +7 -3
  76. package/package.json +2 -1
  77. /package/.out/core/mobx/specs/{merge_field_adapters_with_two_way_converter.d.ts → merge_field_adapters_with_two_way_converter.tests.d.ts} +0 -0
@@ -7,12 +7,12 @@ $ tsup
7
7
  CLI Target: esnext
8
8
  CJS Build start
9
9
  ESM Build start
10
- CJS dist/index.cjs 49.29 KB
11
- CJS ⚡️ Build success in 106ms
12
- ESM dist/index.js 45.41 KB
13
- ESM ⚡️ Build success in 106ms
10
+ CJS dist/index.cjs 50.73 KB
11
+ CJS ⚡️ Build success in 112ms
12
+ ESM dist/index.js 46.84 KB
13
+ ESM ⚡️ Build success in 113ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 10312ms
16
- DTS dist/index.d.cts 35.93 KB
17
- DTS dist/index.d.ts 35.93 KB
18
- Done in 11.43s.
15
+ DTS ⚡️ Build success in 9662ms
16
+ DTS dist/index.d.cts 36.94 KB
17
+ DTS dist/index.d.ts 36.94 KB
18
+ Done in 10.73s.
@@ -1,3 +1,3 @@
1
1
  yarn run v1.22.22
2
2
  $ tsc
3
- Done in 8.11s.
3
+ Done in 7.38s.
@@ -1,3 +1,3 @@
1
1
  yarn run v1.22.22
2
2
  $ json -f package.json -f package.exports.json --merge > package.release.json
3
- Done in 0.11s.
3
+ Done in 0.13s.
@@ -25,12 +25,16 @@ type ModelOfPresenter<P extends FormPresenter<any, any, any, any>> = ReturnType<
25
25
  export function useDefaultMobxFormHooks<P extends FormPresenter<any, any, any, any>>(
26
26
  presenter: P,
27
27
  value: ValueOfPresenter<P>,
28
- onValidSubmit?: <Path extends ValuePathsOfPresenter<P>>(
29
- model: ModelOfPresenter<P>,
30
- valuePath: Path,
31
- ) => void,
28
+ {
29
+ onValidFieldSubmit,
30
+ onValidFormSubmit,
31
+ }: {
32
+ onValidFieldSubmit?: <Path extends ValuePathsOfPresenter<P>>(model: ModelOfPresenter<P>, valuePath: Path) => void,
33
+ onValidFormSubmit?: (model: ModelOfPresenter<P>, value: ValueOfPresenter<P>) => void,
34
+ },
32
35
  ): {
33
36
  model: ModelOfPresenter<P>,
37
+ onFormSubmit?: (value: ValueOfPresenter<P>) => void,
34
38
  } & Omit<FieldsViewProps<ModelOfPresenter<P>['fields']>, 'fields'> {
35
39
  const model = useMemo(function () {
36
40
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -57,14 +61,14 @@ export function useDefaultMobxFormHooks<P extends FormPresenter<any, any, any, a
57
61
  const onFieldSubmit = useCallback(
58
62
  function<Path extends ValuePathsOfPresenter<P>> (valuePath: Path) {
59
63
  if (presenter.validateField(model, valuePath)) {
60
- onValidSubmit?.(model, valuePath)
64
+ onValidFieldSubmit?.(model, valuePath)
61
65
  }
62
66
  return false
63
67
  },
64
68
  [
65
69
  presenter,
66
70
  model,
67
- onValidSubmit,
71
+ onValidFieldSubmit,
68
72
  ],
69
73
  )
70
74
 
@@ -85,10 +89,24 @@ export function useDefaultMobxFormHooks<P extends FormPresenter<any, any, any, a
85
89
  ],
86
90
  )
87
91
 
92
+ const onFormSubmit = useCallback(
93
+ function () {
94
+ if (presenter.validateAll(model)) {
95
+ onValidFormSubmit?.(model, model.value)
96
+ }
97
+ },
98
+ [
99
+ presenter,
100
+ model,
101
+ onValidFormSubmit,
102
+ ],
103
+ )
104
+
88
105
  return {
89
106
  model,
90
107
  onFieldValueChange,
91
108
  onFieldSubmit,
92
109
  onFieldBlur,
110
+ onFormSubmit,
93
111
  }
94
112
  }
@@ -85,14 +85,14 @@ describe('all', function () {
85
85
  ValueOfType<typeof typeDef>
86
86
  >
87
87
  >
88
- let t: Partial<{
88
+ type C = Partial<{
89
89
  readonly $: ConvenientFieldAdapter<Readonly<Record<'a' | 'b', number>>, ValueOfType<typeof typeDef>>,
90
90
  readonly ['$.a']: ConvenientFieldAdapter<number, ValueOfType<typeof typeDef>>,
91
91
  readonly ['$.b']: ConvenientFieldAdapter<number, ValueOfType<typeof typeDef>>,
92
92
  }>
93
93
 
94
94
  it('equals expected type', function () {
95
- expectTypeOf(t).toEqualTypeOf<T>()
95
+ expectTypeOf<C>().toEqualTypeOf<T>()
96
96
  })
97
97
  })
98
98
 
@@ -104,13 +104,13 @@ describe('all', function () {
104
104
  FlattenedValuesOfType<typeof typeDef>,
105
105
  ValueOfType<typeof typeDef>
106
106
  >
107
- let t: Partial<{
107
+ type C = Partial<{
108
108
  readonly $: ConvenientFieldAdapter<{ readonly x: string, readonly y: boolean }, ValueOfType<typeof typeDef>>,
109
109
  readonly ['$.x']: ConvenientFieldAdapter<string, ValueOfType<typeof typeDef>>,
110
110
  readonly ['$.y']: ConvenientFieldAdapter<boolean, ValueOfType<typeof typeDef>>,
111
111
  }>
112
112
  it('equals expected type', function () {
113
- expectTypeOf(t).toEqualTypeOf<T>()
113
+ expectTypeOf<C>().toEqualTypeOf<T>()
114
114
  })
115
115
 
116
116
  it('matches representative adapters', function () {
@@ -147,12 +147,12 @@ describe('all', function () {
147
147
  A,
148
148
  typeof valuePathsToTypePaths
149
149
  >
150
- let t: {
150
+ type C = {
151
151
  readonly '$.a': A['$.x'],
152
152
  readonly '$.b': A['$.y'],
153
153
  }
154
154
  it('equals expected type', function () {
155
- expectTypeOf(t).toEqualTypeOf<T>()
155
+ expectTypeOf<C>().toEqualTypeOf<T>()
156
156
  })
157
157
  })
158
158
  })
@@ -33,14 +33,14 @@ describe('MergedOfFieldAdapterWithTwoWayConverter', function () {
33
33
  }
34
34
  type M = MergedOfFieldAdaptersWithTwoWayConverter<T, typeof error4, typeof context>
35
35
 
36
- let m: {
36
+ type C = {
37
37
  readonly x: FieldAdapter<boolean, string, typeof error1 | typeof error4, 'x', typeof context>,
38
38
  readonly y: FieldAdapter<number, boolean, typeof error2 | typeof error4, 'y', typeof context>,
39
39
  readonly z: FieldAdapter<string, number, typeof error3 | typeof error4, 'z', typeof context>,
40
40
  }
41
41
 
42
42
  it('merges the errors', function () {
43
- expectTypeOf<M>().toEqualTypeOf(m)
43
+ expectTypeOf<M>().toEqualTypeOf<C>()
44
44
  })
45
45
  })
46
46
 
@@ -117,23 +117,23 @@ describe('mergeFieldAdaptersWithTwoWayConverter', function () {
117
117
  beforeEach(function () {
118
118
  result = merged.booleanAdapter.revert!(true, 'booleanAdapter', context)
119
119
  })
120
- })
121
120
 
122
- it('returns the same value on revert', function () {
123
- expect(result).toEqual(expect.objectContaining({
124
- value: true,
125
- type: UnreliableFieldConversionType.Success,
126
- }))
127
- })
121
+ it('returns the same value on revert', function () {
122
+ expect(result).toEqual(expect.objectContaining({
123
+ value: true,
124
+ type: UnreliableFieldConversionType.Success,
125
+ }))
126
+ })
128
127
 
129
- it('calls the mocked converter', function () {
130
- expect(converter.revert).toHaveBeenCalledOnce()
131
- expect(converter.revert).toHaveBeenCalledWith(true, 'booleanAdapter', context)
132
- })
128
+ it('calls the mocked converter', function () {
129
+ expect(converter.revert).toHaveBeenCalledOnce()
130
+ expect(converter.revert).toHaveBeenCalledWith(true, 'booleanAdapter', context)
131
+ })
133
132
 
134
- it('calls the mocked adapter', function () {
135
- expect(booleanAdapter.revert).toHaveBeenCalledOnce()
136
- expect(booleanAdapter.revert).toHaveBeenCalledWith(true, 'booleanAdapter', context)
133
+ it('calls the mocked adapter', function () {
134
+ expect(booleanAdapter.revert).toHaveBeenCalledOnce()
135
+ expect(booleanAdapter.revert).toHaveBeenCalledWith(true, 'booleanAdapter', context)
136
+ })
137
137
  })
138
138
  })
139
139
  })
@@ -1,15 +1,26 @@
1
+ import {
2
+ list,
3
+ numberType,
4
+ object,
5
+ stringType,
6
+ } from '@strictly/define'
1
7
  import { type FieldAdapter } from 'core/mobx/field_adapter'
2
8
  import {
3
9
  subFormFieldAdapters,
4
10
  } from 'core/mobx/sub_form_field_adapters'
5
- import { mockDeep } from 'vitest-mock-extended'
11
+ import { UnreliableFieldConversionType } from 'types/field_converters'
12
+ import {
13
+ mockDeep,
14
+ mockReset,
15
+ } from 'vitest-mock-extended'
6
16
 
7
17
  describe('subFormFieldAdapters', () => {
8
- const fieldAdapter1: FieldAdapter<string, boolean> = mockDeep()
9
- const fieldAdapter2: FieldAdapter<number, boolean> = mockDeep()
10
-
11
18
  describe('empty value', () => {
12
- const adapters = subFormFieldAdapters({}, '$.a')
19
+ const adapters = subFormFieldAdapters(
20
+ {},
21
+ '$.a',
22
+ stringType,
23
+ )
13
24
 
14
25
  it('equals expected type', () => {
15
26
  expectTypeOf(adapters).toEqualTypeOf<{}>()
@@ -21,29 +32,102 @@ describe('subFormFieldAdapters', () => {
21
32
  })
22
33
 
23
34
  describe('single adapter', () => {
24
- const adapters = subFormFieldAdapters({
35
+ const mockedFieldAdapter1 = mockDeep<Required<FieldAdapter<string, boolean, number, '$', string>>>()
36
+ const fieldAdapter1: FieldAdapter<string, boolean, number, '$', string> = mockedFieldAdapter1
37
+
38
+ const type = object().field('a', stringType)
39
+ const subAdapters = {
25
40
  $: fieldAdapter1,
26
- }, '$.a')
41
+ } as const
42
+ const adapters = subFormFieldAdapters<
43
+ typeof subAdapters,
44
+ '$.a',
45
+ { '$.a': '$.a' },
46
+ typeof type
47
+ >(
48
+ subAdapters,
49
+ '$.a',
50
+ type,
51
+ )
52
+
53
+ beforeEach(() => {
54
+ mockReset(mockedFieldAdapter1)
55
+ })
27
56
 
28
57
  it('equals expected type', () => {
29
- expectTypeOf(adapters).toEqualTypeOf<{
30
- '$.a': FieldAdapter<string, boolean>,
58
+ // TODO toEqualTypeOf (cannot reason about revert optionality, seems to be a TS issue as they
59
+ // are both optional AFAICT)
60
+ expectTypeOf(adapters).toMatchTypeOf<{
61
+ '$.a': FieldAdapter<string, boolean, number, '$.a', {
62
+ a: string,
63
+ }>,
31
64
  }>()
32
65
  })
33
66
 
34
67
  it('equals expected value', () => {
35
- expect(adapters).toEqual({ '$.a': fieldAdapter1 })
68
+ expect(adapters).toEqual({ '$.a': expect.anything() })
69
+ })
70
+
71
+ it('calls convert with the correct paths and values', () => {
72
+ const mockedReturnedValue = {
73
+ value: false,
74
+ required: false,
75
+ readonly: false,
76
+ }
77
+ mockedFieldAdapter1.convert.mockReturnValue(mockedReturnedValue)
78
+
79
+ const returnedValue = adapters['$.a'].convert('x', '$.a', { a: 'y' })
80
+ expect(fieldAdapter1.convert).toHaveBeenCalledWith('x', '$', 'y')
81
+ expect(returnedValue).toEqual(mockedReturnedValue)
82
+ })
83
+
84
+ it('calls revert with the correct paths and values', () => {
85
+ const mockedReturnedValue = {
86
+ type: UnreliableFieldConversionType.Success,
87
+ value: 'ok',
88
+ } as const
89
+ mockedFieldAdapter1.revert.mockReturnValue(mockedReturnedValue)
90
+
91
+ const returnedValue = adapters['$.a'].revert?.(true, '$.a', { a: 'y' })
92
+ expect(fieldAdapter1.revert).toHaveBeenCalledWith(true, '$', 'y')
93
+ expect(returnedValue).toEqual(mockedReturnedValue)
94
+ })
95
+
96
+ it('calls create with the correct paths and values', () => {
97
+ const mockedReturnedValue = 'x'
98
+ mockedFieldAdapter1.create.mockReturnValue(mockedReturnedValue)
99
+
100
+ const returnedValue = adapters['$.a'].create('$.a', { a: 'y' })
101
+ expect(fieldAdapter1.create).toHaveBeenCalledWith('$', 'y')
102
+ expect(returnedValue).toEqual(mockedReturnedValue)
36
103
  })
37
104
  })
38
105
 
39
106
  describe('multiple adapters', () => {
40
- const adapters = subFormFieldAdapters({
41
- '$.x': fieldAdapter1,
42
- '$.y': fieldAdapter2,
43
- }, '$.a')
107
+ const mockedFieldAdapter1 = mockDeep<Required<FieldAdapter<string, boolean>>>()
108
+ const fieldAdapter1: FieldAdapter<string, boolean, number> = mockedFieldAdapter1
109
+ const mockedFieldAdapter2 = mockDeep<FieldAdapter<number, boolean>>()
110
+ const fieldAdapter2: FieldAdapter<number, boolean> = mockedFieldAdapter2
111
+
112
+ const type = object()
113
+ .field('a', object().field('x', stringType).field('y', numberType))
114
+ const adapters = subFormFieldAdapters(
115
+ {
116
+ '$.x': fieldAdapter1,
117
+ '$.y': fieldAdapter2,
118
+ },
119
+ '$.a',
120
+ type,
121
+ )
122
+
123
+ beforeEach(() => {
124
+ mockReset(mockedFieldAdapter1)
125
+ mockReset(mockedFieldAdapter2)
126
+ })
44
127
 
45
128
  it('equals expected type', () => {
46
- expectTypeOf(adapters).toEqualTypeOf<{
129
+ // TODO toEqualTypeOf (seems to be a TS error)
130
+ expectTypeOf(adapters).toMatchTypeOf<{
47
131
  '$.a.x': FieldAdapter<string, boolean>,
48
132
  '$.a.y': FieldAdapter<number, boolean>,
49
133
  }>()
@@ -51,8 +135,100 @@ describe('subFormFieldAdapters', () => {
51
135
 
52
136
  it('equals expected value', () => {
53
137
  expect(adapters).toEqual({
54
- '$.a.x': fieldAdapter1,
55
- '$.a.y': fieldAdapter2,
138
+ '$.a.x': expect.anything(),
139
+ '$.a.y': expect.anything(),
140
+ })
141
+ })
142
+
143
+ describe('calls convert with correct paths and values', () => {
144
+ const subContext = {
145
+ x: 'a',
146
+ y: 1,
147
+ } as const
148
+ const context = {
149
+ a: subContext,
150
+ }
151
+
152
+ it('calls $.a.x', () => {
153
+ const mockedReturnedValue = {
154
+ value: true,
155
+ readonly: true,
156
+ required: false,
157
+ }
158
+ mockedFieldAdapter1.convert.mockReturnValue(mockedReturnedValue)
159
+
160
+ const returnedValue = adapters['$.a.x'].convert('b', '$.a.x', context)
161
+ expect(fieldAdapter1.convert).toHaveBeenCalledWith('b', '$.x', subContext)
162
+ expect(returnedValue).toEqual(mockedReturnedValue)
163
+ })
164
+
165
+ it('calls $.a.y', () => {
166
+ const mockedReturnedValue = {
167
+ value: false,
168
+ readonly: false,
169
+ required: false,
170
+ }
171
+ mockedFieldAdapter2.convert.mockReturnValue(mockedReturnedValue)
172
+
173
+ const returnedValue = adapters['$.a.y'].convert(2, '$.a.y', context)
174
+ expect(fieldAdapter2.convert).toHaveBeenCalledWith(2, '$.y', subContext)
175
+ expect(returnedValue).toEqual(mockedReturnedValue)
176
+ })
177
+ })
178
+ })
179
+
180
+ describe('list adapter', () => {
181
+ const mockedFieldAdapter1 = mockDeep<Required<FieldAdapter<string, boolean, number, '$', string>>>()
182
+ const fieldAdapter1: FieldAdapter<string, boolean, number, '$', string> = mockedFieldAdapter1
183
+ const type = list(stringType)
184
+ const subAdapters = {
185
+ $: fieldAdapter1,
186
+ }
187
+ const adapters = subFormFieldAdapters<
188
+ typeof subAdapters,
189
+ '$.*',
190
+ {
191
+ '$.*': `$.${number}`,
192
+ },
193
+ typeof type
194
+ >(
195
+ subAdapters,
196
+ '$.*',
197
+ type,
198
+ )
199
+
200
+ beforeEach(() => {
201
+ mockReset(mockedFieldAdapter1)
202
+ })
203
+
204
+ it('equals expected type', () => {
205
+ // TODO toEqualTypeOf (seems to be a TS error)
206
+ expectTypeOf(adapters).toMatchTypeOf<{
207
+ '$.*': FieldAdapter<string, boolean, number, `$.${number}`, string[]>,
208
+ }>()
209
+ })
210
+
211
+ it('equals expected value', () => {
212
+ expect(adapters).toEqual({
213
+ '$.*': expect.anything(),
214
+ })
215
+ })
216
+
217
+ describe('calls convert with correct paths and values', () => {
218
+ const subContext = 'a'
219
+ const context = [subContext]
220
+
221
+ it('calls $.*', () => {
222
+ const mockedReturnedValue = {
223
+ value: false,
224
+ readonly: false,
225
+ required: false,
226
+ }
227
+ mockedFieldAdapter1.convert.mockReturnValue(mockedReturnedValue)
228
+
229
+ const returnedValue = adapters['$.*'].convert('b', '$.0', context)
230
+ expect(fieldAdapter1.convert).toHaveBeenCalledWith('b', '$', subContext)
231
+ expect(returnedValue).toEqual(mockedReturnedValue)
56
232
  })
57
233
  })
58
234
  })
@@ -1,21 +1,84 @@
1
1
  import { type StringConcatOf } from '@strictly/base'
2
- import { type FieldAdapter } from './field_adapter'
2
+ import {
3
+ flattenValuesOfType,
4
+ type Type,
5
+ type ValueOfType,
6
+ } from '@strictly/define'
7
+ import {
8
+ type ErrorOfFieldAdapter,
9
+ type FieldAdapter,
10
+ type FromOfFieldAdapter,
11
+ type ToOfFieldAdapter,
12
+ type ValuePathOfFieldAdapter,
13
+ } from './field_adapter'
3
14
 
4
- type SubFormFieldAdapters<SubAdapters extends Record<string, FieldAdapter>, P extends string> = {
5
- [K in keyof SubAdapters as K extends StringConcatOf<'$', infer S> ? `${P}${S}` : never]: SubAdapters[K]
15
+ type SubFormFieldAdapter<F extends FieldAdapter, ValuePath extends string, Context> = FieldAdapter<
16
+ FromOfFieldAdapter<F>,
17
+ ToOfFieldAdapter<F>,
18
+ ErrorOfFieldAdapter<F>,
19
+ ValuePathOfFieldAdapter<F> extends StringConcatOf<'$', infer ValuePathSuffix> ? `${ValuePath}${ValuePathSuffix}`
20
+ // assume string (they don't care about the value path as a type) if there the path doesn't have a $ prefix
21
+ : string,
22
+ Context
23
+ >
24
+
25
+ type SubFormFieldAdapters<
26
+ SubAdapters extends Record<string, FieldAdapter>,
27
+ TypePath extends string,
28
+ ValuePath extends string,
29
+ Context,
30
+ > = {
31
+ [
32
+ K in keyof SubAdapters as K extends StringConcatOf<'$', infer TypePathSuffix> ? `${TypePath}${TypePathSuffix}`
33
+ : never
34
+ ]: SubFormFieldAdapter<
35
+ SubAdapters[K],
36
+ ValuePath,
37
+ Context
38
+ >
6
39
  }
7
40
 
8
- export function subFormFieldAdapters<SubAdapters extends Record<string, FieldAdapter>, P extends string>(
41
+ export function subFormFieldAdapters<
42
+ SubAdapters extends Record<string, FieldAdapter>,
43
+ TypePath extends string,
44
+ TypePathsToValuePaths extends Record<TypePath, string>,
45
+ ContextType extends Type,
46
+ >(
9
47
  subAdapters: SubAdapters,
10
- prefix: P,
11
- ): SubFormFieldAdapters<SubAdapters, P> {
48
+ parentTypePath: TypePath,
49
+ contextType: ContextType,
50
+ ): SubFormFieldAdapters<SubAdapters, TypePath, TypePathsToValuePaths[TypePath], ValueOfType<ContextType>> {
51
+ // assume the number of '.' in the type path will correspond to the number of '.' in the value path
52
+ const dotCount = parentTypePath.split('.').length
53
+ function getSubValuePathAndContext(valuePath: string, context: ValueOfType<ContextType>) {
54
+ const parentValuePath = valuePath.split('.').slice(0, dotCount).join('.')
55
+ const subValuePath = valuePath.replace(parentValuePath, '$')
56
+ const subContext = flattenValuesOfType(contextType, context)[parentValuePath]
57
+ return [
58
+ subValuePath,
59
+ subContext,
60
+ ] as const
61
+ }
62
+
12
63
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
13
64
  return Object.entries(subAdapters).reduce<Record<string, FieldAdapter>>((acc, [
14
- subKey,
15
- subValue,
65
+ subTypePath,
66
+ subAdapter,
16
67
  ]) => {
17
- const key = subKey.replace('$', prefix)
18
- acc[key] = subValue
68
+ const typePath = subTypePath.replace('$', parentTypePath)
69
+ // adapt field adapter with new path and context
70
+ const adaptedAdapter: FieldAdapter = {
71
+ convert: (from, valuePath, context) => {
72
+ return subAdapter.convert(from, ...getSubValuePathAndContext(valuePath, context))
73
+ },
74
+ create: (valuePath, context) => {
75
+ return subAdapter.create(...getSubValuePathAndContext(valuePath, context))
76
+ },
77
+ revert: subAdapter.revert && ((from, valuePath, context) => {
78
+ return subAdapter.revert!(from, ...getSubValuePathAndContext(valuePath, context))
79
+ }),
80
+ }
81
+ acc[typePath] = adaptedAdapter
19
82
  return acc
20
- }, {}) as SubFormFieldAdapters<SubAdapters, P>
83
+ }, {}) as SubFormFieldAdapters<SubAdapters, TypePath, TypePathsToValuePaths[TypePath], ValueOfType<ContextType>>
21
84
  }