Skip to content
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

[Autocomplete] Add getOptionSelected prop #18443

Closed
1 task done
miguelbalboa opened this issue Nov 18, 2019 · 38 comments · Fixed by #18695
Closed
1 task done

[Autocomplete] Add getOptionSelected prop #18443

miguelbalboa opened this issue Nov 18, 2019 · 38 comments · Fixed by #18695
Assignees
Labels
component: autocomplete This is the name of the generic UI component, not the React module! good first issue Great for first contributions. Enable to learn the contribution process. new feature New feature or request

Comments

@miguelbalboa
Copy link

  • 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!

@oliviertassinari
Copy link
Member

@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?

@oliviertassinari
Copy link
Member

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
    })
  }}
/>

@oliviertassinari oliviertassinari added the component: autocomplete This is the name of the generic UI component, not the React module! label Nov 18, 2019
@miguelbalboa
Copy link
Author

miguelbalboa commented Nov 18, 2019

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!

@oliviertassinari oliviertassinari added the new feature New feature or request label Nov 19, 2019
@oliviertassinari
Copy link
Member

@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.

@miguelbalboa
Copy link
Author

Great! I will work in the pull request.

@oliviertassinari oliviertassinari added the good first issue Great for first contributions. Enable to learn the contribution process. label Nov 19, 2019
@rovnyart
Copy link

+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

@oliviertassinari
Copy link
Member

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.

@oliviertassinari oliviertassinari changed the title [AutoComplete] expose getOptionSelected prop [Autocomplete] Add getOptionSelected prop Nov 27, 2019
@florleb
Copy link

florleb commented Nov 27, 2019

Any news about this the PR from this issue ?

@oliviertassinari
Copy link
Member

@florleb Do you want to give it a shot?

@florleb
Copy link

florleb commented Nov 27, 2019

@florleb Do you want to give it a shot?

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

@SandraMarcelaHerreraArriaga
Copy link
Contributor

SandraMarcelaHerreraArriaga commented Nov 29, 2019

Hello @florleb are you working on it?

@oliviertassinari
Copy link
Member

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 {

@florleb
Copy link

florleb commented Dec 2, 2019

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

@DarkKnight1992
Copy link
Contributor

DarkKnight1992 commented Dec 4, 2019

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

@oliviertassinari
Copy link
Member

oliviertassinari commented Dec 4, 2019

@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.

@DarkKnight1992
Copy link
Contributor

DarkKnight1992 commented Dec 4, 2019

makes sense, thanks. Will send pr soon with getOptionSelected

@DarkKnight1992
Copy link
Contributor

DarkKnight1992 commented Dec 5, 2019

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

@oliviertassinari
Copy link
Member

oliviertassinari commented Dec 5, 2019

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

@DarkKnight1992
Copy link
Contributor

DarkKnight1992 commented Dec 5, 2019

@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.

@oliviertassinari
Copy link
Member

@DarkKnight1992 You can use findIndex with getOptionSelected.

@DarkKnight1992
Copy link
Contributor

@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 ?

@oliviertassinari
Copy link
Member

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

@DarkKnight1992
Copy link
Contributor

DarkKnight1992 commented Dec 5, 2019

@oliviertassinari draft pull requested #18695

@miguelbalboa
Copy link
Author

You guys are awesome!
Thanks!

@Legilimens
Copy link

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.

@oliviertassinari
Copy link
Member

@Legilimens Yes.

@Legilimens
Copy link

@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

@oliviertassinari
Copy link
Member

I can't here, please ask on StackOverflow.

@JuanmaMenendez
Copy link

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

What do you think @DarkKnight1992 ?

@DarkKnight1992
Copy link
Contributor

DarkKnight1992 commented Mar 14, 2020

@JuanmaMenendez select already works if you provide a primitive value

@JuanmaMenendez
Copy link

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

@DarkKnight1992
Copy link
Contributor

@oliviertassinari is that's possible ?

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 15, 2020

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

@JuanmaMenendez
Copy link

@oliviertassinari thanks for your answer, can you please give us a link about the use of getOptionSelected in a Select component, I don't see anything in the docs https://material-ui.com/api/select/

@zeljkocurcic
Copy link

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)

@oliviertassinari
Copy link
Member

@zeljkocurcic Upgrade to the latest version.

@aakers
Copy link

aakers commented Nov 29, 2020

@zeljkocurcic Upgrade to the latest version.

Latest version of what? I have the latest version of Autocomplete that I see published and the newest versions of React and React DOM.

@nikobojs
Copy link

I get the same warning using

    "@material-ui/core": "5.0.0-beta.1",
    "@material-ui/lab": "5.0.0-alpha.40",

Also, i can import Autocomplete from both lab and core. I don't know if this is on purpose?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: autocomplete This is the name of the generic UI component, not the React module! good first issue Great for first contributions. Enable to learn the contribution process. new feature New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.