Reputation: 2047
In my React app I am trying to catch some event when checkbox is clicked in order to proceed some state filtering, and show only items are needed.
event
is coming from child's checkbox with some name
. There is 3 checkboxes, so I need to know the name
which one is clicked.
some of them
<input
type="checkbox"
name="name1"
onClick={filterHandler}
/>Something</div>
state is something like
state = {
items = [
{
name: "name1",
useful: true
},{
name: "name2",
useful: false
}],
filteredItems: []
}
here is the handler
filterHandler = (evt) => {
let checked = evt.target.checked;
let name = evt.target.name;
let filteredItems = this.state.items.filter(item => {
return item[name] === checked; // filtered [{...},{...}...]
});
// when filtered - push to state and show all from here in <ItemsList />
this.setState({ filteredItems })
}
ItemsList component for "showing" is like so:
<ItemsList
items={this.state.filteredItems.length === 0 ? this.state.items : this.state.filteredItems}
/>
When checkbox is one and only - its working fine. But I have three of those boxes -- complications appears:
1) when checking next box I operate with original un-filtered items array - so for this purpose I need already filtered array.
2) I cant use my filteredItems
array reviously filtered, because when unchecking box that array gets empty.To have 3rd "temporary" array seems a little weird.
I tried this way, pretty similar also
this.setState({
filteredItems: this.state.items.filter(item => {
if (item[name] === checked) {
console.log('catch');
return Object.assign({}, item)
} else {
console.log('no hits')
}
})
and this is almost good, but when uncheck filteredItems
are filled with opposite values ((
I feel there is a better approach, please suggest.
Upvotes: 5
Views: 25422
Reputation: 36594
You can do it by storing the checked state of the filters.
For example, your state can look something like:
state = {
items: [
{
name: "name1",
useful: true
},
{
name: "name2",
useful: false
}
],
filters: { 'name1': false, 'name2': false}, // key value pair name:checked
filteredItems: []
};
Then your click/change handler would update both the filtered list and the actual filters state (what's checked).
Here's an example of that:
(Update: Heavily commented as per request in comments)
// Using syntax: someFunc = (params) => { ... }
// To avoid having to bind(this) in constructor
onChange = evt => {
// const is like var and let but doesn't change
// We need to capture anything dependent on
// evt.target in synchronous code, and
// and setState below is asynchronous
const name = evt.target.name;
const checked = evt.target.checked;
// Passing function instead of object to setState
// This is the recommended way if
// new state depends on existing state
this.setState(prevState => {
// We create a new object for filters
const filters = {
// We add all existing filters
// This adds them with their existing values
...prevState.filters,
// This is like:
// filters[name] = checked
// which just overrides the value of
// the prop that has the name of checkbox
[name]: checked
};
// Object.keys() will return ["name1", "name2"]
// But make sure that "filters" in
// our initial state has all values
const activeFilterNames = Object.keys(filters).filter(
// We then filter this list to only the ones that
// have their value set to true
// (meaning: checked)
// We set this in the `const filter =` part above
filterName => filters[filterName]
);
// We get the full list of items
// (Make sure it's set in initial state)
// Then we filter it to match only checked
const filteredItems = prevState.items.filter(item =>
// For each item, we loop over
// all checked filters
// some() means: return true if any of the
// array elements in `activeFilterNames`
// matches the condition
activeFilterNames.some(
// The condition is simply the filter name is
// the same as the item name
activeFilterName => activeFilterName === item.name
)
);
// The object returned from setState function
// is what we want to change in the state
return {
// this is the same as
// { filter: filters,
// filteredItems: filteredItems }
// Just taking advantage of how const names
// are the same as prop names
filters,
filteredItems
};
});
};
I'm using latest features of JS / Babel in here but hopefully the code is clear. I also had to use evt.target
before entering setState()
Here's a full component example:
import * as React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
state = {
items: [
{
name: "name1",
useful: true
},
{
name: "name2",
useful: false
}
],
filters: { name1: false, name2: false },
filteredItems: []
};
// Using syntax: someFunc = (params) => { ... }
// To avoid having to bind(this) in constructor
onChange = evt => {
// const is like var and let but doesn't change
// We need to capture anything dependent on
// evt.target in synchronous code, and
// and setState below is asynchronous
const name = evt.target.name;
const checked = evt.target.checked;
// Passing function instead of object to setState
// This is the recommended way if
// new state depends on existing state
this.setState(prevState => {
// We create a new object for filters
const filters = {
// We add all existing filters
// This adds them with their existing values
...prevState.filters,
// This is like:
// filters[name] = checked
// which just overrides the value of
// the prop that has the name of checkbox
[name]: checked
};
// Object.keys() will return ["name1", "name2"]
// But make sure that "filters" in
// our initial state has all values
const activeFilterNames = Object.keys(filters).filter(
// We then filter this list to only the ones that
// have their value set to true
// (meaning: checked)
// We set this in the `const filter =` part above
filterName => filters[filterName]
);
// We get the full list of items
// (Make sure it's set in initial state)
// Then we filter it to match only checked
const filteredItems = prevState.items.filter(item =>
// For each item, we loop over
// all checked filters
// some() means: return true if any of the
// array elements in `activeFilterNames`
// matches the condition
activeFilterNames.some(
// The condition is simply the filter name is
// the same as the item name
activeFilterName => activeFilterName === item.name
)
);
// The object returned from setState function
// is what we want to change in the state
return {
// this is the same as
// { filter: filters,
// filteredItems: filteredItems }
// Just taking advantage of how const names
// are the same as prop names
filters,
filteredItems
};
});
};
renderCheckboxes() {
return Object.keys(this.state.filters).map((name, index) => {
return (
<label key={index}>
<input
onChange={this.onChange}
type="checkbox"
checked={this.state.filters[name]}
name={name}
/>
{name}
</label>
);
});
}
render() {
const items = this.state.filteredItems.length
? this.state.filteredItems
: this.state.items;
return (
<div>
<div>{this.renderCheckboxes()}</div>
<ul>
{items.map(item => (
<li key={item.name}>
{item.name}
{item.useful && " (useful)"}
</li>
))}
</ul>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can try it live from here:
https://codesandbox.io/embed/6z8754nq1n
You can of course create different variations of this as you wish. For example you might choose to move the filtering to the render function instead of the change event, or store how you store the selected filters, etc, or just use it as is. Whatever suits you best :)
Upvotes: 7
Reputation: 112807
You could introduce an additional piece of state checked
in which you store your checked values, and use those for your filtering instead.
Example (CodeSandbox)
class App extends React.Component {
state = {
items: [
{
name: "name1",
useful: true
},
{
name: "name2",
useful: false
}
],
checked: {}
};
filterHandler = event => {
const { name } = event.target;
this.setState(previousState => {
const checked = { ...previousState.checked };
checked[name] = !checked[name];
return { checked };
});
};
render() {
const { items, checked } = this.state;
let filteredItems = items.filter(item => checked[item.name]);
filteredItems = filteredItems.length === 0 ? items : filteredItems;
return (
<div>
<div>
<input
type="checkbox"
name="name1"
value={checked["name1"]}
onClick={this.filterHandler}
/>Name 1
<input
type="checkbox"
name="name2"
value={checked["name2"]}
onClick={this.filterHandler}
/>Name 2
</div>
<div>
{filteredItems.map(item => <div key={item.name}> {item.name} </div>)}
</div>
</div>
);
}
}
Upvotes: 1
Reputation: 1684
Try to work with the name & append the toggled one to state.
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
Upvotes: 3