Material-ui: [Autocomplete] Add getOptionSelected prop

Created on 18 Nov 2019  路  36Comments  路  Source: mui-org/material-ui

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Summary 馃挕

To bring more flexibility to the Autocomplete component, It will be great to be able customize the behavior on when an option should be selected by exposing a getOptionSelected prop similar to getOptionDisabled.

Examples 馃寛

const RoleSelect = (props) => {


  const [role, setRole] = useState();
  /*
   * options format is: [{id: 1, name: "The Role"}]
   */
  const { options: optionsProp } = props;

  return (
    <Autocomplete
      value={role}
      options={optionsProp}
      filterOptions={(options, state) => {
        /*
         * Custom function returns a transformed array of options with the format:
         * [{item: {id: 1, name: "The Role"}, matches: []}]
         */
        return myCustomFuse.search(options, state.inputValue)
      }}
      getOptionSelected={(option, { multiple, value }) => {
         if (!multiple) {
          /*
           * PROPOSAL for single selection, be able to provide own logic.
           */     
          return (option.item.id === value.id);  
         }

         return false;
      }}
      renderOption={(option) => {
        /*
         * matches property is used with a custom Highlight component. 
         */
        return (
          <Highlight matches={option.matches}>
            {option.item.name}
          </Highlight>
        );
      }},
      onChange={(event, newValue) => {
        // Only need the item in the value, ignore matches property.
        setRole(newValue.item)
      }}
    />
  )

}

Motivation 馃敠


Currently I'm trying to use https://fusejs.io/ in the prop filterOptions to filter the options, but fuseJs wraps the options on another object, a simple comparison as in here won't work. also I think adding getOptionSelected will allow to compare by id if is wished value.id === option.id. Let me know your thoughts and if you have a better way to accomplish what I'm trying to do, thanks!

Autocomplete enhancement good first issue

Most helpful comment

You guys are awesome!
Thanks!

All 36 comments

@miguelbalboa I'm not sure that getOptionSelected() would cover all the problems. Returning a different structure in the filter step impacts getOptionLabel and onChange too. Would you confirm?

What's wrong with?

<Autocomplete
  value={role}
  options={optionsProp}
  filterOptions={(options, state) => {
    /*
     * Custom function returns a transformed array of options with the format:
     * [{item: {id: 1, name: "The Role"}, matches: []}]
     */
    return myCustomFuse.search(options, state.inputValue).map(x => {
      x.item.matches = x.matches
      return x.item
    })
  }}
/>

Hi @oliviertassinari, yes, returning a different structure will impact getOptionLabel and onChange, the developer should be aware of that, maybe is not a good idea. There is noting absolute wrong in your example, actually I'm goin to use it in that way right now, Thank you!

Coming back to the initial proposal about exposing getOptionSelected, usually I get the initial selected value from a different source that the options Array. that means that initially, the Object value will be different from the Object option, just doing value === option as in useAutocomplete will not initially select the option.

const RoleSelect = () => {

  // Value will come from different source with a pre-populated initial value.
  const [role, setRole] = useState({
    id: 2,
    name: "Second Role"
  });

  // Options are in different source
  const [roles, setRoles] = useState([
    {id: 1, name: "My Role"},
    {id: 2, name: "Second Role"}
  ]);

  // Because role is a different Object that roles[1], the selected option will not be highlighted. 
  return (
    <Autocomplete
      value={role}
      options={roles}
      getOptionLabel={role => role.name}
      onChange={(event, newValue) => {
        setRole(newValue);
      }}
      renderInput={params => (
        <TextField {...params} label="Role select" fullWidth />
      )}
    />
  );
};

That is why I thought about providing a way to specifically do value.id === option.id. Let me know if you have a better solution for this case. Thanks!

@miguelbalboa This is interesting, they are ways to work around the problem, but it seems valid but a getOptionSelected prop could make it simpler. I think that we would accept a pull request for it.

Great! I will work in the pull request.

+1. Also need this feature, because in my case value object and any of options objects cannot be simply compared, i need some way to compare value.id with option.id, maybe need some getOptionValue prop as in react-select

We have a use case in the documentation for this method: http://material-ui.com/components/autocomplete#asynchronous-requests. The options are fetched from the network, they don't match with a previously selected value.

Any news about this the PR from this issue ?

@florleb Do you want to give it a shot?

@florleb Do you want to give it a shot?

I wish i could if i was better :/. What should i do ?

Hello @florleb are you working on it?

Regarding the changes, I think that it could go with something in this order:

diff --git a/docs/src/pages/components/autocomplete/Asynchronous.tsx b/docs/src/pages/components/autocomplete/Asynchronous.tsx
index acd873074..fb9c2d4b5 100644
--- a/docs/src/pages/components/autocomplete/Asynchronous.tsx
+++ b/docs/src/pages/components/autocomplete/Asynchronous.tsx
@@ -59,6 +59,7 @@ export default function Asynchronous() {
       onClose={() => {
         setOpen(false);
       }}
+      getOptionSelected={(option, value) => option.name === value.name}
       getOptionLabel={option => option.name}
       options={options}
       loading={loading}
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
index b3f87c525..f7fa6cae7 100644
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -204,6 +204,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
     freeSolo = false,
     getOptionDisabled,
     getOptionLabel = x => x,
+    getOptionSelected,
     groupBy,
     id: idProp,
     includeInputInList = false,
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
index d67795e0b..310167c6c 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
@@ -90,6 +90,11 @@ export interface UseAutocompleteProps {
    * It's used to fill the input (and the list box options if `renderOption` is not provided).
    */
   getOptionLabel?: (option: any) => string;
+  /**
+   * Used to determine if an option is selected.
+   * Uses strict equality by default.
+   */
+  getOptionSelected?: (option: any, value: any) => boolean;
   /**
    * If provided, the options will be grouped under the returned string.
    * The groupBy value is also used as the text for group headings when `renderGroup` is not provided.
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index a8fd29e1a..bda4a6720 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -83,6 +83,7 @@ export default function useAutocomplete(props) {
     freeSolo = false,
     getOptionDisabled,
     getOptionLabel = x => x,
+    getOptionSelected = (option, value) => option === value,
     groupBy,
     id: idProp,
     includeInputInList = false,
@@ -239,7 +240,9 @@ export default function useAutocomplete(props) {
         options.filter(option => {
           if (
             filterSelectedOptions &&
-            (multiple ? value.indexOf(option) !== -1 : value === option)
+            (multiple ? value : [value]).some(
+              value2 => value2 !== null && getOptionSelected(option, value2),
+            )
           ) {
             return false;
           }
@@ -791,7 +794,9 @@ export default function useAutocomplete(props) {
       },
     }),
     getOptionProps: ({ index, option }) => {
-      const selected = multiple ? value.indexOf(option) !== -1 : value === option;
+      const selected = (multiple ? value : [value]).some(
+        value2 => value2 != null && getOptionSelected(option, value2),
+      );
       const disabled = getOptionDisabled ? getOptionDisabled(option) : false;

       return {

Well finally i don't use defaultValue but i use value instead and it doesnt seems to have the problem anymore.

Hi @oliviertassinari i opened up this issue #18499

I was going to make a pr for this but not sure if that's needs or not.

since i would need to loop through the selectedValue in renderOption

        renderOption={(option, { selected, inputValue }) => (
            <React.Fragment>
              <Checkbox
                icon={icon}
                checkedIcon={checkedIcon}
                style={{ marginRight: 8 }}
                checked={selected}
              />
              {option.title}
            </React.Fragment>
          )} 

as long as i have value not sure adding another props getOptionSelected would help anyway.

However, if value can be return in the renderOption alongwith selected and inputValue should suffice this use case

right now, it only returning

        {renderOption(option, {
          selected: optionProps["aria-selected"],
          inputValue
        })}

adding value to the above function should give some flexibility to show the selected option

Just a thought, lemme know if that makes sense

@DarkKnight1992 We have a use case in the demo, where we fetch new options from the API each time the autocomplete popup is displayed. Without this getOptionSelected prop, we would need to alter the returned options from the API, to replace the selected options, with the reference from the value.

In your case, it wouldn't be enough. The autocomplete relies on the selected state to implement some behavior correctly, like for accessibility and keyboard navigation. If this state is wrong, you can stop right here, fixing the options display wouldn't be enough.

makes sense, thanks. Will send pr soon with getOptionSelected

Regarding the changes, I think that it could go with something in this order:

diff --git a/docs/src/pages/components/autocomplete/Asynchronous.tsx b/docs/src/pages/components/autocomplete/Asynchronous.tsx
index acd873074..fb9c2d4b5 100644
--- a/docs/src/pages/components/autocomplete/Asynchronous.tsx
+++ b/docs/src/pages/components/autocomplete/Asynchronous.tsx
@@ -59,6 +59,7 @@ export default function Asynchronous() {
       onClose={() => {
         setOpen(false);
       }}
+      getOptionSelected={(option, value) => option.name === value.name}
       getOptionLabel={option => option.name}
       options={options}
       loading={loading}
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
index b3f87c525..f7fa6cae7 100644
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -204,6 +204,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
     freeSolo = false,
     getOptionDisabled,
     getOptionLabel = x => x,
+    getOptionSelected,
     groupBy,
     id: idProp,
     includeInputInList = false,
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
index d67795e0b..310167c6c 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
@@ -90,6 +90,11 @@ export interface UseAutocompleteProps {
    * It's used to fill the input (and the list box options if `renderOption` is not provided).
    */
   getOptionLabel?: (option: any) => string;
+  /**
+   * Used to determine if an option is selected.
+   * Uses strict equality by default.
+   */
+  getOptionSelected?: (option: any, value: any) => boolean;
   /**
    * If provided, the options will be grouped under the returned string.
    * The groupBy value is also used as the text for group headings when `renderGroup` is not provided.
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index a8fd29e1a..bda4a6720 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -83,6 +83,7 @@ export default function useAutocomplete(props) {
     freeSolo = false,
     getOptionDisabled,
     getOptionLabel = x => x,
+    getOptionSelected = (option, value) => option === value,
     groupBy,
     id: idProp,
     includeInputInList = false,
@@ -239,7 +240,9 @@ export default function useAutocomplete(props) {
         options.filter(option => {
           if (
             filterSelectedOptions &&
-            (multiple ? value.indexOf(option) !== -1 : value === option)
+            (multiple ? value : [value]).some(
+              value2 => value2 !== null && getOptionSelected(option, value2),
+            )
           ) {
             return false;
           }
@@ -791,7 +794,9 @@ export default function useAutocomplete(props) {
       },
     }),
     getOptionProps: ({ index, option }) => {
-      const selected = multiple ? value.indexOf(option) !== -1 : value === option;
+      const selected = (multiple ? value : [value]).some(
+        value2 => value2 != null && getOptionSelected(option, value2),
+      );
       const disabled = getOptionDisabled ? getOptionDisabled(option) : false;

       return {

the selected state is correctly thanks to these changes but now i can select the same option twice when multiple. Any suggestion having a some trouble tracking why is this happening

figured out the issue but not sure how should fix them another prop maybe ? not sure

const itemIndex = value.indexOf(item);

so selecting and deselecting also relies on reference

@DarkKnight1992 Interesting, I think that we can use an IE 11 compatible version of Array.prototype.findIndex.

@oliviertassinari Array.prototype.findIndex behave the same way. That's kinda makes since i am const itemIndex = value.indexOf(item); or const itemIndex = value.findIndex(v => v === item); for object equality and since object can't be equal without using reference, the rules holds up well or well atleast that's what i have known.

Lemme know if i am doing anything wrong here.

@DarkKnight1992 You can use findIndex with getOptionSelected.

@oliviertassinari getOptionSelected is working as expected since i am not comparing objects but a property in that option

getOptionSelected={(option, value) => {
    return option.title === value.title;
 }}

i am working on codesandox liveview, do you want to take a look into that ?

@DarkKnight1992 If you want, you can open a draft pull request, and I will have a look.

@oliviertassinari draft pull requested #18695

You guys are awesome!
Thanks!

Hi! Can i use default selected prop from string, if i use async autocomplete?
That is, I need to use string value in default, before data is loading. For example, default value could be like country name from my state manager, and use in async request if i change this autocomplete field.

@Legilimens Yes.

@Legilimens Yes.

Could you give an example, please? I tried to do this, but could not. I found exapmple how use default value for some option value, but i not found how i can use any string for default value

I can't here, please ask on StackOverflow.

For the same reasons, this property (getOptionSelected) should be added to the \ component.

What do you think @DarkKnight1992 ?

@JuanmaMenendez select already works if you provide a primitive value

Yes but I need to provide and select an object, not a primitive value.

@oliviertassinari is that's possible ?

@DarkKnight1992 It's supported but discouraged with the non-native select. It's not supported by the native select.

I have a warning when using this prop, not sure if there's anything I can do about it:

Warning: React does not recognize the `getOptionSelected` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `getoptionselected` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
    in div (created by ForwardRef(Autocomplete))
    in ForwardRef(Autocomplete) (created by WithStyles(ForwardRef(Autocomplete)))
    in WithStyles(ForwardRef(Autocomplete)) (created by Field)

@zeljkocurcic Upgrade to the latest version.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

finaiized picture finaiized  路  3Comments

anthony-dandrea picture anthony-dandrea  路  3Comments

reflog picture reflog  路  3Comments

mb-copart picture mb-copart  路  3Comments

FranBran picture FranBran  路  3Comments