Skip to content

Commit efef6e6

Browse files
committed
feat(select): add async options to select field
closes #4
1 parent 7f07c5f commit efef6e6

File tree

10 files changed

+155
-26
lines changed

10 files changed

+155
-26
lines changed

src/components/SelectField.tsx

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,59 @@
1+
import {
2+
CircularProgress,
3+
FormControl,
4+
FormHelperText,
5+
InputLabel,
6+
ListItemText,
7+
MenuItem,
8+
Select,
9+
Stack,
10+
Typography,
11+
} from '@mui/material';
112
import { Controller } from 'react-hook-form';
2-
import { FormControl, FormHelperText, InputLabel, ListItemText, MenuItem, Select } from '@mui/material';
3-
import { useFieldLabel } from '../index';
4-
import type { FieldProps } from '../index';
5-
import type { FieldValues } from 'react-hook-form';
13+
import { useAsyncFieldLabels, useFieldLabels, useFieldWithOptionsLabels } from '../index';
14+
import { useMemo } from 'react';
15+
import type { AsyncFieldProps, FieldProps, FieldWithOptionsProps, ObjectLike } from '../index';
616
import type {
17+
CircularProgressProps,
718
FormControlProps,
819
FormHelperTextProps,
920
InputLabelProps,
1021
ListItemTextProps,
1122
MenuItemProps,
1223
SelectProps,
24+
StackProps,
25+
TypographyProps,
1326
} from '@mui/material';
27+
import type { FieldValues } from 'react-hook-form';
1428

1529
interface MuiProps {
1630
formControlProps?: FormControlProps;
1731
selectProps?: SelectProps;
1832
inputLabelProps?: InputLabelProps;
1933
menuItemProps?: MenuItemProps;
34+
loadingMenuItemProps?: MenuItemProps;
35+
errorMenuItemProps?: MenuItemProps;
36+
noOptionsMenuItemProps?: MenuItemProps;
2037
listItemTextProps?: ListItemTextProps;
2138
formHelperTextProps?: FormHelperTextProps;
39+
loadingTypographyProps?: TypographyProps;
40+
errorTypographyProps?: TypographyProps;
41+
noOptionsTypographyProps?: TypographyProps;
42+
loadingStackProps?: StackProps;
43+
loadingCircularProgressProps?: CircularProgressProps;
2244
}
2345

24-
export interface SelectFieldProps<T extends FieldValues, V extends Record<string, unknown>> extends FieldProps<T> {
46+
export interface SelectFieldProps<T extends FieldValues, V extends ObjectLike>
47+
extends FieldProps<T>,
48+
AsyncFieldProps,
49+
FieldWithOptionsProps<V> {
2550
muiProps?: MuiProps;
26-
options: V[];
2751
optionValueAccessor: (value: V) => string | number;
2852
optionLabelAccessor: (value: V) => string;
2953
optionExtraLabelAccessor?: (value: V) => string;
3054
}
3155

32-
export const SelectField = <T extends FieldValues, V extends Record<string, unknown>>({
56+
export const SelectField = <T extends FieldValues, V extends ObjectLike>({
3357
control,
3458
label,
3559
name,
@@ -41,8 +65,17 @@ export const SelectField = <T extends FieldValues, V extends Record<string, unkn
4165
optionLabelAccessor,
4266
optionValueAccessor,
4367
optionExtraLabelAccessor,
68+
loadingErrorLabel,
69+
isLoading = false,
70+
loadingLabel,
71+
isError = false,
72+
noOptionsLabel,
4473
}: SelectFieldProps<T, V>) => {
45-
const { fieldLabel } = useFieldLabel({ isOptional, label, requiredLabel });
74+
const { fieldLabel } = useFieldLabels({ isOptional, label, requiredLabel });
75+
const { fieldLoadingErrorLabel, fieldLoadingLabel } = useAsyncFieldLabels({ loadingErrorLabel, loadingLabel });
76+
const { fieldNoOptionsLabel } = useFieldWithOptionsLabels({ noOptionsLabel });
77+
78+
const displayOptions = useMemo(() => !isLoading && !isError, [isError, isLoading]);
4679

4780
return (
4881
<Controller
@@ -61,7 +94,7 @@ export const SelectField = <T extends FieldValues, V extends Record<string, unkn
6194
aria-required={isOptional ? 'false' : 'true'}
6295
label={fieldLabel}
6396
renderValue={(selected) => {
64-
const found = options.find((option) => optionValueAccessor(option) === selected);
97+
const found = options?.find((option) => optionValueAccessor(option) === selected);
6598

6699
if (found) {
67100
return optionLabelAccessor(found);
@@ -70,19 +103,56 @@ export const SelectField = <T extends FieldValues, V extends Record<string, unkn
70103
return '';
71104
}}
72105
>
73-
{options.map((option) => (
106+
{isLoading && (
107+
<MenuItem
108+
{...muiProps?.loadingMenuItemProps}
109+
disabled
110+
>
111+
<Stack
112+
alignItems='center'
113+
flexDirection='row'
114+
justifyContent='space-between'
115+
width='100%'
116+
{...muiProps?.loadingStackProps}
117+
>
118+
<Typography {...muiProps?.loadingTypographyProps}>{fieldLoadingLabel}</Typography>
119+
<CircularProgress
120+
size={30}
121+
{...muiProps?.loadingCircularProgressProps}
122+
/>
123+
</Stack>
124+
</MenuItem>
125+
)}
126+
{isError && (
127+
<MenuItem
128+
{...muiProps?.errorMenuItemProps}
129+
disabled
130+
>
131+
<Typography {...muiProps?.errorTypographyProps}>{fieldLoadingErrorLabel}</Typography>
132+
</MenuItem>
133+
)}
134+
{displayOptions && options?.length === 0 && (
74135
<MenuItem
75-
{...muiProps?.menuItemProps}
76-
key={optionValueAccessor(option)}
77-
value={optionValueAccessor(option)}
136+
disabled
137+
{...muiProps?.noOptionsMenuItemProps}
78138
>
79-
<ListItemText
80-
{...muiProps?.listItemTextProps}
81-
primary={optionLabelAccessor(option)}
82-
secondary={optionExtraLabelAccessor?.(option)}
83-
/>
139+
<Typography {...muiProps?.noOptionsTypographyProps}>{fieldNoOptionsLabel}</Typography>
84140
</MenuItem>
85-
))}
141+
)}
142+
{displayOptions &&
143+
options?.map((option) => (
144+
<MenuItem
145+
{...muiProps?.menuItemProps}
146+
key={optionValueAccessor(option)}
147+
value={optionValueAccessor(option)}
148+
>
149+
<ListItemText
150+
{...muiProps?.listItemTextProps}
151+
primary={optionLabelAccessor(option)}
152+
secondary={optionExtraLabelAccessor?.(option)}
153+
/>
154+
</MenuItem>
155+
))}
86156
</Select>
87157
{invalid && (
88158
<FormHelperText {...muiProps?.formHelperTextProps}>

src/components/TextField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Controller } from 'react-hook-form';
22
import { TextField as MuiTextField } from '@mui/material';
3-
import { useFieldLabel } from '../index';
3+
import { useFieldLabels } from '../index';
44
import type { FieldProps } from '../index';
55
import type { FieldValues } from 'react-hook-form';
66
import type { TextFieldProps as MuiTextFieldProps } from '@mui/material';
@@ -22,7 +22,7 @@ export const TextField = <T extends FieldValues>({
2222
requiredLabel,
2323
onErrorMessage,
2424
}: TextFieldProps<T>) => {
25-
const { fieldLabel } = useFieldLabel({ isOptional, label, requiredLabel });
25+
const { fieldLabel } = useFieldLabels({ isOptional, label, requiredLabel });
2626

2727
return (
2828
<Controller

src/hooks/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export * from './useFieldLabel';
1+
export * from './useFieldLabels';
2+
export * from './useAsyncFieldLabels';
3+
export * from './useFieldWithOptionsLabels';

src/hooks/useAsyncFieldLabels.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useMemo } from 'react';
2+
import { useMuiFormConfig } from '../index';
3+
import type { AsyncFieldProps } from '../index';
4+
5+
interface Props extends Omit<AsyncFieldProps, 'isLoading'> {}
6+
7+
export const useAsyncFieldLabels = ({ loadingErrorLabel, loadingLabel }: Props) => {
8+
const { globalLoadingErrorLabel, globalLoadingLabel } = useMuiFormConfig();
9+
10+
const fieldLoadingLabel = useMemo(() => loadingLabel || globalLoadingLabel, [loadingLabel, globalLoadingLabel]);
11+
const fieldLoadingErrorLabel = useMemo(
12+
() => loadingErrorLabel || globalLoadingErrorLabel,
13+
[loadingErrorLabel, globalLoadingErrorLabel],
14+
);
15+
16+
return { fieldLoadingErrorLabel, fieldLoadingLabel };
17+
};

src/hooks/useFieldLabel.ts renamed to src/hooks/useFieldLabels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface Props extends Pick<FieldProps<FieldValues>, 'requiredLabel'> {
88
isOptional: boolean;
99
}
1010

11-
export const useFieldLabel = ({ label, isOptional, requiredLabel }: Props) => {
11+
export const useFieldLabels = ({ label, isOptional, requiredLabel }: Props) => {
1212
const { globalRequiredLabel } = useMuiFormConfig();
1313

1414
const fieldLabel = useMemo(() => {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useMemo } from 'react';
2+
import { useMuiFormConfig } from '../index';
3+
import type { FieldWithOptionsProps, ObjectLike } from '../index';
4+
5+
interface Props extends Pick<FieldWithOptionsProps<ObjectLike>, 'noOptionsLabel'> {}
6+
7+
export const useFieldWithOptionsLabels = ({ noOptionsLabel }: Props) => {
8+
const { globalNoOptionsLabel } = useMuiFormConfig();
9+
10+
const fieldNoOptionsLabel = useMemo(
11+
() => noOptionsLabel || globalNoOptionsLabel,
12+
[globalNoOptionsLabel, noOptionsLabel],
13+
);
14+
15+
return { fieldNoOptionsLabel };
16+
};

src/providers/MuiFormConfigProvider.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ import type { PropsWithChildren } from 'react';
55
* Types
66
*/
77
export interface MuiFormConfig {
8-
globalRequiredLabel: string | (() => string);
8+
globalRequiredLabel: string;
9+
globalLoadingErrorLabel: string;
10+
globalNoOptionsLabel: string;
11+
globalLoadingLabel: string;
912
}
1013

1114
interface MuiFormConfigProps extends PropsWithChildren {
12-
config: MuiFormConfig;
15+
config: Partial<MuiFormConfig>;
1316
}
1417

1518
/**
1619
* Initializations
1720
*/
1821
export const defaultValues: MuiFormConfig = {
22+
globalLoadingErrorLabel: 'Error getting the data.',
23+
globalLoadingLabel: 'Loading...',
24+
globalNoOptionsLabel: 'No options.',
1925
globalRequiredLabel: '*',
2026
};
2127

@@ -43,7 +49,10 @@ export const useMuiFormConfig = () => {
4349
* Provider
4450
*/
4551
export const MuiFormConfigProvider = ({ children, config }: MuiFormConfigProps) => {
46-
const [configValue] = useState<MuiFormConfig>(config);
52+
const [configValue] = useState<MuiFormConfig>({
53+
...defaultValues,
54+
...config,
55+
});
4756

4857
return <MuiFormConfigContext.Provider value={configValue}>{children}</MuiFormConfigContext.Provider>;
4958
};

src/types/fields.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Control, FieldPath, FieldValues } from 'react-hook-form';
2+
import { ObjectLike } from './utils';
23

34
export interface FieldProps<T extends FieldValues> {
45
control: Control<T>;
@@ -8,3 +9,15 @@ export interface FieldProps<T extends FieldValues> {
89
requiredLabel?: string;
910
onErrorMessage?: (error: string) => string;
1011
}
12+
13+
export interface AsyncFieldProps {
14+
loadingLabel?: string;
15+
isLoading?: boolean;
16+
loadingErrorLabel?: string;
17+
isError?: boolean;
18+
}
19+
20+
export interface FieldWithOptionsProps<V extends ObjectLike> {
21+
options?: V[];
22+
noOptionsLabel?: string;
23+
}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './fields';
2+
export * from './utils';

src/types/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type ObjectLike = Record<string, any>;

0 commit comments

Comments
 (0)