Reputation: 1851
when using MUI Autocomplete, the generic value defined in onChange
/ value
is the interface of the object set in the options
property.
For example, in an autocomplete with the following:
<Autocomplete
options={top100Films}
onChange={onChange}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
Where top100Films
has the interface:
interface Props {
id: number;
label: string
}
The onChange
and value
properties would have the type of Props
....which is an object. Now in my form, I'd only like to submit the id
of the Props
object. As a result, I would like to have the type of my Autocomplete value
propery equal number
.
Therefore, I think we need to wrap the Autocomplete
so that we can modify the value
and onChange
values. My initial thought for this was to do the following:
const [value, setValue] = useState<number>();
const onChange = useCallback((value: number) => {
})
<WrappedAutocomplete
valueProp={"id"} // <-- this defines the property that we should be extracting the value from. It's a scalar. Should only be able to select from keys of the Props interface.
options={top100Films} // <-- nothing changed here, this still has the interface object of Props
value={value} // <-- assuming this is a controlled form value, this value should match the type of "id", in this case, number.
onChange={onChange} // <-- other half of controlled component, this should be (value: TypeExtractedFromValueProp) => void;
{...allTheOtherAutocompleteProps}
/>
Also something to note is that Autocompletes
take a multiple
attribute. If it is true
that means that value
would need to be the Array
type of the scalar defined by valueProp
.
Does this issue make sense? Some further reading can be found here: https://github.com/mui/material-ui/issues/23708
For those talented with Typescript, I could really use your help on defining the implementation of a WrappedAutocomplete
!
Upvotes: 2
Views: 2605
Reputation: 1851
Here's my (successful) attempt, please let me know if there's a better implementation. Note, I used Except
which is essentially Omit
from type-fest
but it actually removes the property from the type unlike Omit
.
I created a new interface to &
with existing props:
interface CustomProps<
TOption extends Record<keyof TOption, TOption[keyof TOption]>,
TInternal extends TOption[TProp],
TProp extends keyof TOption,
TLabel extends keyof TOption,
Multiple extends boolean | undefined = undefined,
DisableClearable extends boolean | undefined = undefined,
FreeSolo extends boolean | undefined = undefined,
> {
valueKey: TProp;
labelKey: TLabel;
value: AutocompleteValue<TInternal, Multiple, DisableClearable, FreeSolo>;
onChange: (value: AutocompleteValue<TInternal, Multiple, DisableClearable, FreeSolo>) => void;
options: TOption[]
}
function WrappedAutocomplete<
TOption extends Record<keyof TOption, TOption[keyof TOption]>,
TInternal extends TOption[TProp],
TProp extends keyof TOption,
TLabel extends keyof TOption,
Multiple extends boolean | undefined = undefined,
DisableClearable extends boolean | undefined = undefined,
FreeSolo extends boolean | undefined = undefined,
>({ valueKey, value, labelKey, onChange, ...props }: Except<AutocompleteProps<TInternal, Multiple, DisableClearable, FreeSolo>, "value" | "onChange" | "options"> & CustomProps<TOption, TInternal, TProp, TLabel, Multiple, DisableClearable, FreeSolo>) {
const options = props.options.map((option) => option[valueKey]);
return (
<>
<Autocomplete
{...props}
options={options}
value={value}
onChange={(event, value) => {
onChange(value)
}}
getOptionLabel={(option) => {
if (typeof option === "string") {
return option;
} else {
const item = props.options.find((o) => o[valueKey] === option)
if (!!item) {
return item[labelKey]
} else {
return "NOT FOUND"
}
}
}}
/>
</>
)
}
So now you can do something like this:
<WrappedAutocomplete
multiple
options={top100Films}
valueKey={"id"}
labelKey={"label"}
value={value} <--- since multiple is true, this is number[]
disabled={disabled}
onChange={(value) => {
onChange(value) <-- value is also number[] instead of Props type
}}
renderInput={(params) => (
<TextField
{...params}
label={label}
inputProps={{
...params.inputProps,
autoComplete: 'new-password', // disable autocomplete and autofill
}}
helperText={helperText}
/>
)}
/>
Upvotes: 4