Jordan Lewallen
Jordan Lewallen

Reputation: 1851

MUI Typescript Autocomplete - wrap component to return scalars values of certain property instead of object

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

Answers (1)

Jordan Lewallen
Jordan Lewallen

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

Related Questions