@strictly/react-form 0.0.3 → 0.0.5

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 (50) hide show
  1. package/.out/.storybook/main.js +3 -1
  2. package/.out/core/mobx/field_adapter_builder.d.ts +1 -1
  3. package/.out/core/mobx/field_adapter_builder.js +1 -2
  4. package/.out/core/mobx/specs/sub_form_field_adapters.tests.d.ts +1 -0
  5. package/.out/core/mobx/specs/sub_form_field_adapters.tests.js +41 -0
  6. package/.out/core/mobx/sub_form_field_adapters.d.ts +7 -0
  7. package/.out/core/mobx/sub_form_field_adapters.js +8 -0
  8. package/.out/index.d.ts +1 -0
  9. package/.out/index.js +1 -0
  10. package/.out/mantine/create_list.d.ts +5 -4
  11. package/.out/mantine/create_list.js +4 -2
  12. package/.out/mantine/create_sub_form.d.ts +6 -0
  13. package/.out/mantine/create_sub_form.js +40 -0
  14. package/.out/mantine/hooks.d.ts +5 -2
  15. package/.out/mantine/hooks.js +9 -0
  16. package/.out/mantine/specs/list_hooks.stories.js +6 -6
  17. package/.out/mantine/specs/sub_form_hooks.stories.d.ts +15 -0
  18. package/.out/mantine/specs/sub_form_hooks.stories.js +107 -0
  19. package/.out/tsconfig.tsbuildinfo +1 -1
  20. package/.out/types/specs/list_fields_of_fields.tests.d.ts +1 -0
  21. package/.out/types/specs/list_fields_of_fields.tests.js +12 -0
  22. package/.out/types/specs/sub_form_fields.tests.d.ts +1 -0
  23. package/.out/types/specs/sub_form_fields.tests.js +12 -0
  24. package/.out/types/sub_form_fields.d.ts +7 -0
  25. package/.out/types/sub_form_fields.js +1 -0
  26. package/.storybook/main.ts +3 -1
  27. package/.turbo/turbo-build.log +8 -8
  28. package/.turbo/turbo-check-types.log +1 -1
  29. package/.turbo/turbo-release$colon$exports.log +1 -1
  30. package/core/mobx/field_adapter_builder.ts +3 -4
  31. package/core/mobx/specs/sub_form_field_adapters.tests.ts +59 -0
  32. package/core/mobx/sub_form_field_adapters.ts +21 -0
  33. package/dist/index.cjs +86 -24
  34. package/dist/index.d.cts +22 -8
  35. package/dist/index.d.ts +22 -8
  36. package/dist/index.js +85 -24
  37. package/index.ts +1 -0
  38. package/mantine/create_list.tsx +10 -4
  39. package/mantine/create_sub_form.tsx +70 -0
  40. package/mantine/hooks.tsx +29 -5
  41. package/mantine/specs/__snapshots__/list_hooks.tests.tsx.snap +56 -8
  42. package/mantine/specs/list_hooks.stories.tsx +16 -6
  43. package/mantine/specs/sub_form_hooks.stories.tsx +135 -0
  44. package/package.json +1 -1
  45. package/types/specs/list_fields_of_fields.tests.ts +29 -0
  46. package/types/specs/sub_form_fields.tests.ts +22 -0
  47. package/types/sub_form_fields.ts +7 -0
  48. package/.out/field_converters/list_converter.d.ts +0 -2
  49. package/.out/field_converters/list_converter.js +0 -13
  50. package/field_converters/list_converter.ts +0 -20
@@ -1,4 +1,6 @@
1
- import { dirname, join, } from 'path';
1
+ import { createRequire } from 'module';
2
+ import { dirname, join, } from 'node:path';
3
+ const require = createRequire(import.meta.url);
2
4
  /**
3
5
  * This function is used to resolve the absolute path of a package.
4
6
  * It is needed in projects that use Yarn PnP or are set up within a monorepo.
@@ -17,5 +17,5 @@ export declare function adapterFromTwoWayConverter<From, To, E, ValuePath extend
17
17
  export declare function adapterFromPrototype<From, To, ValuePath extends string, Context>(converter: AnnotatedFieldConverter<From, To, ValuePath, Context>, prototype: From): FieldAdapterBuilder<From, To, never, ValuePath, Context>;
18
18
  export declare function adapterFromPrototype<From, To, E, ValuePath extends string, Context>(converter: TwoWayFieldConverter<From, To, E, ValuePath, Context>, prototype: From): FieldAdapterBuilder<From, To, E, ValuePath, Context>;
19
19
  export declare function identityAdapter<V, ValuePath extends string, Context>(prototype: V, required?: boolean): FieldAdapterBuilder<V, V, never, ValuePath, Context>;
20
- export declare function listAdapter<E, K extends string, ValuePath extends string, Context>(): FieldAdapterBuilder<readonly E[], readonly K[], never, ValuePath, Context>;
20
+ export declare function listAdapter<E, ValuePath extends string, Context>(): FieldAdapterBuilder<readonly E[], readonly E[], never, ValuePath, Context>;
21
21
  export {};
@@ -1,6 +1,5 @@
1
1
  import { chainAnnotatedFieldConverter, chainUnreliableFieldConverter, } from 'field_converters/chain_field_converter';
2
2
  import { annotatedIdentityConverter, unreliableIdentityConverter, } from 'field_converters/identity_converter';
3
- import { listConverter } from 'field_converters/list_converter';
4
3
  import { MaybeIdentityConverter } from 'field_converters/maybe_identity_converter';
5
4
  import { prototypingFieldValueFactory } from 'field_value_factories/prototyping_field_value_factory';
6
5
  class FieldAdapterBuilder {
@@ -48,5 +47,5 @@ export function identityAdapter(prototype, required) {
48
47
  return new FieldAdapterBuilder(annotatedIdentityConverter(required), prototypingFieldValueFactory(prototype), unreliableIdentityConverter());
49
48
  }
50
49
  export function listAdapter() {
51
- return new FieldAdapterBuilder(listConverter(), prototypingFieldValueFactory([]));
50
+ return new FieldAdapterBuilder(annotatedIdentityConverter(false), prototypingFieldValueFactory([]), unreliableIdentityConverter());
52
51
  }
@@ -0,0 +1,41 @@
1
+ import { subFormFieldAdapters, } from 'core/mobx/sub_form_field_adapters';
2
+ import { mockDeep } from 'vitest-mock-extended';
3
+ describe('subFormFieldAdapters', () => {
4
+ const fieldAdapter1 = mockDeep();
5
+ const fieldAdapter2 = mockDeep();
6
+ describe('empty value', () => {
7
+ const adapters = subFormFieldAdapters({}, '$.a');
8
+ it('equals expected type', () => {
9
+ expectTypeOf(adapters).toEqualTypeOf();
10
+ });
11
+ it('equals expected value', () => {
12
+ expect(adapters).toEqual({});
13
+ });
14
+ });
15
+ describe('single adapter', () => {
16
+ const adapters = subFormFieldAdapters({
17
+ $: fieldAdapter1,
18
+ }, '$.a');
19
+ it('equals expected type', () => {
20
+ expectTypeOf(adapters).toEqualTypeOf();
21
+ });
22
+ it('equals expected value', () => {
23
+ expect(adapters).toEqual({ '$.a': fieldAdapter1 });
24
+ });
25
+ });
26
+ describe('multiple adapters', () => {
27
+ const adapters = subFormFieldAdapters({
28
+ '$.x': fieldAdapter1,
29
+ '$.y': fieldAdapter2,
30
+ }, '$.a');
31
+ it('equals expected type', () => {
32
+ expectTypeOf(adapters).toEqualTypeOf();
33
+ });
34
+ it('equals expected value', () => {
35
+ expect(adapters).toEqual({
36
+ '$.a.x': fieldAdapter1,
37
+ '$.a.y': fieldAdapter2,
38
+ });
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,7 @@
1
+ import { type StringConcatOf } from '@strictly/base';
2
+ import { type FieldAdapter } from './field_adapter';
3
+ type SubFormFieldAdapters<SubAdapters extends Record<string, FieldAdapter>, P extends string> = {
4
+ [K in keyof SubAdapters as K extends StringConcatOf<'$', infer S> ? `${P}${S}` : never]: SubAdapters[K];
5
+ };
6
+ export declare function subFormFieldAdapters<SubAdapters extends Record<string, FieldAdapter>, P extends string>(subAdapters: SubAdapters, prefix: P): SubFormFieldAdapters<SubAdapters, P>;
7
+ export {};
@@ -0,0 +1,8 @@
1
+ export function subFormFieldAdapters(subAdapters, prefix) {
2
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
3
+ return Object.entries(subAdapters).reduce((acc, [subKey, subValue,]) => {
4
+ const key = subKey.replace('$', prefix);
5
+ acc[key] = subValue;
6
+ return acc;
7
+ }, {});
8
+ }
package/.out/index.d.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';
package/.out/index.js 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';
@@ -4,12 +4,13 @@ import { type Fields } from 'types/field';
4
4
  import { type ListFieldsOfFields } from 'types/list_fields_of_fields';
5
5
  import { type ValueTypeOfField } from 'types/value_type_of_field';
6
6
  import { type MantineFieldComponent, type MantineForm } from './types';
7
- export type SuppliedListProps<Value = any> = {
7
+ export type SuppliedListProps<Value = any, ListPath extends string = string> = {
8
8
  values: readonly Value[];
9
+ listPath: ListPath;
9
10
  };
10
11
  export declare function createList<F extends Fields, K extends keyof ListFieldsOfFields<F>, Props extends SuppliedListProps<ElementOfArray<ValueTypeOfField<F[K]>>> & {
11
- children: (value: ElementOfArray<ValueTypeOfField<F[K]>>, index: number) => React.ReactNode;
12
+ children: (valuePath: `${K}.${number}`, value: ElementOfArray<ValueTypeOfField<F[K]>>, index: number) => React.ReactNode;
12
13
  }>(this: MantineForm<F>, valuePath: K, List: ComponentType<Props>): MantineFieldComponent<SuppliedListProps<ElementOfArray<ValueTypeOfField<F[K]>>>, Props>;
13
- export declare function DefaultList<Value>({ values, children, }: SuppliedListProps<Value> & {
14
- children: (value: Value, index: number) => React.ReactNode;
14
+ export declare function DefaultList<Value, ListPath extends string>({ values, listPath, children, }: SuppliedListProps<Value, ListPath> & {
15
+ children: (valuePath: `${ListPath}.${number}`, value: Value, index: number) => React.ReactNode;
15
16
  }): import("react/jsx-runtime").JSX.Element;
@@ -5,12 +5,14 @@ export function createList(valuePath, List) {
5
5
  const values = [...this.fields[valuePath].value];
6
6
  return {
7
7
  values,
8
+ listPath: valuePath,
8
9
  };
9
10
  };
10
11
  return createUnsafePartialObserverComponent(List, propSource);
11
12
  }
12
- export function DefaultList({ values, children, }) {
13
+ export function DefaultList({ values, listPath, children, }) {
13
14
  return (_jsx(_Fragment, { children: values.map(function (value, index) {
14
- return children(value, index);
15
+ const valuePath = `${listPath}.${index}`;
16
+ return children(valuePath, value, index);
15
17
  }) }));
16
18
  }
@@ -0,0 +1,6 @@
1
+ import type { FormProps } from 'core/props';
2
+ import type { ComponentType } from 'react';
3
+ import type { AllFieldsOfFields } from 'types/all_fields_of_fields';
4
+ import type { Fields } from 'types/field';
5
+ import type { SubFormFields } from 'types/sub_form_fields';
6
+ export declare function createSubForm<F extends Fields, K extends keyof AllFieldsOfFields<F>, S extends Fields = SubFormFields<F, K>>(valuePath: K, SubForm: ComponentType<FormProps<S>>, observableProps: FormProps<F>): () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { observer } from 'mobx-react';
3
+ export function createSubForm(valuePath, SubForm, observableProps) {
4
+ function toKey(subKey) {
5
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
6
+ return subKey.replace('$', valuePath);
7
+ }
8
+ function toSubKey(key) {
9
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
10
+ return key.replace(valuePath, '$');
11
+ }
12
+ function onFieldValueChange(subKey, value) {
13
+ // convert from subKey to key
14
+ observableProps.onFieldValueChange(toKey(subKey), value);
15
+ }
16
+ function onFieldBlur(subKey) {
17
+ observableProps.onFieldBlur?.(toKey(subKey));
18
+ }
19
+ function onFieldFocus(subKey) {
20
+ observableProps.onFieldFocus?.(toKey(subKey));
21
+ }
22
+ function onFieldSubmit(subKey) {
23
+ observableProps.onFieldSubmit?.(toKey(subKey));
24
+ }
25
+ return observer(function () {
26
+ // convert fields to sub-fields
27
+ const subFields = Object.entries(observableProps.fields).reduce((acc, [fieldKey, fieldValue,]) => {
28
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
29
+ if (fieldKey.startsWith(valuePath)) {
30
+ acc[toSubKey(fieldKey)] = fieldValue;
31
+ }
32
+ return acc;
33
+ }, {});
34
+ return (_jsx(SubForm
35
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
36
+ , {
37
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
38
+ fields: subFields, onFieldBlur: onFieldBlur, onFieldFocus: onFieldFocus, onFieldSubmit: onFieldSubmit, onFieldValueChange: onFieldValueChange }));
39
+ });
40
+ }
@@ -8,6 +8,7 @@ import { type ErrorOfField } from 'types/error_of_field';
8
8
  import { type Fields } from 'types/field';
9
9
  import { type ListFieldsOfFields } from 'types/list_fields_of_fields';
10
10
  import { type StringFieldsOfFields } from 'types/string_fields_of_fields';
11
+ import { type SubFormFields } from 'types/sub_form_fields';
11
12
  import { type ValueTypeOfField } from 'types/value_type_of_field';
12
13
  import { type SuppliedCheckboxProps } from './create_checkbox';
13
14
  import { DefaultList, type SuppliedListProps } from './create_list';
@@ -29,8 +30,9 @@ declare class MantineFormImpl<F extends Fields> implements MantineForm<F> {
29
30
  private readonly radioCache;
30
31
  private readonly pillCache;
31
32
  private readonly listCache;
33
+ private readonly subFormCache;
32
34
  accessor fields: F;
33
- onFieldValueChange: (<K extends keyof F>(this: void, key: K, value: F[K]['value']) => void) | undefined;
35
+ onFieldValueChange: <K extends keyof F>(this: void, key: K, value: F[K]['value']) => void;
34
36
  onFieldFocus: ((this: void, key: keyof F) => void) | undefined;
35
37
  onFieldBlur: ((this: void, key: keyof F) => void) | undefined;
36
38
  onFieldSubmit: ((this: void, key: keyof F) => boolean | void) | undefined;
@@ -47,6 +49,7 @@ declare class MantineFormImpl<F extends Fields> implements MantineForm<F> {
47
49
  radio<K extends keyof StringFieldsOfFields<F>, P extends SuppliedRadioProps>(valuePath: K, value: ValueTypeOfField<F[K]>, Radio: ComponentType<P>): MantineFieldComponent<SuppliedRadioProps, P, ErrorOfField<F[K]>>;
48
50
  pill<K extends keyof AllFieldsOfFields<F>>(valuePath: K): MantineFieldComponent<SuppliedPillProps, PillProps, ErrorOfField<F[K]>>;
49
51
  pill<K extends keyof AllFieldsOfFields<F>, P extends SuppliedPillProps>(valuePath: K, Pill: ComponentType<P>): MantineFieldComponent<SuppliedPillProps, P>;
50
- list<K extends keyof ListFieldsOfFields<F>>(valuePath: K): MantineFieldComponent<SuppliedListProps<ElementOfArray<F[K]>>, ComponentProps<typeof DefaultList<ElementOfArray<F[K]>>>>;
52
+ list<K extends keyof ListFieldsOfFields<F>>(valuePath: K): MantineFieldComponent<SuppliedListProps<`${K}.${number}`>, ComponentProps<typeof DefaultList<ElementOfArray<F[K]['value']>, K>>>;
53
+ subForm<K extends keyof AllFieldsOfFields<F>, S extends SubFormFields<F, K>>(valuePath: K, SubForm: ComponentType<FormProps<S>>): ComponentType;
51
54
  }
52
55
  export {};
@@ -8,6 +8,7 @@ import { createList, DefaultList, } from './create_list';
8
8
  import { createPill, } from './create_pill';
9
9
  import { createRadio, } from './create_radio';
10
10
  import { createRadioGroup, } from './create_radio_group';
11
+ import { createSubForm } from './create_sub_form';
11
12
  import { createTextInput, } from './create_text_input';
12
13
  import { createValueInput, } from './create_value_input';
13
14
  function SimpleSelect(props) {
@@ -62,6 +63,7 @@ class MantineFormImpl {
62
63
  radioCache = new Cache(createRadio.bind(this));
63
64
  pillCache = new Cache(createPill.bind(this));
64
65
  listCache = new Cache(createList.bind(this));
66
+ subFormCache = new Cache(createSubForm.bind(this));
65
67
  @observable.ref
66
68
  accessor fields;
67
69
  onFieldValueChange;
@@ -127,4 +129,11 @@ class MantineFormImpl {
127
129
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
128
130
  return this.listCache.retrieveOrCreate(valuePath, DefaultList);
129
131
  }
132
+ // TODO have the returned component take any non-overlapping props as props
133
+ subForm(valuePath, SubForm) {
134
+ return this.subFormCache.retrieveOrCreate(valuePath,
135
+ // strip props from component since we lose information in the cache
136
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
137
+ SubForm, this);
138
+ }
130
139
  }
@@ -5,8 +5,8 @@ import { useMantineForm } from 'mantine/hooks';
5
5
  function Component(props) {
6
6
  const form = useMantineForm(props);
7
7
  const List = form.list('$');
8
- return (_jsx(Paper, { p: 'sm', withBorder: true, children: _jsx(Stack, { children: _jsx(List, { children: function (valuePath) {
9
- return (_jsxs(Code, { children: ["ValuePath: ", valuePath] }, valuePath));
8
+ return (_jsx(Paper, { p: 'sm', withBorder: true, children: _jsx(Stack, { children: _jsx(List, { children: function (valuePath, value, index) {
9
+ return (_jsxs(Code, { children: [_jsxs("span", { children: ["ValuePath: ", valuePath] }), _jsx("br", {}), _jsxs("span", { children: ["Value: ", value] }), _jsx("br", {}), _jsxs("span", { children: ["Index: ", index] })] }, valuePath));
10
10
  } }) }) }));
11
11
  }
12
12
  const meta = {
@@ -37,10 +37,10 @@ export const Populated = {
37
37
  readonly: false,
38
38
  required: false,
39
39
  value: [
40
- '$.4',
41
- '$.6',
42
- '$.19',
43
- '$.0',
40
+ 'A',
41
+ 'B',
42
+ 'C',
43
+ 'D',
44
44
  ],
45
45
  },
46
46
  },
@@ -0,0 +1,15 @@
1
+ import { type Meta, type StoryObj } from '@storybook/react';
2
+ import { type FormProps } from 'core/props';
3
+ import { type Field } from 'types/field';
4
+ declare function Component(props: FormProps<{
5
+ $: Field<string, string>;
6
+ '$.a': Field<string, string>;
7
+ }>): import("react/jsx-runtime").JSX.Element;
8
+ declare const meta: Meta<typeof Component>;
9
+ export default meta;
10
+ type Story = StoryObj<typeof Component>;
11
+ export declare const Empty: Story;
12
+ export declare const Populated: Story;
13
+ export declare const Required: Story;
14
+ export declare const Disabled: Story;
15
+ export declare const CustomError: Story;
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Stack, } from '@mantine/core';
3
+ import { action } from '@storybook/addon-actions';
4
+ import { useMantineForm } from 'mantine/hooks';
5
+ function SubFormImpl(props) {
6
+ const form = useMantineForm(props);
7
+ const TextInput = form.textInput('$');
8
+ return _jsx(TextInput, { label: 'sub form' });
9
+ }
10
+ function Component(props) {
11
+ const form = useMantineForm(props);
12
+ const SubForm = form.subForm('$.a', SubFormImpl);
13
+ const TextInput = form.textInput('$');
14
+ return (_jsxs(Stack, { children: [_jsx(TextInput, { label: 'form' }), _jsx(SubForm, {})] }));
15
+ }
16
+ const meta = {
17
+ component: Component,
18
+ args: {
19
+ onFieldBlur: action('onFieldBlur'),
20
+ onFieldFocus: action('onFieldFocus'),
21
+ onFieldSubmit: action('onFieldSubmit'),
22
+ onFieldValueChange: action('onFieldValueChange'),
23
+ },
24
+ };
25
+ export default meta;
26
+ export const Empty = {
27
+ args: {
28
+ fields: {
29
+ $: {
30
+ readonly: false,
31
+ required: false,
32
+ value: '',
33
+ },
34
+ '$.a': {
35
+ readonly: false,
36
+ required: false,
37
+ value: '',
38
+ },
39
+ },
40
+ },
41
+ };
42
+ export const Populated = {
43
+ args: {
44
+ fields: {
45
+ $: {
46
+ readonly: false,
47
+ required: false,
48
+ value: 'Hello',
49
+ },
50
+ '$.a': {
51
+ readonly: false,
52
+ required: false,
53
+ value: 'World',
54
+ },
55
+ },
56
+ },
57
+ };
58
+ export const Required = {
59
+ args: {
60
+ fields: {
61
+ $: {
62
+ readonly: false,
63
+ required: true,
64
+ value: 'xxx',
65
+ },
66
+ '$.a': {
67
+ readonly: false,
68
+ required: true,
69
+ value: 'yyy',
70
+ },
71
+ },
72
+ },
73
+ };
74
+ export const Disabled = {
75
+ args: {
76
+ fields: {
77
+ $: {
78
+ readonly: true,
79
+ required: false,
80
+ value: 'xxx',
81
+ },
82
+ '$.a': {
83
+ readonly: true,
84
+ required: false,
85
+ value: 'yyy',
86
+ },
87
+ },
88
+ },
89
+ };
90
+ export const CustomError = {
91
+ args: {
92
+ fields: {
93
+ $: {
94
+ readonly: false,
95
+ required: false,
96
+ value: 'xxx',
97
+ error: 'form error',
98
+ },
99
+ '$.a': {
100
+ readonly: false,
101
+ required: false,
102
+ value: 'xxx',
103
+ error: 'sub form error',
104
+ },
105
+ },
106
+ },
107
+ };