@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.
Files changed (38) hide show
  1. package/.out/.storybook/main.js +3 -1
  2. package/.out/core/mobx/specs/sub_form_field_adapters.tests.d.ts +1 -0
  3. package/.out/core/mobx/specs/sub_form_field_adapters.tests.js +41 -0
  4. package/.out/core/mobx/sub_form_field_adapters.d.ts +7 -0
  5. package/.out/core/mobx/sub_form_field_adapters.js +8 -0
  6. package/.out/index.d.ts +1 -0
  7. package/.out/index.js +1 -0
  8. package/.out/mantine/create_sub_form.d.ts +6 -0
  9. package/.out/mantine/create_sub_form.js +40 -0
  10. package/.out/mantine/hooks.d.ts +5 -3
  11. package/.out/mantine/hooks.js +9 -0
  12. package/.out/mantine/specs/sub_form_hooks.stories.d.ts +15 -0
  13. package/.out/mantine/specs/sub_form_hooks.stories.js +107 -0
  14. package/.out/tsconfig.tsbuildinfo +1 -1
  15. package/.out/types/specs/list_fields_of_fields.tests.d.ts +1 -0
  16. package/.out/types/specs/list_fields_of_fields.tests.js +12 -0
  17. package/.out/types/specs/sub_form_fields.tests.d.ts +1 -0
  18. package/.out/types/specs/sub_form_fields.tests.js +12 -0
  19. package/.out/types/sub_form_fields.d.ts +7 -0
  20. package/.out/types/sub_form_fields.js +1 -0
  21. package/.storybook/main.ts +3 -1
  22. package/.turbo/turbo-build.log +8 -8
  23. package/.turbo/turbo-check-types.log +1 -1
  24. package/.turbo/turbo-release$colon$exports.log +1 -1
  25. package/core/mobx/specs/sub_form_field_adapters.tests.ts +59 -0
  26. package/core/mobx/sub_form_field_adapters.ts +21 -0
  27. package/dist/index.cjs +241 -10302
  28. package/dist/index.d.cts +17 -4
  29. package/dist/index.d.ts +17 -4
  30. package/dist/index.js +258 -10333
  31. package/index.ts +1 -0
  32. package/mantine/create_sub_form.tsx +70 -0
  33. package/mantine/hooks.tsx +29 -6
  34. package/mantine/specs/sub_form_hooks.stories.tsx +135 -0
  35. package/package.json +5 -4
  36. package/types/specs/list_fields_of_fields.tests.ts +29 -0
  37. package/types/specs/sub_form_fields.tests.ts +22 -0
  38. 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: (<K extends keyof F>(this: void, key: K, value: F[K]['value']) => void) | undefined
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<ElementOfArray<F[K]>>,
356
- ComponentProps<typeof DefaultList<ElementOfArray<F[K]>>>
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<ElementOfArray<F[K]>>,
364
- ComponentProps<typeof DefaultList<ElementOfArray<F[K]>>>,
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.2",
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