@strictly/react-form 0.0.2 → 0.0.4
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.
- package/.out/.storybook/main.js +3 -1
- package/.out/core/mobx/specs/sub_form_field_adapters.tests.d.ts +1 -0
- package/.out/core/mobx/specs/sub_form_field_adapters.tests.js +41 -0
- package/.out/core/mobx/sub_form_field_adapters.d.ts +7 -0
- package/.out/core/mobx/sub_form_field_adapters.js +8 -0
- package/.out/index.d.ts +1 -0
- package/.out/index.js +1 -0
- package/.out/mantine/create_sub_form.d.ts +6 -0
- package/.out/mantine/create_sub_form.js +40 -0
- package/.out/mantine/hooks.d.ts +5 -3
- package/.out/mantine/hooks.js +9 -0
- package/.out/mantine/specs/sub_form_hooks.stories.d.ts +15 -0
- package/.out/mantine/specs/sub_form_hooks.stories.js +107 -0
- package/.out/tsconfig.tsbuildinfo +1 -1
- package/.out/types/specs/list_fields_of_fields.tests.d.ts +1 -0
- package/.out/types/specs/list_fields_of_fields.tests.js +12 -0
- package/.out/types/specs/sub_form_fields.tests.d.ts +1 -0
- package/.out/types/specs/sub_form_fields.tests.js +12 -0
- package/.out/types/sub_form_fields.d.ts +7 -0
- package/.out/types/sub_form_fields.js +1 -0
- package/.storybook/main.ts +3 -1
- package/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-check-types.log +1 -1
- package/.turbo/turbo-release$colon$exports.log +1 -1
- package/core/mobx/specs/sub_form_field_adapters.tests.ts +59 -0
- package/core/mobx/sub_form_field_adapters.ts +21 -0
- package/dist/index.cjs +241 -10302
- package/dist/index.d.cts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js +258 -10333
- package/index.ts +1 -0
- package/mantine/create_sub_form.tsx +70 -0
- package/mantine/hooks.tsx +29 -6
- package/mantine/specs/sub_form_hooks.stories.tsx +135 -0
- package/package.json +5 -4
- package/types/specs/list_fields_of_fields.tests.ts +29 -0
- package/types/specs/sub_form_fields.tests.ts +22 -0
- package/types/sub_form_fields.ts +7 -0
package/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './core/mobx/form_fields_of_field_adapters'
|
|
|
6
6
|
export * from './core/mobx/form_presenter'
|
|
7
7
|
export * from './core/mobx/merge_field_adapters_with_two_way_converter'
|
|
8
8
|
export * from './core/mobx/merge_field_adapters_with_validators'
|
|
9
|
+
export * from './core/mobx/sub_form_field_adapters'
|
|
9
10
|
export * from './core/mobx/types'
|
|
10
11
|
export * from './core/props'
|
|
11
12
|
export * from './field_converters/integer_to_string_converter'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { FormProps } from 'core/props'
|
|
2
|
+
import { observer } from 'mobx-react'
|
|
3
|
+
import type { ComponentType } from 'react'
|
|
4
|
+
import type { AllFieldsOfFields } from 'types/all_fields_of_fields'
|
|
5
|
+
import type { Fields } from 'types/field'
|
|
6
|
+
import type { SubFormFields } from 'types/sub_form_fields'
|
|
7
|
+
import type { ValueTypeOfField } from 'types/value_type_of_field'
|
|
8
|
+
|
|
9
|
+
export function createSubForm<
|
|
10
|
+
F extends Fields,
|
|
11
|
+
K extends keyof AllFieldsOfFields<F>,
|
|
12
|
+
S extends Fields = SubFormFields<F, K>,
|
|
13
|
+
>(
|
|
14
|
+
valuePath: K,
|
|
15
|
+
SubForm: ComponentType<FormProps<S>>,
|
|
16
|
+
observableProps: FormProps<F>,
|
|
17
|
+
) {
|
|
18
|
+
function toKey(subKey: string | number | symbol): string {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
20
|
+
return (subKey as string).replace('$', valuePath as string)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toSubKey(key: string | number | symbol): string {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
25
|
+
return (key as string).replace(valuePath as string, '$')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onFieldValueChange<SubK extends keyof S>(
|
|
29
|
+
subKey: SubK,
|
|
30
|
+
value: ValueTypeOfField<S[SubK]>,
|
|
31
|
+
) {
|
|
32
|
+
// convert from subKey to key
|
|
33
|
+
observableProps.onFieldValueChange(toKey(subKey), value)
|
|
34
|
+
}
|
|
35
|
+
function onFieldBlur(subKey: keyof S) {
|
|
36
|
+
observableProps.onFieldBlur?.(toKey(subKey))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function onFieldFocus(subKey: keyof S) {
|
|
40
|
+
observableProps.onFieldFocus?.(toKey(subKey))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function onFieldSubmit(subKey: keyof S) {
|
|
44
|
+
observableProps.onFieldSubmit?.(toKey(subKey))
|
|
45
|
+
}
|
|
46
|
+
return observer(function () {
|
|
47
|
+
// convert fields to sub-fields
|
|
48
|
+
const subFields = Object.entries(observableProps.fields).reduce<Record<string, unknown>>((acc, [
|
|
49
|
+
fieldKey,
|
|
50
|
+
fieldValue,
|
|
51
|
+
]) => {
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
53
|
+
if (fieldKey.startsWith(valuePath as string)) {
|
|
54
|
+
acc[toSubKey(fieldKey)] = fieldValue
|
|
55
|
+
}
|
|
56
|
+
return acc
|
|
57
|
+
}, {})
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<SubForm
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
62
|
+
fields={subFields as S}
|
|
63
|
+
onFieldBlur={onFieldBlur}
|
|
64
|
+
onFieldFocus={onFieldFocus}
|
|
65
|
+
onFieldSubmit={onFieldSubmit}
|
|
66
|
+
onFieldValueChange={onFieldValueChange}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
}
|
package/mantine/hooks.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
} from '@mantine/core'
|
|
14
14
|
import {
|
|
15
15
|
Cache,
|
|
16
|
-
type ElementOfArray,
|
|
17
16
|
} from '@strictly/base'
|
|
18
17
|
import { type FormProps } from 'core/props'
|
|
19
18
|
import {
|
|
@@ -35,6 +34,7 @@ import {
|
|
|
35
34
|
} from 'types/field'
|
|
36
35
|
import { type ListFieldsOfFields } from 'types/list_fields_of_fields'
|
|
37
36
|
import { type StringFieldsOfFields } from 'types/string_fields_of_fields'
|
|
37
|
+
import { type SubFormFields } from 'types/sub_form_fields'
|
|
38
38
|
import { type ValueTypeOfField } from 'types/value_type_of_field'
|
|
39
39
|
import {
|
|
40
40
|
createCheckbox,
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
createRadioGroup,
|
|
58
58
|
type SuppliedRadioGroupProps,
|
|
59
59
|
} from './create_radio_group'
|
|
60
|
+
import { createSubForm } from './create_sub_form'
|
|
60
61
|
import {
|
|
61
62
|
createTextInput,
|
|
62
63
|
type SuppliedTextInputProps,
|
|
@@ -175,10 +176,18 @@ class MantineFormImpl<
|
|
|
175
176
|
> = new Cache(
|
|
176
177
|
createList.bind(this),
|
|
177
178
|
)
|
|
179
|
+
private readonly subFormCache: Cache<
|
|
180
|
+
// the cache cannot reference keys, so we just use any
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
[keyof AllFieldsOfFields<F>, ComponentType<any>, FormProps<F>],
|
|
183
|
+
ComponentType
|
|
184
|
+
> = new Cache(
|
|
185
|
+
createSubForm.bind(this),
|
|
186
|
+
)
|
|
178
187
|
|
|
179
188
|
@observable.ref
|
|
180
189
|
accessor fields: F
|
|
181
|
-
onFieldValueChange
|
|
190
|
+
onFieldValueChange!: <K extends keyof F>(this: void, key: K, value: F[K]['value']) => void
|
|
182
191
|
onFieldFocus: ((this: void, key: keyof F) => void) | undefined
|
|
183
192
|
onFieldBlur: ((this: void, key: keyof F) => void) | undefined
|
|
184
193
|
onFieldSubmit: ((this: void, key: keyof F) => boolean | void) | undefined
|
|
@@ -352,17 +361,31 @@ class MantineFormImpl<
|
|
|
352
361
|
list<
|
|
353
362
|
K extends keyof ListFieldsOfFields<F>,
|
|
354
363
|
>(valuePath: K): MantineFieldComponent<
|
|
355
|
-
SuppliedListProps
|
|
356
|
-
ComponentProps<typeof DefaultList
|
|
364
|
+
SuppliedListProps<`${K}.${number}`>,
|
|
365
|
+
ComponentProps<typeof DefaultList<`${K}.${number}`>>
|
|
357
366
|
> {
|
|
358
367
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
359
368
|
return this.listCache.retrieveOrCreate(
|
|
360
369
|
valuePath,
|
|
361
370
|
DefaultList,
|
|
362
371
|
) as MantineFieldComponent<
|
|
363
|
-
SuppliedListProps
|
|
364
|
-
ComponentProps<typeof DefaultList
|
|
372
|
+
SuppliedListProps<`${K}.${number}`>,
|
|
373
|
+
ComponentProps<typeof DefaultList<`${K}.${number}`>>,
|
|
365
374
|
ErrorOfField<F[K]>
|
|
366
375
|
>
|
|
367
376
|
}
|
|
377
|
+
|
|
378
|
+
// TODO have the returned component take any non-overlapping props as props
|
|
379
|
+
subForm<
|
|
380
|
+
K extends keyof AllFieldsOfFields<F>,
|
|
381
|
+
S extends SubFormFields<F, K>,
|
|
382
|
+
>(valuePath: K, SubForm: ComponentType<FormProps<S>>): ComponentType {
|
|
383
|
+
return this.subFormCache.retrieveOrCreate(
|
|
384
|
+
valuePath,
|
|
385
|
+
// strip props from component since we lose information in the cache
|
|
386
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
387
|
+
SubForm as ComponentType,
|
|
388
|
+
this,
|
|
389
|
+
)
|
|
390
|
+
}
|
|
368
391
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Stack,
|
|
3
|
+
} from '@mantine/core'
|
|
4
|
+
import { action } from '@storybook/addon-actions'
|
|
5
|
+
import {
|
|
6
|
+
type Meta,
|
|
7
|
+
type StoryObj,
|
|
8
|
+
} from '@storybook/react'
|
|
9
|
+
import { type FormProps } from 'core/props'
|
|
10
|
+
import { useMantineForm } from 'mantine/hooks'
|
|
11
|
+
import { type Field } from 'types/field'
|
|
12
|
+
|
|
13
|
+
function SubFormImpl(props: FormProps<{
|
|
14
|
+
$: Field<string, string>,
|
|
15
|
+
}>) {
|
|
16
|
+
const form = useMantineForm(props)
|
|
17
|
+
const TextInput = form.textInput('$')
|
|
18
|
+
return <TextInput label='sub form' />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Component(props: FormProps<{
|
|
22
|
+
$: Field<string, string>,
|
|
23
|
+
'$.a': Field<string, string>,
|
|
24
|
+
}>) {
|
|
25
|
+
const form = useMantineForm(props)
|
|
26
|
+
const SubForm = form.subForm('$.a', SubFormImpl)
|
|
27
|
+
const TextInput = form.textInput('$')
|
|
28
|
+
return (
|
|
29
|
+
<Stack>
|
|
30
|
+
<TextInput label='form' />
|
|
31
|
+
<SubForm />
|
|
32
|
+
</Stack>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const meta: Meta<typeof Component> = {
|
|
37
|
+
component: Component,
|
|
38
|
+
args: {
|
|
39
|
+
onFieldBlur: action('onFieldBlur'),
|
|
40
|
+
onFieldFocus: action('onFieldFocus'),
|
|
41
|
+
onFieldSubmit: action('onFieldSubmit'),
|
|
42
|
+
onFieldValueChange: action('onFieldValueChange'),
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default meta
|
|
47
|
+
|
|
48
|
+
type Story = StoryObj<typeof Component>
|
|
49
|
+
|
|
50
|
+
export const Empty: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
fields: {
|
|
53
|
+
$: {
|
|
54
|
+
readonly: false,
|
|
55
|
+
required: false,
|
|
56
|
+
value: '',
|
|
57
|
+
},
|
|
58
|
+
'$.a': {
|
|
59
|
+
readonly: false,
|
|
60
|
+
required: false,
|
|
61
|
+
value: '',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Populated: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
fields: {
|
|
70
|
+
$: {
|
|
71
|
+
readonly: false,
|
|
72
|
+
required: false,
|
|
73
|
+
value: 'Hello',
|
|
74
|
+
},
|
|
75
|
+
'$.a': {
|
|
76
|
+
readonly: false,
|
|
77
|
+
required: false,
|
|
78
|
+
value: 'World',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const Required: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
fields: {
|
|
87
|
+
$: {
|
|
88
|
+
readonly: false,
|
|
89
|
+
required: true,
|
|
90
|
+
value: 'xxx',
|
|
91
|
+
},
|
|
92
|
+
'$.a': {
|
|
93
|
+
readonly: false,
|
|
94
|
+
required: true,
|
|
95
|
+
value: 'yyy',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const Disabled: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
fields: {
|
|
104
|
+
$: {
|
|
105
|
+
readonly: true,
|
|
106
|
+
required: false,
|
|
107
|
+
value: 'xxx',
|
|
108
|
+
},
|
|
109
|
+
'$.a': {
|
|
110
|
+
readonly: true,
|
|
111
|
+
required: false,
|
|
112
|
+
value: 'yyy',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const CustomError: Story = {
|
|
119
|
+
args: {
|
|
120
|
+
fields: {
|
|
121
|
+
$: {
|
|
122
|
+
readonly: false,
|
|
123
|
+
required: false,
|
|
124
|
+
value: 'xxx',
|
|
125
|
+
error: 'form error',
|
|
126
|
+
},
|
|
127
|
+
'$.a': {
|
|
128
|
+
readonly: false,
|
|
129
|
+
required: false,
|
|
130
|
+
value: 'xxx',
|
|
131
|
+
error: 'sub form error',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Chris <chris.glover@gmail.com> (@madmaw)",
|
|
3
3
|
"dependencies": {
|
|
4
|
+
"@mantine/core": "^7.13.4",
|
|
5
|
+
"@mantine/hooks": "^7.13.4",
|
|
4
6
|
"@strictly/base": "*",
|
|
5
7
|
"@strictly/define": "*",
|
|
6
8
|
"mobx": "^6.13.5",
|
|
7
9
|
"mobx-react": "^9.1.1",
|
|
8
|
-
"react": "^19.0.0"
|
|
10
|
+
"react": "^19.0.0 || ^18.3.1",
|
|
11
|
+
"react-dom": "^19.0.0 || ^18.3.1"
|
|
9
12
|
},
|
|
10
13
|
"description": "Types and utilities for creating React forms",
|
|
11
14
|
"devDependencies": {
|
|
12
15
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
13
16
|
"@babel/plugin-transform-class-static-block": "^7.26.0",
|
|
14
|
-
"@mantine/core": "^7.13.4",
|
|
15
17
|
"@storybook/addon-actions": "^8.4.5",
|
|
16
18
|
"@storybook/addon-essentials": "^8.4.5",
|
|
17
19
|
"@storybook/addon-interactions": "^8.4.5",
|
|
@@ -29,7 +31,6 @@
|
|
|
29
31
|
"@vitejs/plugin-react": "^4.3.3",
|
|
30
32
|
"concurrently": "^9.1.2",
|
|
31
33
|
"jsdom": "^25.0.1",
|
|
32
|
-
"react-dom": "^19.0.0",
|
|
33
34
|
"resize-observer-polyfill": "^1.5.1",
|
|
34
35
|
"storybook": "^8.4.5",
|
|
35
36
|
"type-fest": "^4.26.1",
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
"test:watch": "vitest"
|
|
69
70
|
},
|
|
70
71
|
"type": "module",
|
|
71
|
-
"version": "0.0.
|
|
72
|
+
"version": "0.0.4",
|
|
72
73
|
"exports": {
|
|
73
74
|
".": {
|
|
74
75
|
"import": {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Field } from 'types/field'
|
|
2
|
+
import { type ListFieldsOfFields } from 'types/list_fields_of_fields'
|
|
3
|
+
|
|
4
|
+
describe('ListFieldsOfFields', () => {
|
|
5
|
+
it('matches the expected type of an empty set of fields', () => {
|
|
6
|
+
type T = ListFieldsOfFields<{}>
|
|
7
|
+
expectTypeOf<T>().toEqualTypeOf<{}>()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('matches the expected type of a set of fields containing a single list', () => {
|
|
11
|
+
type X = {
|
|
12
|
+
l: Field<number[]>,
|
|
13
|
+
}
|
|
14
|
+
type T = ListFieldsOfFields<X>
|
|
15
|
+
expectTypeOf<T>().toEqualTypeOf<X>()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('matches the expected type of a set of fields containing a multiple fields, including a list', () => {
|
|
19
|
+
type X = {
|
|
20
|
+
readonly a: Field<number>,
|
|
21
|
+
readonly b: Field<string>,
|
|
22
|
+
readonly l: Field<readonly number[]>,
|
|
23
|
+
}
|
|
24
|
+
type T = ListFieldsOfFields<X>
|
|
25
|
+
expectTypeOf<T>().toEqualTypeOf<{
|
|
26
|
+
readonly l: Field<readonly number[]>,
|
|
27
|
+
}>()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Simplify } from 'type-fest'
|
|
2
|
+
import { type Field } from 'types/field'
|
|
3
|
+
import { type SubFormFields } from 'types/sub_form_fields'
|
|
4
|
+
|
|
5
|
+
describe('SubFormFields', () => {
|
|
6
|
+
it('works on single field', () => {
|
|
7
|
+
type T = Simplify<SubFormFields<{ $: Field }, '$'>>
|
|
8
|
+
expectTypeOf<T>().toEqualTypeOf<{ $: Field }>()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('works on more two fields', () => {
|
|
12
|
+
type T = Simplify<SubFormFields<{ '$.a': Field<string>, '$.b': Field<boolean> }, '$.a'>>
|
|
13
|
+
expectTypeOf<T>().toEqualTypeOf<{ $: Field<string> }>()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('works on subfields', () => {
|
|
17
|
+
type T = Simplify<
|
|
18
|
+
SubFormFields<{ $: Field<null>, '$.a': Field<string>, '$.a.b': Field<boolean>, '$.a.b.c': Field<number> }, '$.a'>
|
|
19
|
+
>
|
|
20
|
+
expectTypeOf<T>().toEqualTypeOf<{ $: Field<string>, '$.b': Field<boolean>, '$.b.c': Field<number> }>()
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type StringConcatOf } from '@strictly/base'
|
|
2
|
+
import { type Fields } from './field'
|
|
3
|
+
|
|
4
|
+
export type SubFormFields<F extends Fields, P extends keyof F> = P extends string ? {
|
|
5
|
+
[K in keyof F as K extends StringConcatOf<`${P}.`, infer S> ? `$.${S}` : never]: F[K]
|
|
6
|
+
} & { $: F[P] }
|
|
7
|
+
: never
|