6

Let's start by consider this little snippet:

import "./styles.css";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import Checkbox from "@material-ui/core/Checkbox";
import Select from "@material-ui/core/Select";
import React, { Component } from "react";

class WidgetList extends Component {
  constructor(props) {
    super(props);
    this.state = {};
  }

  onChangeTag = (event, item) => {
    item.tag = event.target.value;
    console.log("changing tag", event.target, item.name);
  };

  render() {
    const { allItems } = this.props;
    const { selectedItems } = this.props;

    return (
      <List>
        {allItems.map((item) => {
          return (
            <ListItem
              key={item.name}
              role={undefined}
              dense
              button
              disableRipple
            >
              <ListItemIcon>
                <Checkbox
                  edge="start"
                  checked={selectedItems.has(item)}
                  tabIndex={-1}
                  disableRipple
                />
              </ListItemIcon>
              <ListItemText primary={`${item.name}`} />
              <ListItemSecondaryAction>
                <Select
                  native
                  value={item.tag}
                  onChange={(e) => this.onChangeTag(e, item)}
                >
                  <option value={0}>foo</option>
                  <option value={1}>bar</option>
                  <option value={2}>baz</option>
                </Select>
              </ListItemSecondaryAction>
            </ListItem>
          );
        })}
      </List>
    );
  }
}

let allItems = [
  { name: "1", tag: 0 },
  { name: "2", tag: 1 },
  { name: "3", tag: 2 }
];
let selectedItems = new Set([allItems[0], allItems[1]]);

export default function App() {
  return (
    <div className="App">
      <WidgetList allItems={allItems} selectedItems={selectedItems} />
    </div>
  );
}

I'd like to understand what's the way to change the value of the select widgets when clicking a different one. Right now no matter which one you select the widget won't change.

To see what I mean you can play with it on this sandbox, no matter if you select foo, bar, baz values on the select widget the widget itself won't update.

BPL
  • 9,632
  • 9
  • 59
  • 117

3 Answers3

6

You need to change your approach to store selected items state so that it can handle that efficiently.

The main mistake was in this line:

item.tag = event.target.value;

You cannot do that because the item is immutable.

I have updated the code from your sandbox to make the select work properly.

import "./styles.css";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import Checkbox from "@material-ui/core/Checkbox";
import Select from "@material-ui/core/Select";
import React, { Component } from "react";

class WidgetList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      allItems: [],
      selectedItems: {}
    };
  }

  onChangeTag = (event, item) => {
    this.setState({
      selectedItems: {
        ...this.state.selectedItems,
        [selectedItems[item.tag.toString()]]: event.target.value
      }
    });
  };

  render() {
    const { allItems } = this.props;
    const { selectedItems } = this.props;

    return (
      <List>
        {allItems.map((item) => {
          return (
            <ListItem
              key={item.name}
              role={undefined}
              dense
              button
              disableRipple
            >
              <ListItemIcon>
                <Checkbox
                  edge="start"
                  checked={selectedItems.has(item)}
                  tabIndex={-1}
                  disableRipple
                />
              </ListItemIcon>
              <ListItemText primary={`${item.name}`} />
              <ListItemSecondaryAction>
                <Select
                  native
                  value={selectedItems[item.tag.toString()]}
                  onChange={(e) => this.onChangeTag(e, item)}
                >
                  <option value={0}>foo</option>
                  <option value={1}>bar</option>
                  <option value={2}>baz</option>
                </Select>
              </ListItemSecondaryAction>
            </ListItem>
          );
        })}
      </List>
    );
  }
}

let allItems = [
  { name: "1", tag: 0 },
  { name: "2", tag: 1 },
  { name: "3", tag: 2 }
];
let selectedItems = new Set([allItems[0], allItems[1]]);

export default function App() {
  return (
    <div className="App">
      <WidgetList allItems={allItems} selectedItems={selectedItems} />
    </div>
  );
}
Hashir Baig
  • 2,162
  • 15
  • 23
  • First of all, thanks for your answer, now it seems you can change the widgets but i've noticed something weird... the initial values of the select widget now are wrong, why is that? :/ , +1 in the meantime – BPL Sep 11 '20 at 12:31
  • Great answer, I leave an upvote, I hope it motivate you to keep going on Stack Overflow. thanks. – AmerllicA Sep 20 '20 at 06:22
5

You can achieve the same result without using state (as what suggested by most answers) by implementing an uncontrolled component. This way data will be handled by the DOM and not by React.

Just change the value prop on your Select component to defaultValue.

<Select
  native
  defaultValue={item.tag}
  onChange={(e) => this.onChangeTag(e, item)}
>
  <option value={0}>foo</option>
  <option value={1}>bar</option>
  <option value={2}>baz</option>
</Select>

From the docs

With an uncontrolled component, you often want React to specify the initial value, but leave subsequent updates uncontrolled. To handle this case, you can specify a defaultValue attribute instead of value.

Edit crimson-haze-9gpec

bertdida
  • 4,988
  • 2
  • 16
  • 22
1

You need to update allItems value on onChange for working of onChange select i.e

onChangeTag = (event, item) => {
    item.tag = event.target.value;
    const updatedValue = [...this.state.allItems];
    this.setState({
      allItems: updatedValue.map((data) => {
        if (data.name === item.name) {
          data = item;
        }
        return data;
      })
    });
  };

Here is full code:

import "./styles.css";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import ListItemText from "@material-ui/core/ListItemText";
import Checkbox from "@material-ui/core/Checkbox";
import Select from "@material-ui/core/Select";
import React, { Component } from "react";

class WidgetList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      allItems: this.props.allItems
    };
  }

  onChangeTag = (event, item) => {
    item.tag = event.target.value;
    const updatedValue = [...this.state.allItems];
    this.setState({
      allItems: updatedValue.map((data) => {
        if (data.name === item.name) {
          data = item;
        }
        return data;
      })
    });
  };

  render() {
    const { allItems } = this.state;
    const { selectedItems } = this.props;

    return (
      <List>
        {allItems.map((item) => {
          return (
            <ListItem
              key={item.name}
              role={undefined}
              dense
              button
              disableRipple
            >
              <ListItemIcon>
                <Checkbox
                  edge="start"
                  checked={selectedItems.has(item)}
                  tabIndex={-1}
                  disableRipple
                />
              </ListItemIcon>
              <ListItemText primary={`${item.name}`} />
              <ListItemSecondaryAction>
                <Select
                  native
                  value={item.tag}
                  onChange={(e) => this.onChangeTag(e, item)}
                >
                  <option value={0}>foo</option>
                  <option value={1}>bar</option>
                  <option value={2}>baz</option>
                </Select>
              </ListItemSecondaryAction>
            </ListItem>
          );
        })}
      </List>
    );
  }
}

let allItems = [
  { name: "1", tag: 0 },
  { name: "2", tag: 1 },
  { name: "3", tag: 2 }
];
let selectedItems = new Set([allItems[0], allItems[1]]);

export default function App() {
  return (
    <div className="App">
      <WidgetList allItems={allItems} selectedItems={selectedItems} />
    </div>
  );
}


Here is the demo: https://codesandbox.io/s/pensive-shadow-4srcg?file=/src/App.js:0-2354

Shubham Verma
  • 4,918
  • 1
  • 9
  • 22
  • well you need to send him the change handle to the component or the allItems list will be different between the two places – TalOrlanczyk Sep 19 '20 at 19:47