Fsanna
Fsanna

Reputation: 377

MUI Select component with custom children item

I'm trying to create a Select component with a series of custom items that are mapped through a list. Each item has a specific type and, based on that type, the menu item will have a certain MUI icon. I created a specific component for managing the whole Select component and another specific component to display each item, its value and its icon. The problem is that the onChange function is not triggered whenever I click on one of the items. What could the problem be?

I'm attaching a code of what I'm doing.

const myTypes = {
  TYPE_1: "Type 1",
  TYPE_2: "Type 2",
  TYPE_3: "Type 3",
};

function TypeSelect(props) {
  const classes = useStyles();
  const [state, setState] = useState(myTypes.TYPE_1);

  const onChangeType = (e) => {
    setState(e.target.value);
  };

  return (
    <Select
      id="select-type"
      value={state}
      onChange={onChangeType}
    >
      {Object.keys(myTypes).map((type) => (
        <TypeSelectMenuItem
          type={myTypes[type]}
          key={type}
        />
      ))}
    </Select>
  );
}

function TypeSelectMenuItem({ type }) {
  const renderIcon = () => {
    switch (type) {
      case myTypes.TYPE_1:
        return <ShortTextIcon />; // Material-UI icon
      case myTypes.TYPE_2:
        return <SubjectIcon />; // Material-UI icon
      case myTypes.TYPE_3:
        return <RadioButtonCheckedIcon />; // Material-UI icon
      default:
        return <Fragment />;
    }
  };

  return (
    <MenuItem value={type} >
      <ListItemIcon>{renderIcon()}</ListItemIcon>
      <ListItemText primary={type} />
    </MenuItem>
  );
}

Upvotes: 6

Views: 14270

Answers (1)

NearHuscarl
NearHuscarl

Reputation: 81763

According to Material-UI docs, this is how you use the MenuItem component to render Select's options:

<Select value={age} onChange={handleChange}>
  <MenuItem value={10}>Ten</MenuItem>
  <MenuItem value={20}>Twenty</MenuItem>
  <MenuItem value={30}>Thirty</MenuItem>
</Select>

When you want to pass a custom component instead of MenuItem as Select children item. For example TypeSelectMenuItem, you need to do the following things:

1. Pass all props to your custom item component

SelectInput will clone every child and attach additional event handlers as extra props behind the scene, so make sure to pass all of them down using spread operator:

const TypeSelectMenuItem = (props) => {
  return (
    <MenuItem {...props}>
      {...}
    </MenuItem>
  );
};
<Select value={state} onChange={onChangeType}>
  {Object.keys(myTypes).map((type) => (
    <TypeSelectMenuItem value={myTypes[type]} key={type}>
      {myTypes[type]}
    </TypeSelectMenuItem>
  ))}
</Select>

2. Pass the option value to the value props

Do not invent a new props name like what you did here:

<TypeSelectMenuItem type={myTypes[type]} key={type} />

The correct way is to pass to value instead of type props:

<TypeSelectMenuItem value={myTypes[type]} key={type} />

The reason is because internally, SelectInput references props.value instead of your props.type. See this and this. Using anything other than props.value will lead to unexpected behaviors.

3. Reference data-value instead of value in your custom component

When cloning item elements, SelectInput set the value props to undefined and utilize data-value props instead, So you need to use data-value props internally:

const TypeSelectMenuItem = (props) => {
  // when passing as children item of Select. props.value is set to undefined.
  // Use props['data-value'] instead.
  return (
    <MenuItem {...props}>
      {...}
      <ListItemText primary={props["data-value"]} />
    </MenuItem>
  );
};

4. Pass children to your custom item component to render selected value

SelectInput uses children props to render selected value, so you need to make sure to pass something as children to display:

const TypeSelectMenuItem = (props) => {
  return (
    <MenuItem {...props}>
      // this is children of MenuItem, so it is not displayed as selected value
      <ListItemIcon>{renderIcon()}</ListItemIcon>
      <ListItemText primary={props["data-value"]} />
    </MenuItem>
  );
};
<Select value={state} onChange={onChangeType}>
  {Object.keys(myTypes).map((type) => (
    <TypeSelectMenuItem value={myTypes[type]} key={type}>
      // this is children of TypeSelectMenuItem, so it is displayed as selected value
      {myTypes[type]}
    </TypeSelectMenuItem>
  ))}
</Select>

Live Demo

Edit 66943324/material-ui-select-component-with-custom-children-item

Upvotes: 16

Related Questions