Reputation: 91
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>
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
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
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
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
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
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