T Doe
T Doe

Reputation: 91

Pass click event to nested component - React | Material-UI

Im having an issue. When you click listItem (whole separated li element) I want to call the onChange function on the checkbox component inside listItem.

I could easily move that function from checkbox to parent but I will loose the checked prop.

Checking the checkbox itself doesnt work, but just open the console to see the results. I want handleToggle function to fire properly when whole element is clicked (not only checkbox)

  <List>
    {['first', 'second', 'third'].map((name, i) => (
      <ListItem key={i} dense button>
        <ListItemText primary={name} />
        <Checkbox
          disableRipple
          checked={false}
          onChange={(evt, checked) => this.handleToggle(evt, checked, name)}
        />
      </ListItem>
    ))}
  </List>

Code SandBox

Edit

I don't want to use state in this component at all. Summarizing - how to pass event from ListItem (parent) to it's children (Checkbox)?

Final edit: I've found out the way how to deal with it. No state needed. Just two simple lines of code.

Since checkbox state is fully controlled by redux, I just moved the onClick func to the ListItem element with one line on code in it's body:

...dispatch(toggle(!this.props.elements[nameOfElement], name));

Anyways thanks to everyone for help. Upvoted every answer.

Upvotes: 8

Views: 5220

Answers (5)

timetowonder
timetowonder

Reputation: 5411

Looks like you just want to toggle the checkbox when the associated text is clicked.

All you need to do is either wrap everything in a <label /> tag or just wrap the text in the <label htmlFor="inputId" /> tag with the appropriate htmlFor attribute:

No javascript magic is required.

Upvotes: 0

dhilt
dhilt

Reputation: 20734

Since you are planning to use Redux, I would suggest a simplest implementation of your case in terms of React Redux.

Reducer

const inititalState = {
  list: [ // keep all your data in redux store
    { id: 1, name: 'first', checked: false },
    { id: 2, name: 'second', checked: false },
    { id: 3, name: 'third', checked: false }
  ]
};

export function list(state = inititalState, action) {
  switch (action.type) {
    case 'CHANGE': // changing the list of items (checked property)
      return {
        ...state,
        list: state.list.map(i => 
          i.id === action.item.id ? { ...action.item, checked: action.value } : i
        ) 
      };
    default:
      return state
  }
}

Action

// just the only action to change 'checked' property of a given item in the list
export function change(item, value) {
  return { type: 'CHANGE', item, value };
}

At last, the Component connected with redux store:

class App extends React.Component {
  handleToggle = (item) => {
    // dispatching the "change" redux-action
    this.props.dispatch(change(item, !item.checked)); // here's the toggling logic (!)
  }

  render() {
    return (
      <List>
        {this.props.list.map((item, i) => ( // map the list from the redux store
          <ListItem key={item.id} dense button // (i)ndex for the key value would be ok too
            onClick={() => this.handleToggle(item)}> // to call the redux-action by click
            <ListItemText primary={item.name} />
            <Checkbox
              disableRipple
              checked={item.checked} // this is the main binding
            />                       // with no need of onChange handling
          </ListItem>
        ))}
      </List>
    )
  }
};

// pass the list from the redux-store to the component
function mapStateToProps(state, ownProps) {
  return { list: state.list };
}

const _App = connect(mapStateToProps)(App); // connect the component to the redux store

This is very basic implementation, I'd recommend to follow best practices, move App component out of index.js, the state logic (!item.checked) could be placed in the action creator or reducer etc... Also, I created a working demo based on your codesandbox: https://codesandbox.io/s/xpk099y40w.

Upvotes: 3

Philippe Sultan
Philippe Sultan

Reputation: 2318

One way to tackle this issue might be to use React refs to keep a reference of the child <ListItem/>, and probably use an uncontrolled component in order to detach your output from state updates, that is in this case, replace <Checkbox /> with <input type="checkbox"/>.

Checkboxes would then be updated either directly from the DOM element itself using onChange on <input />, or through React using onClick on the <ListIem/> that references the <input /> DOM element.

...
class App extends React.Component {
  constructor(props) {
    super(props);
    this.checkboxes = [
      {
        name: "first",
        checked: false
      },
      {
        name: "second",
        checked: false
      },
      {
        name: "third",
        checked: false
      }
    ];
  }

  handleListItemClicked = (evt, name) => {
    console.log("ListItem clicked :", name);
    this.checkboxes[name].checked = !this.checkboxes[name].checked;
  };

  handleInputChanged = (evt, name) => {
    console.log("input changed, evt.target :", evt.target);
    evt.target.checked = !evt.target.checked;
  };

  render() {
    return (
      <List>
        {this.checkboxes.map(({ name }, i) => (
          <div>
            <ListItem
              key={i}
              onClick={evt => this.handleListItemClicked(evt, name)}
              dense
              button
            >
              <ListItemText primary={name} />
              <input
                type="checkbox"
                name={name}
                ref={checkbox => {
                  this.checkboxes[name] = checkbox;
                }}
                onChange={evt => this.handleInputChanged(evt, name)}
              />
            </ListItem>
          </div>
        ))}
      </List>
    );
  }
}

...

Here is a fork of your initial Code Sandbox : https://codesandbox.io/s/mox93j6nry

Hope this helps!

Upvotes: 3

bluesixty
bluesixty

Reputation: 2407

Roby, beat me too it, but I handled the state change slightly differently.

The idea is to manage the state (checked on not) of all the check boxes in the parents state. Then you have a handleToggle function in the parent that will update the parents state with the checked value of all the checkboxes.

This state is then passed to each checkbox as a prop.

Also, it is not a good idea to use the index from map as a key in the ListItem. If you add and remove items from the list, React will get confused as to which item is which.

Here is the code:

import React from "react";

import { render } from "react-dom";
import Hello from "./Hello";

import List, {
  ListItem,
  ListItemSecondaryAction,
  ListItemText
} from "material-ui/List";
import Checkbox from "material-ui/Checkbox";

const styles = {
  fontFamily: "sans-serif",
  textAlign: "center"
};

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [],
      listChecked: []
    };
  }

  componentWillMount() {
    this.setState({
      list: ["first", "second", "third"],
      listChecked: [{ first: false }, { second: false }, { third: false }]
    });
  }

  handleToggle = evt => {
    console.log(evt.target.checked);
    const name = evt.target.name;
    this.setState({ name: !this.state.listChecked[name]})
    // this.props.dispatch(x(checked, name));
  };

  render() {
    const { list } = this.state;
    return (
      <List>
        {list.map((name, i) => (
          <ListItem key={name} dense button>
            <ListItemText primary={name} />
            <Checkbox
              disableRipple
              checked={this.state.listChecked[name]}
              onChange={this.handleToggle}
            />
          </ListItem>
        ))}
      </List>
    );
  }
}

render(<App />, document.getElementById("root"));

and the CodeSandbox Example

Upvotes: 3

Andrei Roba
Andrei Roba

Reputation: 2322

You don't really need to pass the checked prop to your parent. You could instead keep track of the checked rows in your component state:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      checked: []
    }
  }

  isChecked(name) {
    return this.state.checked.indexOf(name) > -1
  }

  handleToggle = (evt, name) => {
    if (this.isChecked(name)) {
      this.setState({
        checked: this.state.checked.filter(i => i !== name)
      })
    } else {
      this.setState({
        checked: [...this.state.checked, name]
      })
    }
  }

  render() {
    return (
      <List>
        {['first', 'second', 'third'].map((name, i) => (
          <ListItem key={i}
            onClick={evt => this.handleToggle(evt, name)} dense button>
            <ListItemText primary={name} />
            <Checkbox
              disableRipple
              label={name}
              checked={this.isChecked(name)}
            />
          </ListItem>
        ))}
      </List>
    )
  }
};

Working sample.

Upvotes: 2

Related Questions