@availity/mui-controlled-form 1.1.2 → 1.2.0

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.
@@ -1,5 +1,5 @@
1
1
  import { AsyncAutocomplete, AsyncAutocompleteProps } from '@availity/mui-autocomplete';
2
- import { RegisterOptions, FieldValues, Controller } from 'react-hook-form';
2
+ import { RegisterOptions, FieldValues, Controller, useFormContext } from 'react-hook-form';
3
3
  import { ChipTypeMap } from '@mui/material/Chip';
4
4
  import { ControllerProps } from './Types';
5
5
 
@@ -9,7 +9,7 @@ export type ControlledAsyncAutocompleteProps<
9
9
  DisableClearable extends boolean | undefined,
10
10
  FreeSolo extends boolean | undefined,
11
11
  ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
12
- > = Omit<
12
+ > = { defaultToFirstOption?: boolean; defaultToOnlyOption?: boolean } & Omit<
13
13
  AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>,
14
14
  'onBlur' | 'onChange' | 'value' | 'name'
15
15
  > &
@@ -30,19 +30,16 @@ export const ControlledAsyncAutocomplete = <
30
30
  shouldUnregister,
31
31
  value,
32
32
  FieldProps,
33
+ defaultToFirstOption,
34
+ defaultToOnlyOption,
33
35
  ...rest
34
36
  }: ControlledAsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => {
37
+ const { setValue } = useFormContext();
35
38
  return (
36
39
  <Controller
37
40
  name={name}
38
41
  defaultValue={rest.defaultValue}
39
- rules={{
40
- onBlur,
41
- onChange,
42
- shouldUnregister,
43
- value,
44
- ...rules,
45
- }}
42
+ rules={{ onBlur, onChange, shouldUnregister, value, ...rules }}
46
43
  shouldUnregister={shouldUnregister}
47
44
  render={({ field: { onChange, value, onBlur, ref }, fieldState: { error } }) => (
48
45
  <AsyncAutocomplete
@@ -70,6 +67,19 @@ export const ControlledAsyncAutocomplete = <
70
67
  }}
71
68
  onBlur={onBlur}
72
69
  value={value || null}
70
+ loadOptions={async (offset, limit, inputValue) => {
71
+ const { options, hasMore, offset: returnedOffsetValue } = await rest.loadOptions(offset, limit, inputValue);
72
+
73
+ if (defaultToFirstOption && offset === 0) {
74
+ setValue(name, options[0]);
75
+ }
76
+
77
+ if (defaultToOnlyOption && offset === 0 && options.length === 1) {
78
+ setValue(name, options[0]);
79
+ }
80
+
81
+ return { options, hasMore, offset: returnedOffsetValue };
82
+ }}
73
83
  />
74
84
  )}
75
85
  />
@@ -1,10 +1,25 @@
1
- import { CodesAutocomplete, CodesAutocompleteProps } from '@availity/mui-autocomplete';
2
- import { Controller, RegisterOptions, FieldValues } from 'react-hook-form';
3
- import { ControllerProps } from './Types';
1
+ import type { Code } from '@availity/mui-autocomplete';
2
+ import { fetchCodes, handleGetCodesOptionLabel } from '@availity/mui-autocomplete';
3
+ import { ApiConfig } from '@availity/api-axios';
4
+ import { ChipTypeMap } from '@mui/material/Chip';
5
+ import type { Optional } from './utils';
6
+ import { ControlledAsyncAutocomplete, ControlledAsyncAutocompleteProps } from './AsyncAutocomplete';
4
7
 
5
- export type ControlledCodesAutocompleteProps = Omit<CodesAutocompleteProps, 'onBlur' | 'onChange' | 'value' | 'name'> &
6
- Pick<RegisterOptions<FieldValues, string>, 'onBlur' | 'onChange' | 'value'> &
7
- ControllerProps;
8
+ export interface ControlledCodesAutocompleteProps<
9
+ Option = Code,
10
+ Multiple extends boolean | undefined = false,
11
+ DisableClearable extends boolean | undefined = false,
12
+ FreeSolo extends boolean | undefined = false,
13
+ ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
14
+ > extends Omit<
15
+ Optional<ControlledAsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'queryKey'>,
16
+ 'loadOptions'
17
+ > {
18
+ /** The code list id. */
19
+ list: string;
20
+ /** Config passed to the AvCodesApi.query function */
21
+ apiConfig?: ApiConfig;
22
+ }
8
23
 
9
24
  export const ControlledCodesAutocomplete = ({
10
25
  name,
@@ -15,48 +30,38 @@ export const ControlledCodesAutocomplete = ({
15
30
  shouldUnregister,
16
31
  value,
17
32
  FieldProps,
33
+ apiConfig = {},
34
+ queryOptions,
35
+ queryKey = 'codes-autocomplete',
36
+ list,
37
+ watchParams,
18
38
  ...rest
19
39
  }: ControlledCodesAutocompleteProps) => {
40
+ const handleLoadOptions = async (offset: number, limit: number, inputValue: string) => {
41
+ const resp = await fetchCodes({
42
+ ...apiConfig,
43
+ params: { ...apiConfig.params, list, offset, limit, q: inputValue },
44
+ });
45
+
46
+ return resp;
47
+ };
48
+
20
49
  return (
21
- <Controller
50
+ <ControlledAsyncAutocomplete
22
51
  name={name}
23
52
  defaultValue={defaultValue}
24
- rules={{
25
- onBlur,
26
- onChange,
27
- shouldUnregister,
28
- value,
29
- ...rules,
30
- }}
53
+ onBlur={onBlur}
54
+ onChange={onChange}
55
+ rules={rules}
31
56
  shouldUnregister={shouldUnregister}
32
- render={({ field: { onChange, value, onBlur, ref }, fieldState: { error } }) => (
33
- <CodesAutocomplete
34
- {...rest}
35
- FieldProps={{
36
- required: typeof rules.required === 'object' ? rules.required.value : !!rules.required,
37
- ...FieldProps,
38
- error: !!error,
39
- helperText: error?.message ? (
40
- <>
41
- {error.message}
42
- <br />
43
- {FieldProps?.helperText}
44
- </>
45
- ) : (
46
- FieldProps?.helperText
47
- ),
48
- inputRef: ref,
49
- }}
50
- onChange={(event, value, reason) => {
51
- if (reason === 'clear') {
52
- onChange(null);
53
- }
54
- onChange(value);
55
- }}
56
- onBlur={onBlur}
57
- value={value || null}
58
- />
59
- )}
57
+ value={value}
58
+ FieldProps={FieldProps}
59
+ getOptionLabel={handleGetCodesOptionLabel}
60
+ queryKey={queryKey}
61
+ queryOptions={{ enabled: !!list, ...queryOptions }}
62
+ watchParams={{ list, ...watchParams }}
63
+ {...rest}
64
+ loadOptions={handleLoadOptions}
60
65
  />
61
66
  );
62
67
  };
@@ -1,10 +1,23 @@
1
- import { OrganizationAutocomplete, OrgAutocompleteProps } from '@availity/mui-autocomplete';
2
- import { Controller, RegisterOptions, FieldValues } from 'react-hook-form';
3
- import { ControllerProps } from './Types';
1
+ import type { Organization } from '@availity/mui-autocomplete';
2
+ import { handleGetOrgOptionLabel, fetchOrgs } from '@availity/mui-autocomplete';
3
+ import type { ChipTypeMap } from '@mui/material/Chip';
4
+ import type { ApiConfig } from '@availity/api-axios';
5
+ import type { Optional } from './utils';
6
+ import { ControlledAsyncAutocomplete, ControlledAsyncAutocompleteProps } from './AsyncAutocomplete';
4
7
 
5
- export type ControlledOrgAutocompleteProps = Omit<OrgAutocompleteProps, 'onBlur' | 'onChange' | 'value' | 'name'> &
6
- Pick<RegisterOptions<FieldValues, string>, 'onBlur' | 'onChange' | 'value'> &
7
- ControllerProps;
8
+ export interface ControlledOrgAutocompleteProps<
9
+ Option = Organization,
10
+ Multiple extends boolean | undefined = false,
11
+ DisableClearable extends boolean | undefined = false,
12
+ FreeSolo extends boolean | undefined = false,
13
+ ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
14
+ > extends Omit<
15
+ Optional<ControlledAsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'queryKey'>,
16
+ 'loadOptions'
17
+ > {
18
+ /** Axios ApiConfig */
19
+ apiConfig?: ApiConfig;
20
+ }
8
21
 
9
22
  export const ControlledOrganizationAutocomplete = ({
10
23
  name,
@@ -15,48 +28,29 @@ export const ControlledOrganizationAutocomplete = ({
15
28
  shouldUnregister,
16
29
  value,
17
30
  FieldProps,
31
+ queryKey = 'org-autocomplete',
32
+ apiConfig = {},
18
33
  ...rest
19
34
  }: ControlledOrgAutocompleteProps) => {
35
+ const handleLoadOptions = async (offset: number, limit: number) => {
36
+ const resp = await fetchOrgs({ ...apiConfig, params: { dropdown: true, ...apiConfig.params, offset, limit } });
37
+
38
+ return resp;
39
+ };
20
40
  return (
21
- <Controller
41
+ <ControlledAsyncAutocomplete
22
42
  name={name}
23
43
  defaultValue={defaultValue}
24
- rules={{
25
- onBlur,
26
- onChange,
27
- shouldUnregister,
28
- value,
29
- ...rules,
30
- }}
44
+ onBlur={onBlur}
45
+ onChange={onChange}
46
+ rules={rules}
31
47
  shouldUnregister={shouldUnregister}
32
- render={({ field: { onChange, value, onBlur, ref }, fieldState: { error } }) => (
33
- <OrganizationAutocomplete
34
- {...rest}
35
- FieldProps={{
36
- required: typeof rules.required === 'object' ? rules.required.value : !!rules.required,
37
- ...FieldProps,
38
- error: !!error,
39
- helperText: error?.message ? (
40
- <>
41
- {error.message}
42
- <br />
43
- {FieldProps?.helperText}
44
- </>
45
- ) : (
46
- FieldProps?.helperText
47
- ),
48
- inputRef: ref,
49
- }}
50
- onChange={(event, value, reason) => {
51
- if (reason === 'clear') {
52
- onChange(null);
53
- }
54
- onChange(value);
55
- }}
56
- onBlur={onBlur}
57
- value={value || null}
58
- />
59
- )}
48
+ value={value}
49
+ FieldProps={FieldProps}
50
+ getOptionLabel={handleGetOrgOptionLabel}
51
+ queryKey={queryKey}
52
+ {...rest}
53
+ loadOptions={handleLoadOptions}
60
54
  />
61
55
  );
62
56
  };
@@ -1,13 +1,25 @@
1
- import { ProviderAutocomplete, ProviderAutocompleteProps } from '@availity/mui-autocomplete';
2
- import { Controller, RegisterOptions, FieldValues } from 'react-hook-form';
3
- import { ControllerProps } from './Types';
1
+ import type { Provider } from '@availity/mui-autocomplete';
2
+ import { handleGetProviderOptionLabel, fetchProviders } from '@availity/mui-autocomplete';
3
+ import type { ChipTypeMap } from '@mui/material/Chip';
4
+ import type { Optional } from './utils';
5
+ import type { ApiConfig } from '@availity/api-axios';
6
+ import { ControlledAsyncAutocomplete, type ControlledAsyncAutocompleteProps } from './AsyncAutocomplete';
4
7
 
5
- export type ControlledProviderAutocompleteProps = Omit<
6
- ProviderAutocompleteProps,
7
- 'onBlur' | 'onChange' | 'value' | 'name'
8
- > &
9
- Pick<RegisterOptions<FieldValues, string>, 'onBlur' | 'onChange' | 'value'> &
10
- ControllerProps;
8
+ export interface ControlledProviderAutocompleteProps<
9
+ Option = Provider,
10
+ Multiple extends boolean | undefined = false,
11
+ DisableClearable extends boolean | undefined = false,
12
+ FreeSolo extends boolean | undefined = false,
13
+ ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'],
14
+ > extends Omit<
15
+ Optional<ControlledAsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>, 'queryKey'>,
16
+ 'loadOptions'
17
+ > {
18
+ /** Customer ID of the Organization you are requesting the providers for */
19
+ customerId: string;
20
+ /** Config passed to the AvProvidersApi.getProviders function */
21
+ apiConfig?: ApiConfig;
22
+ }
11
23
 
12
24
  export const ControlledProviderAutocomplete = ({
13
25
  name,
@@ -18,47 +30,35 @@ export const ControlledProviderAutocomplete = ({
18
30
  shouldUnregister,
19
31
  value,
20
32
  FieldProps,
33
+ apiConfig = {},
34
+ customerId,
35
+ queryKey = 'prov-autocomplete',
21
36
  ...rest
22
37
  }: ControlledProviderAutocompleteProps) => {
38
+ const handleLoadOptions = async (offset: number, limit: number, inputValue: string) => {
39
+ const resp = await fetchProviders(customerId, {
40
+ ...apiConfig,
41
+ params: { ...apiConfig.params, offset, limit, q: inputValue },
42
+ });
43
+
44
+ return resp;
45
+ };
23
46
  return (
24
- <Controller
47
+ <ControlledAsyncAutocomplete
25
48
  name={name}
26
49
  defaultValue={defaultValue}
27
- rules={{
28
- onBlur,
29
- onChange,
30
- shouldUnregister,
31
- value,
32
- ...rules,
33
- }}
50
+ onBlur={onBlur}
51
+ onChange={onChange}
52
+ rules={rules}
34
53
  shouldUnregister={shouldUnregister}
35
- render={({ field: { onChange, value, onBlur }, fieldState: { error } }) => (
36
- <ProviderAutocomplete
37
- {...rest}
38
- FieldProps={{
39
- required: typeof rules.required === 'object' ? rules.required.value : !!rules.required,
40
- ...FieldProps,
41
- error: !!error,
42
- helperText: error?.message ? (
43
- <>
44
- {error.message}
45
- <br />
46
- {FieldProps?.helperText}
47
- </>
48
- ) : (
49
- FieldProps?.helperText
50
- ),
51
- }}
52
- onChange={(event, value, reason) => {
53
- if (reason === 'clear') {
54
- onChange(null);
55
- }
56
- onChange(value);
57
- }}
58
- onBlur={onBlur}
59
- value={value || null}
60
- />
61
- )}
54
+ value={value}
55
+ FieldProps={FieldProps}
56
+ getOptionLabel={handleGetProviderOptionLabel}
57
+ queryOptions={{ enabled: !!customerId }}
58
+ queryKey={queryKey}
59
+ watchParams={{ customerId }}
60
+ {...rest}
61
+ loadOptions={handleLoadOptions}
62
62
  />
63
63
  );
64
64
  };
@@ -16,7 +16,7 @@ const meta: Meta<typeof ControlledTextField> = {
16
16
  ...TextFieldPropsCategorized,
17
17
  helperText: {
18
18
  type: 'string',
19
- table: { category: 'Input Props' }
19
+ table: { category: 'Input Props' },
20
20
  },
21
21
  },
22
22
  };
@@ -25,22 +25,67 @@ export default meta;
25
25
 
26
26
  export const _ControlledTextField: StoryObj<typeof ControlledTextField> = {
27
27
  render: (args: ControlledTextFieldProps) => {
28
- const methods = useForm({values:{ [args.name]: '' }});
28
+ const methods = useForm({ values: { [args.name]: '' } });
29
29
 
30
30
  return (
31
31
  <FormProvider {...methods}>
32
32
  <form onSubmit={methods.handleSubmit((data) => data)}>
33
33
  <ControlledTextField {...args} />
34
34
  <Grid container direction="row" justifyContent="space-between" marginTop={1}>
35
- <Button disabled={!methods?.formState?.isSubmitSuccessful} children="Reset" color="secondary" onClick={() => methods.reset()} />
35
+ <Button
36
+ disabled={!methods?.formState?.isSubmitSuccessful}
37
+ children="Reset"
38
+ color="secondary"
39
+ onClick={() => methods.reset()}
40
+ />
36
41
  <Button type="submit" disabled={methods?.formState?.isSubmitSuccessful} children="Submit" />
37
42
  </Grid>
38
- { methods?.formState?.isSubmitSuccessful ? (
43
+ {methods?.formState?.isSubmitSuccessful ? (
39
44
  <Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
40
45
  <Typography variant="h2">Submitted Values</Typography>
41
46
  <pre data-testid="result">{JSON.stringify(methods.getValues(), null, 2)}</pre>
42
47
  </Paper>
43
- ) : null }
48
+ ) : null}
49
+ </form>
50
+ </FormProvider>
51
+ );
52
+ },
53
+ args: {
54
+ name: 'controlledTextField',
55
+ placeholder: 'Name',
56
+ required: true,
57
+ rules: {
58
+ required: 'This field is required.',
59
+ maxLength: { value: 10, message: 'Too long' },
60
+ },
61
+ label: 'TextField Label',
62
+ showCharacterCount: true,
63
+ },
64
+ };
65
+
66
+ export const _ControlledTextFieldDisplayOverflow: StoryObj<typeof ControlledTextField> = {
67
+ render: (args: ControlledTextFieldProps) => {
68
+ const methods = useForm({ values: { [args.name]: '' } });
69
+
70
+ return (
71
+ <FormProvider {...methods}>
72
+ <form onSubmit={methods.handleSubmit((data) => data)}>
73
+ <ControlledTextField {...args} />
74
+ <Grid container direction="row" justifyContent="space-between" marginTop={1}>
75
+ <Button
76
+ disabled={!methods?.formState?.isSubmitSuccessful}
77
+ children="Reset"
78
+ color="secondary"
79
+ onClick={() => methods.reset()}
80
+ />
81
+ <Button type="submit" disabled={methods?.formState?.isSubmitSuccessful} children="Submit" />
82
+ </Grid>
83
+ {methods?.formState?.isSubmitSuccessful ? (
84
+ <Paper sx={{ padding: '1.5rem', marginTop: '1.5rem' }}>
85
+ <Typography variant="h2">Submitted Values</Typography>
86
+ <pre data-testid="result">{JSON.stringify(methods.getValues(), null, 2)}</pre>
87
+ </Paper>
88
+ ) : null}
44
89
  </form>
45
90
  </FormProvider>
46
91
  );
@@ -52,8 +97,10 @@ export const _ControlledTextField: StoryObj<typeof ControlledTextField> = {
52
97
  required: true,
53
98
  rules: {
54
99
  required: 'This field is required.',
55
- maxLength: { value: 10, message: 'Too long' }
100
+ maxLength: { value: 10, message: 'Too long' },
56
101
  },
57
102
  label: 'TextField Label',
103
+ displayOverflowMaxLength: true,
104
+ showCharacterCount: true,
58
105
  },
59
106
  };
@@ -80,12 +80,14 @@ describe('ControlledTextField', () => {
80
80
  </TestForm>
81
81
  );
82
82
 
83
- expect(getByText('0/20')).toBeTruthy();
83
+ expect(getByText('0')).toBeTruthy();
84
+ expect(getByText('/20')).toBeTruthy();
84
85
 
85
86
  const input = getByTestId('testTextField');
86
87
  fireEvent.change(input, { target: { value: 'Some Text' } });
87
88
 
88
- expect(getByText('9/20')).toBeTruthy();
89
+ expect(getByText('9')).toBeTruthy();
90
+ expect(getByText('/20')).toBeTruthy();
89
91
 
90
92
  fireEvent.change(input, { target: { value: "Some More Text that doesn't fit" } });
91
93
 
@@ -103,12 +105,14 @@ describe('ControlledTextField', () => {
103
105
  />
104
106
  </TestForm>
105
107
  );
106
- expect(getByText('0/20')).toBeTruthy();
108
+ expect(getByText('0')).toBeTruthy();
109
+ expect(getByText('/20')).toBeTruthy();
107
110
 
108
111
  const input = getByTestId('testTextField');
109
112
  fireEvent.change(input, { target: { value: 'Some Text' } });
110
113
 
111
- expect(getByText('9/20')).toBeTruthy();
114
+ expect(getByText('9')).toBeTruthy();
115
+ expect(getByText('/20')).toBeTruthy();
112
116
 
113
117
  fireEvent.change(input, { target: { value: "Some More Text that doesn't fit" } });
114
118
 
@@ -39,7 +39,7 @@ export const ControlledTextField = ({
39
39
  slotProps={{
40
40
  ...rest.slotProps,
41
41
  htmlInput: {
42
- maxLength: rules.maxLength,
42
+ maxLength: typeof rules.maxLength === 'object' ? rules.maxLength.value : rules.maxLength,
43
43
  ...rest.slotProps?.htmlInput,
44
44
  },
45
45
  }}
package/src/lib/Types.tsx CHANGED
@@ -188,6 +188,7 @@ export const AllControllerPropertiesCategorized: CategorizedControllerPropsObjec
188
188
  };
189
189
 
190
190
  export const TextFieldPropsCategorized: TextFieldPropsObject = {
191
+ displayOverflowMaxLength: { table: { category: 'Input Props' } },
191
192
  slotProps: { table: { category: 'Input Props' } },
192
193
  className: { table: { category: 'Input Props' } },
193
194
  style: { table: { category: 'Input Props' } },
@@ -241,6 +242,8 @@ export const RadioGroupPropsCategorized: RadioGroupPropsObject = {
241
242
 
242
243
  export const ProviderAutocompletePropsCategorized: ProviderAutocompletePropsObject = {
243
244
  classes: { table: { category: 'Input Props' } },
245
+ defaultToFirstOption: { table: { category: 'Input Props' } },
246
+ defaultToOnlyOption: { table: { category: 'Input Props' } },
244
247
  id: { table: { category: 'Input Props' } },
245
248
  onKeyDown: { table: { category: 'Input Props' } },
246
249
  sx: { table: { category: 'Input Props' } },
@@ -296,6 +299,8 @@ export const ProviderAutocompletePropsCategorized: ProviderAutocompletePropsObje
296
299
 
297
300
  export const OrganizationAutocompletePropsCategorized: OrganizationAutocompletePropsObject = {
298
301
  classes: { table: { category: 'Input Props' } },
302
+ defaultToFirstOption: { table: { category: 'Input Props' } },
303
+ defaultToOnlyOption: { table: { category: 'Input Props' } },
299
304
  id: { table: { category: 'Input Props' } },
300
305
  onKeyDown: { table: { category: 'Input Props' } },
301
306
  sx: { table: { category: 'Input Props' } },
@@ -399,6 +404,8 @@ export const DatepickerPropsCategorized: DatepickerPropsObject = {
399
404
 
400
405
  export const CodesAutocompletePropsCategorized: CodesAutocompletePropsObject = {
401
406
  classes: { table: { category: 'Input Props' } },
407
+ defaultToFirstOption: { table: { category: 'Input Props' } },
408
+ defaultToOnlyOption: { table: { category: 'Input Props' } },
402
409
  id: { table: { category: 'Input Props' } },
403
410
  onKeyDown: { table: { category: 'Input Props' } },
404
411
  sx: { table: { category: 'Input Props' } },
@@ -454,6 +461,8 @@ export const CodesAutocompletePropsCategorized: CodesAutocompletePropsObject = {
454
461
 
455
462
  export const AsyncAutocompletePropsCategorized: AsyncAutocompletePropsObject = {
456
463
  classes: { table: { category: 'Input Props' } },
464
+ defaultToFirstOption: { table: { category: 'Input Props' } },
465
+ defaultToOnlyOption: { table: { category: 'Input Props' } },
457
466
  id: { table: { category: 'Input Props' } },
458
467
  onKeyDown: { table: { category: 'Input Props' } },
459
468
  sx: { table: { category: 'Input Props' } },
@@ -0,0 +1 @@
1
+ export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;