Repository: mui/material-ui (TypeScript)
Date: 2018-10-27 10:28:59+00:00
Task type: Bug Fix

Issue:
[SelectInput] Allow selected to be chosen with non-strict equality
## Problem (short story)

Detection of the selected choice in `SelectInput` uses strict equality (`===`) with no option for a non-strict equality fallback. This prevents value initialization from the query string (which isn't strongly typed).

## Context (long story)

In an admin, I wrote creation form for Comments. It uses a `<TextField select>` to display the `post_id`, which is a foreign key to the related Posts. The possible choices are under the shape `<MenuItem value={12}>Lorem Ipsum</MenuItem>`. So far, so good.

![kapture 2018-07-03 at 19 29 56](https://user-images.githubusercontent.com/99944/42235259-8cb5b4d0-7ef7-11e8-9cb2-0d7197dde3ad.gif)

I wrote a button linking to this form, setting the URL query string as `?post_id=12`. The form parses the query string as `{ post_id: "12" }`, and uses this object to initialize the form values. However, when I click on the button, the related post isn't pre-selected.

![kapture 2018-07-03 at 19 33 11](https://user-images.githubusercontent.com/99944/42235414-fd300f3a-7ef7-11e8-9bc3-6fce97b740eb.gif)

The problem lies in https://github.com/mui-org/material-ui/blob/b3564a767135b933146f7fe73e3a34b14950c782/packages/material-ui/src/Select/SelectInput.js#L230-L235: it uses strict equality (`===`) to check if one of the `children` matches the `value`. But since the `post_id` comes from the URL, it's a string (e.g. `'12'), while the select choices use number values from the persistence (e.g. `12`). 

## Requested Change

I need to be able to tell the `SelectInput` (and therefore `TextField` as well) to use non-strict equality for value selection.

## Miscellaneous

- [x] This is a v1.x issue (v0.x is no longer maintained).
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

| Tech         | Version |
|--------------|---------|
| Material-UI  | v1.2.0  |
| React        |  16.3 |
| browser      |  Chrome  |



Issue discussion:
@fzaninotto Interesting issue. Of course, you can always normalize the data on React Admin side. Still, I'm eager to see how the native implementation behaves. I think that we should make them (native, non-native) identical. I'm gonna try/test that and see how we can move forward. 
All green for me, it does work this way for the native select.
@oliviertassinari I can add a property that changes the behaviour of equality as strict or non-strict. What do you think?

`
selected = (value === child.props.value && child.props.useStrictEquality) ||
value == child.props.value;
`

Is it a appropriate solution? If it is i can code & test it. 
@mbrn Why do you want to add a property? Should we just change the behavior? But it's not something I would change in a patch release, for sure.
@oliviertassinari  I just want to solve the problem. may We solve this bug for next minor version?
@mbrn The next release will be a minor. We are good to go :).
@oliviertassinari :). I will solve this problem by adding the property. Is it appropriate?
The strategy is to make the native and non-native implementations as close as possible. What's the use case for `useStrictEquality`? 
I don't know. But we have at least one case:). Do you mean that this is not appropriate solution to strategy?

Solution patch:
diff --git a/.size-limit.js b/.size-limit.js
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -22,13 +22,13 @@ module.exports = [
     name: 'The size of all the material-ui modules.',
     webpack: true,
     path: 'packages/material-ui/build/index.js',
-    limit: '93.2 KB',
+    limit: '93.3 KB',
   },
   {
     name: 'The main docs bundle',
     webpack: false,
     path: main.path,
-    limit: '182 KB',
+    limit: '183 KB',
   },
   {
     name: 'The docs home page',
diff --git a/packages/material-ui/src/Select/Select.js b/packages/material-ui/src/Select/Select.js
--- a/packages/material-ui/src/Select/Select.js
+++ b/packages/material-ui/src/Select/Select.js
@@ -172,7 +172,10 @@ Select.propTypes = {
     PropTypes.string,
     PropTypes.number,
     PropTypes.bool,
-    PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])),
+    PropTypes.object,
+    PropTypes.arrayOf(
+      PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool, PropTypes.object]),
+    ),
   ]),
   /**
    * The variant to use.
diff --git a/packages/material-ui/src/Select/SelectInput.js b/packages/material-ui/src/Select/SelectInput.js
--- a/packages/material-ui/src/Select/SelectInput.js
+++ b/packages/material-ui/src/Select/SelectInput.js
@@ -7,6 +7,14 @@ import Menu from '../Menu/Menu';
 import { isFilled } from '../InputBase/utils';
 import { setRef } from '../utils/reactHelpers';
 
+function areEqualValues(a, b) {
+  if (typeof b === 'object' && b !== null) {
+    return a === b;
+  }
+
+  return String(a) === String(b);
+}
+
 /**
  * @ignore - internal component.
  */
@@ -225,12 +233,12 @@ class SelectInput extends React.Component {
           );
         }
 
-        selected = value.indexOf(child.props.value) !== -1;
+        selected = value.some(v => areEqualValues(v, child.props.value));
         if (selected && computeDisplay) {
           displayMultiple.push(child.props.children);
         }
       } else {
-        selected = value === child.props.value;
+        selected = areEqualValues(value, child.props.value);
         if (selected && computeDisplay) {
           displaySingle = child.props.children;
         }
@@ -446,7 +454,10 @@ SelectInput.propTypes = {
     PropTypes.string,
     PropTypes.number,
     PropTypes.bool,
-    PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])),
+    PropTypes.object,
+    PropTypes.arrayOf(
+      PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool, PropTypes.object]),
+    ),
   ]).isRequired,
   /**
    * The variant to use.
diff --git a/pages/api/select.md b/pages/api/select.md
--- a/pages/api/select.md
+++ b/pages/api/select.md
@@ -35,7 +35,7 @@ import Select from '@material-ui/core/Select';
 | <span class="prop-name">open</span> | <span class="prop-type">bool</span> |   | Control `select` open state. You can only use it when the `native` property is `false` (default). |
 | <span class="prop-name">renderValue</span> | <span class="prop-type">func</span> |   | Render the selected value. You can only use it when the `native` property is `false` (default).<br><br>**Signature:**<br>`function(value: any) => ReactElement`<br>*value:* The `value` provided to the component. |
 | <span class="prop-name">SelectDisplayProps</span> | <span class="prop-type">object</span> |   | Properties applied to the clickable div element. |
-| <span class="prop-name">value</span> | <span class="prop-type">union:&nbsp;string&nbsp;&#124;<br>&nbsp;number&nbsp;&#124;<br>&nbsp;bool&nbsp;&#124;<br>&nbsp;arrayOf<br></span> |   | The input value. This property is required when the `native` property is `false` (default). |
+| <span class="prop-name">value</span> | <span class="prop-type">union:&nbsp;string, number, bool, object, arrayOf<br></span> |   | The input value. This property is required when the `native` property is `false` (default). |
 | <span class="prop-name">variant</span> | <span class="prop-type">enum:&nbsp;'standard'&nbsp;&#124;<br>&nbsp;'outlined'&nbsp;&#124;<br>&nbsp;'filled'<br></span> |   | The variant to use. |
 
 Any other properties supplied will be spread to the root element ([Input](/api/input/)).


Test patch:
diff --git a/packages/material-ui/src/Select/SelectInput.test.js b/packages/material-ui/src/Select/SelectInput.test.js
--- a/packages/material-ui/src/Select/SelectInput.test.js
+++ b/packages/material-ui/src/Select/SelectInput.test.js
@@ -61,6 +61,39 @@ describe('<SelectInput />', () => {
     );
   });
 
+  describe('prop: value', () => {
+    it('should select the option based on the number value', () => {
+      const wrapper = shallow(<SelectInput {...defaultProps} value={20} />);
+      assert.deepEqual(wrapper.find(MenuItem).map(m => m.props().selected), [false, true, false]);
+    });
+
+    it('should select the option based on the string value', () => {
+      const wrapper = shallow(<SelectInput {...defaultProps} value="20" />);
+      assert.deepEqual(wrapper.find(MenuItem).map(m => m.props().selected), [false, true, false]);
+    });
+
+    it('should select only the option that matches the object', () => {
+      const obj1 = { id: 1 };
+      const obj2 = { id: 2 };
+
+      const wrapper = shallow(
+        <SelectInput {...defaultProps} value={obj1}>
+          <MenuItem key={1} value={obj1}>
+            1
+          </MenuItem>
+          <MenuItem key={2} value={obj2}>
+            2
+          </MenuItem>
+        </SelectInput>,
+      );
+
+      assert.deepEqual(wrapper.find(MenuItem).map(wrapper2 => wrapper2.props().selected), [
+        true,
+        false,
+      ]);
+    });
+  });
+
   describe('prop: readOnly', () => {
     it('should not trigger any event with readOnly', () => {
       const wrapper = shallow(<SelectInput {...defaultProps} readOnly />);
@@ -281,9 +314,7 @@ describe('<SelectInput />', () => {
       const wrapper = shallow(<SelectInput {...defaultProps} disabled tabIndex={0} />);
       assert.strictEqual(wrapper.find('[data-mui-test="SelectDisplay"]').props().tabIndex, 0);
     });
-  });
 
-  describe('prop: multiple', () => {
     it('should serialize multiple select value', () => {
       const wrapper = shallow(<SelectInput {...defaultProps} value={[10, 30]} multiple />);
       assert.strictEqual(wrapper.find('input').props().value, '10,30');
@@ -294,6 +325,45 @@ describe('<SelectInput />', () => {
       ]);
     });
 
+    describe('when the value matches an option but they are different types', () => {
+      it('should select the options based on the value', () => {
+        const wrapper = shallow(<SelectInput {...defaultProps} value={['10', '20']} multiple />);
+        assert.deepEqual(wrapper.find(MenuItem).map(wrapper2 => wrapper2.props().selected), [
+          true,
+          true,
+          false,
+        ]);
+      });
+    });
+
+    describe('when the value is an object', () => {
+      it('should select only the options that match', () => {
+        const obj1 = { id: 1 };
+        const obj2 = { id: 2 };
+        const obj3 = { id: 3 };
+
+        const wrapper = shallow(
+          <SelectInput {...defaultProps} value={[obj1, obj3]} multiple>
+            <MenuItem key={1} value={obj1}>
+              1
+            </MenuItem>
+            <MenuItem key={2} value={obj2}>
+              2
+            </MenuItem>
+            <MenuItem key={3} value={obj3}>
+              3
+            </MenuItem>
+          </SelectInput>,
+        );
+
+        assert.deepEqual(wrapper.find(MenuItem).map(wrapper2 => wrapper2.props().selected), [
+          true,
+          false,
+          true,
+        ]);
+      });
+    });
+
     it('should throw if non array', () => {
       assert.throw(() => {
         shallow(<SelectInput {...defaultProps} multiple />);