-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[base-ui][material-ui][Autocomplete] Better support for uncontrolled autocomplete in HTML form submissions #43544
Comments
Thanks for opening an issue. This is somewhat similar to #42988, but that one deals with multiple options using React state. Providing the selected option as a second argument in the import React from 'react';
import { Autocomplete, TextField } from '@mui/material';
const movies = [
{ id: 1, title: 'Inception', year: 2010 },
{ id: 2, title: 'Interstellar', year: 2014 },
{ id: 3, title: 'The Dark Knight', year: 2008 },
];
export default function MovieAutocomplete() {
const hiddenInputRef = React.useRef(null);
const handleChange = (event, newValue) => {
if (hiddenInputRef.current) {
hiddenInputRef.current.value = newValue ? newValue.id : '';
}
};
return (
<form action="/your-form-endpoint" method="POST">
<Autocomplete
options={movies}
getOptionLabel={(option) => `${option.title} (${option.year})`}
onChange={handleChange}
renderInput={(params) => (
<>
<TextField
{...params}
label="Select a movie"
variant="outlined"
/>
<input
type="hidden"
name="movie_id"
ref={hiddenInputRef}
/>
</>
)}
/>
<button type="submit">Submit</button>
</form>
);
} This should also work if you have duplicate options, but it's a workaround. We might consider adding more uncontrolled Autocomplete features, possibly in Base UI. Another one related to uncontrolled Autocomplete within a form is #40252. I'll mark this as a new feature request. |
Agree that #42988 is very similar. @ZeeshanTamboli the solution you suggested in that issue uses state, but I notice that the comments and the example code provided by the issue reporter do not. I'm aware I can use a ref in the way you describe but, (like the other issue reporter), was hoping that MUI would provide a simple form-compatible API out of the box. I think it makes a lot of sense for the library to do that. If it's any help to others facing the same issue, I'm running with the below component for now. It has some rough edges but it's working for our needs at the moment. If anyone picks it up and enhances it any further I welcome you to share that code here. (I still hope that the library will provide this behavior natively, though.) import {
AutocompleteOwnerState,
AutocompleteRenderGetTagProps,
Chip,
ChipTypeMap,
Autocomplete as MuiAutocomplete,
AutocompleteProps as MuiAutocompleteProps,
} from "@mui/material";
import React from "react";
type AutocompleteProps<
Value,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
ChipComponent extends React.ElementType = ChipTypeMap["defaultComponent"],
> = MuiAutocompleteProps<
Value,
Multiple,
DisableClearable,
FreeSolo,
ChipComponent
> & {
name?: string;
getInputValue?: (option: Value) => string | number;
findInputValue?: (
optionLabel?: string | number | readonly string[],
) => string | number | undefined;
};
const defaultRenderTags = <
Value,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
ChipComponent extends React.ElementType = ChipTypeMap["defaultComponent"],
>(
value: Value[],
getTagProps: AutocompleteRenderGetTagProps,
ownerState: AutocompleteOwnerState<
Value,
Multiple,
DisableClearable,
FreeSolo,
ChipComponent
>,
) =>
value.map((option, index) => (
<Chip
{...getTagProps({ index })}
label={ownerState.getOptionLabel?.(option) ?? option}
/>
));
const Autocomplete = <
Value,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined = false,
ChipComponent extends React.ElementType = ChipTypeMap["defaultComponent"],
>(
props: AutocompleteProps<
Value,
Multiple,
DisableClearable,
FreeSolo,
ChipComponent
>,
) => {
const { name, getInputValue, findInputValue, ...muiProps } = props;
if (muiProps.value) {
return <MuiAutocomplete {...muiProps} />;
}
if (muiProps.multiple) {
return (
<MuiAutocomplete
{...muiProps}
renderTags={(value, getTagProps, ownerState) => {
const renderTags = props.renderTags || defaultRenderTags;
return (
<>
{renderTags(value, getTagProps, ownerState)}
{value.map((option, index) => {
const { key } = getTagProps({ index });
const value =
getInputValue?.(option) ??
muiProps.getOptionLabel?.(option) ??
String(option);
return (
<input
key={key}
type="hidden"
name={`${name ?? ""}[]`}
value={value}
/>
);
})}
</>
);
}}
/>
);
}
return (
<MuiAutocomplete
{...muiProps}
renderInput={(params) => {
return (
<>
{muiProps.renderInput(params)}
<input
type="hidden"
name={name ?? ""}
value={
findInputValue?.(params.inputProps.value) ??
params.inputProps.value
}
/>
</>
);
}}
/>
);
};
export default Autocomplete; |
Summary
Autocomplete components should work in a regular html form submission, by accepting a
name
prop and adding<input type="hidden" name={nameProp} value={value}/>
elements, when thevalue
prop is not being controlled.If the
multiple
prop istrue
, then a list of hidden input elements should be added using field name array syntax (i.e.name={`${nameProp}[]`})
)Docs should note that when using in uncontrolled mode, the
name
prop should be added to the autocomplete component, and not manually added to the element returned byrenderInput
(usually a TextField), to prevent duplicate entries in the form submission.Ideally, there ought to be a mechanism to specify what the
value
of the hidden input should be, based on the selected option(s). For example in an autocomplete I may want to show a list of movie names, concatenated with the year. When the user selects one, I likely want to send the movieid
in the form submission, not the option label.Currently, I can create the hidden inputs manually fairly easily if it is a multi select, by adding these props:
Or if it is a single select, and the value I want as part of the form submission is the same as the option label, I can just add the
name
prop to the TextField inrenderInput
and I don't need a hidden input:However, if it's a single select and the value I want as part of the form submission is not equal to the option label, I need to add the hidden input field to the
renderInput
prop, and ensure the field name is attached to the hidden input, not the TextField input. Since the selectedoption
is not available in theparams
arg ofrenderInput
, the only thing I can do is a reverse lookup to find theid
based on the option label (and hope there are not two movies with the same title and year in my dataset).This is the only case where as far as I can tell with the current API it is impossible to achieve something satisfactory in user-land, and still use an uncontrolled component. If the
renderInput
callback was provided the selected option as a second arg, we would at least be able to cover this base.For completeness I note that if you use state, you can make even that last use-case work. However enabling the Autocomplete component to operate as an uncontrolled component as part of a form is a big enough win for simplicity for me to feel this feature request is warranted, (in many cases is has positive performance implications too).
Examples
Motivation
With React Router data router patterns, (or Remix) we are able to simplify some aspects of writing forms in React apps by using the normal HTML form pattern. With autocomplete components this is tricky.
Search keywords: uncontrolled autocomplete
The text was updated successfully, but these errors were encountered: