Repository: mlaursen/react-md
Pull request #630 (2017-11-17 16:40:02)

Added ability customize items in SelectField
I have added means to customize different menu items in SelectField.
For example, it could be useful to add some styling to active or disabled items.


Patch:
diff --git a/docs/src/components/Components/SelectFields/index.jsx b/docs/src/components/Components/SelectFields/index.jsx
index a88133717b..ffb9ba264c 100644
--- a/docs/src/components/Components/SelectFields/index.jsx
+++ b/docs/src/components/Components/SelectFields/index.jsx
@@ -28,7 +28,7 @@ string.  This means that the \`SelectField\`'s list would show these values as t
 and when one of those items are selected, the \`SelectField\`'s value would be updated to that
 label as well.
 
-If the item is an object, it will extract the \`itemLabel\` and \`itemValue\` from the object and
+If the item is an object, it will extract the \`itemLabel\`, \`itemValue\` and \`itemProps\` from the object and
 apply all the remaining keys to the \`ListItem\` component. Additional keys can be removed by
 specifying the \`deleteKeys\` prop.
 
diff --git a/src/js/SelectFields/SelectField.d.ts b/src/js/SelectFields/SelectField.d.ts
index 3cee2a1ce5..e1c3afcc17 100644
--- a/src/js/SelectFields/SelectField.d.ts
+++ b/src/js/SelectFields/SelectField.d.ts
@@ -24,6 +24,8 @@ export interface SharedSelectFieldProps extends BaseMenuProps, SharedTextFieldPr
   keyboardMatchingTimeout?: number;
   itemLabel?: string;
   itemValue?: string;
+  itemProps?: string;
+  getItemProps?: (data: Object) => Object;
   defaultValue?: number | string;
   value?: number | string;
   onChange?: (value: number | string, selectedIndex: number, event: React.MouseEvent<HTMLElement>) => void;
diff --git a/src/js/SelectFields/SelectField.js b/src/js/SelectFields/SelectField.js
index 7744dc6481..eaff38db9b 100644
--- a/src/js/SelectFields/SelectField.js
+++ b/src/js/SelectFields/SelectField.js
@@ -183,6 +183,37 @@ export default class SelectField extends PureComponent {
      */
     itemValue: PropTypes.string.isRequired,
 
+    /**
+     * The key to use for extracting a menu item's function
+     * to get additional `ListItem` props if the menu item is an object.
+     *
+     * Example:
+     *
+     * ```js
+     * const item = { something: 'My Label', addProps: ({active}) => active ? {secondaryText: 'some text'} : null };
+     * const itemLabel = 'something';
+     * const itemProps = 'addProps';
+     * ```
+     *
+     * @see {@link #getItemProps}
+     */
+    itemProps: PropTypes.string.isRequired,
+
+    /**
+     * An optional function to get additional `ListItem` props if the menu item is an object.
+     *
+     * An object with the following fields will be passed into the function:
+     * - `index` - item's index
+     * - `active` - whether item is active
+     * - `disabled` - whether item is disabled
+     * - `dataValue` - item's value
+     * - `value` - current list value
+     * - `props` - default `ListItem` props
+     * - `item` - source item's data
+     * - `field` - reference to the component instance
+     */
+    getItemProps: PropTypes.func,
+
     /**
      * The default value to use for the select field. If this is set, it should either match
      * one of the `number` or `string` in your `menuItems` list or be the empty string. If
@@ -531,6 +562,7 @@ export default class SelectField extends PureComponent {
     position: SelectField.Positions.TOP_LEFT,
     itemLabel: 'label',
     itemValue: 'value',
+    itemProps: 'getProps',
     dropdownIcon: <FontIcon>arrow_drop_down</FontIcon>,
     lineDirection: 'left',
     menuItems: [],
@@ -605,8 +637,8 @@ export default class SelectField extends PureComponent {
     return '';
   }
 
-  _getDeleteKeys({ itemLabel, itemValue, deleteKeys }) {
-    const keys = [itemLabel, itemValue];
+  _getDeleteKeys({ itemLabel, itemValue, itemProps, deleteKeys }) {
+    const keys = [itemLabel, itemValue, itemProps];
     if (deleteKeys) {
       return keys.concat(Array.isArray(deleteKeys) ? deleteKeys : [deleteKeys]);
     }
@@ -931,34 +963,46 @@ export default class SelectField extends PureComponent {
       return items;
     }
 
-    const { id, itemLabel, itemValue, position, stripActiveItem } = this.props;
+    const { getItemProps, id, itemLabel, itemProps, itemValue, position, stripActiveItem } = this.props;
     const below = position === SelectField.Positions.BELOW;
     const value = getField(this.props, this.state, 'value');
-    const type = typeof item;
 
-    let props;
     const dataValue = this._getItemPart(item, itemLabel, itemValue);
     const primaryText = this._getItemPart(item, itemLabel, itemValue, true);
-    if (type === 'object') {
-      props = omit(item, this._deleteKeys);
-    }
 
     const active = dataValue === value || dataValue === parseInt(value, 10);
     const stripped = (typeof stripActiveItem !== 'undefined' ? stripActiveItem : below) && active;
     if (!stripped) {
-      const disabled = (props && props.disabled) || false;
+      const objectType = typeof item === 'object';
+      const props = objectType ? omit(item, this._deleteKeys) : {};
+      const disabled = props.disabled || false;
+      props.ref = disabled ? null : this._setListItem;
+      props.id = active ? `${id}-options-active` : null;
+      props.active = active;
+      props.tabIndex = -1;
+      props.primaryText = primaryText;
+      props.key = item.key || dataValue;
+      props.role = 'option';
+      props['data-id'] = disabled ? null : i;
+      props['data-value'] = disabled ? null : dataValue;
+
+      const getProps = (objectType && item[itemProps]) || getItemProps;
+      if (typeof getProps === 'function') {
+        Object.assign(props, getProps({
+          index: i,
+          active,
+          disabled,
+          dataValue,
+          value,
+          props,
+          item,
+          field: this,
+        }));
+      }
+
       items.push(
         <ListItem
           {...props}
-          ref={disabled ? null : this._setListItem}
-          id={active ? `${id}-options-active` : null}
-          active={active}
-          tabIndex={-1}
-          primaryText={primaryText}
-          key={item.key || dataValue}
-          role="option"
-          data-id={disabled ? null : i}
-          data-value={disabled ? null : dataValue}
         />
       );
     }
@@ -1007,6 +1051,8 @@ export default class SelectField extends PureComponent {
       visible: propVisible,
       itemLabel,
       itemValue,
+      itemProps,
+      getItemProps,
       defaultValue,
       defaultVisible,
       onClick,


Test patch:
diff --git a/src/js/SelectFields/__tests__/SelectField.js b/src/js/SelectFields/__tests__/SelectField.js
index 6225c8d0c0..b85e265574 100644
--- a/src/js/SelectFields/__tests__/SelectField.js
+++ b/src/js/SelectFields/__tests__/SelectField.js
@@ -9,6 +9,7 @@ import {
 
 import SelectField from '../SelectField';
 import SelectFieldInput from '../SelectFieldInput';
+import ListItem from '../../Lists/ListItem';
 import FloatingLabel from '../../TextFields/FloatingLabel';
 import Menu from '../../Menus/Menu';
 
@@ -122,6 +123,49 @@ describe('SelectField', () => {
     expect(field.state('activeLabel')).toBe('Missing');
   });
 
+  it('should set additional props for active item', () => {
+    const text = 'active item';
+    function getProps({ active }) {
+      return active ? { secondaryText: text } : null;
+    }
+
+    const field = shallow(<SelectField id="test" menuItems={['1', '2', '3']} value="2" getItemProps={getProps} onChange={jest.fn()} />);
+    let items = field.find(ListItem);
+    const defaultValue = ListItem.defaultProps.secondaryText;
+    expect(items.at(0).prop('secondaryText')).toBe(defaultValue);
+    expect(items.at(1).prop('secondaryText')).toBe(text);
+    expect(items.at(2).prop('secondaryText')).toBe(defaultValue);
+
+    field.setProps({ value: '3' });
+    items = field.find(ListItem);
+    expect(items.at(0).prop('secondaryText')).toBe(defaultValue);
+    expect(items.at(1).prop('secondaryText')).toBe(defaultValue);
+    expect(items.at(2).prop('secondaryText')).toBe(text);
+  });
+
+  it('should use specified field from item data to get additional props for the item', () => {
+    const activeText = 'active item';
+    function getProps({ active }) {
+      return active ? { secondaryText: activeText } : null;
+    }
+
+    const lastText = 'last item';
+
+    const field = shallow(
+      <SelectField
+        id="test"
+        menuItems={['1', '2', { label: 'last', value: 3, addProps: () => ({ secondaryText: lastText }) }]}
+        itemProps="addProps"
+        defaultValue="2"
+        getItemProps={getProps}
+      />
+    );
+    const items = field.find(ListItem);
+    expect(items.at(0).prop('secondaryText')).toBe(ListItem.defaultProps.secondaryText);
+    expect(items.at(1).prop('secondaryText')).toBe(activeText);
+    expect(items.at(2).prop('secondaryText')).toBe(lastText);
+  });
+
   it('should still have the correct label if the menuItems are defined as a list in the render and the parent component rerenders', () => {
     let renderCount = 0;
     class Test extends React.Component {