Search code examples
reactjscheckboxsetstate

How to display data on top of others instead of simultaneously with React checkboxes?


I am new to React and trying to build a TODO app. What I'm trying to achieve is: 1) after each checkbox's click, I would like to show its associated data on top of what is already displayed rather than simultaneously (i.e checkbox 2 displaying its data instead of replacing another checkbox's data).

I guess it has something to do with the way I'm storing and filtering my items but I'm struggling to find the way out.

Also, and for it to be perfect, 2) I'd like to remove the behaviour that checking/unchecking my checkboxes is performing the same action (i.e when all checkboxes are clicked, unticking one will call again its associated function rather than doing nothing and keep its data displayed on the screen) and 3) allowing the fact that some checkboxes cannot be both ticked at the same time (the Clear and Show all checkboxes for example).

Thanks in advance

CODE

import React from "react";
import ReactDOM from "react-dom";
import Items from "./Items.js"

class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
    Data: Items,
    storeData: Items,
    }
  }

  myFunction = (category) => {
    const filterData = this.state.Data.filter(elem => elem.category === category);
    this.setState({storeData: filterData});
  }

  myFunctionAll = () => {
    this.setState({storeData: Items});
  }

  render() {
    console.log(this.state.storeData)
    console.log(this.state.Data)
    return(
      <div>
        <label>
          Filter A
          <input type="checkbox" onClick={() => this.myFunction("A")} />
        </label>
        <label>
          Filter B
          <input type="checkbox" onClick={() => this.myFunction("B")} />
        </label>
        <label>
          Filter C
          <input type="checkbox" onClick={() => this.myFunction("C")} />
        </label>
        <label>
          Clear All
          <input type="checkbox" onClick={() => this.myFunction(null)} />
        </label>
        <label>
          Show All
          <input type="checkbox" onClick={() => this.myFunctionAll()} />
        </label>

        <br />
        {this.state.storeData.map(elem => (
          <li key={elem.id}>
            Id: {elem.id} Category: {elem.category}
          </li>))}
      </div>
    )
  }
}

ReactDOM.render(<Checkbox />, document.getElementById("root"))

DATA

const Items = [
    {
      "id": 1,
      "name": "First item",
      "category": "A"
    },
    {
      "id": 2,
      "name": "Second item",
      "category": "B"
    },
    {
      "id": 3,
      "name": "Third item",
      "category": "C"
    },
    {
      "id": 4,
      "name": "Fourth item",
      "category": "A"
    },
  ]
  
  export default Items

Solution

  • I think the main issue with your code is trying to filter and save back into state your data. I think it is better to instead keep your data intact, i.e. don't mutate it, and instead store in state what "filters" are active. You can simply filter your state data array in the render function on-the-fly.

    Add a filters object to your state.

    state = {
      data: Items,
      filters: {}
    };
    

    Create a toggleFilter function to consume an onChange event. This will toggle what filters are active (i.e. checked) in state.

    toggleFilter = (e) => {
      const { name, checked } = e.target;
      this.setState((prevState) => ({
        filters: {
          ...prevState.filters,
          [name]: checked
        }
      }));
    };
    

    Add a name attribute and onChange handler to each checkbox input, where X is the name of each filter, i.e. "A", "B", or "C".

    <label>
      Filter X
      <input
        type="checkbox"
        name='X'
        onChange={this.toggleFilter}
        checked={!!this.state.filters.X} // <-- coerce to boolean so value is defined
      />
    </label>
    

    Finally, filter and map your data. If some filters object value is truthy then check which filter is active against the current element's category, otherwise just return true to indicate the value should not be filtered from the result array.

    {this.state.data
      .filter((el) => {
        const { filters } = this.state;
        if (Object.values(filters).some(Boolean)) { // <-- if some filter is active
          if (filters.NONE) return false; // <-- if clear all active then filter out
          return filters.ALL || filters[el.category]; // <-- check if ALL or category active
        }
        return true;
      })
      .map((elem) => (
        <li key={elem.id}>
          Id: {elem.id} Category: {elem.category}
        </li>
      ))}
    

    Edit how-to-display-data-on-top-of-others-instead-of-simultaneously-with-react-checkb

    Full Code

    const filterOptions = [
      {
        filter: "A",
        label: "Filter A"
      },
      {
        filter: "B",
        label: "Filter B"
      },
      {
        filter: "C",
        label: "Filter C"
      },
      {
        filter: "NONE",
        label: "Clear All"
      },
      {
        filter: "ALL",
        label: "Show All"
      }
    ];
    
    class Checkbox extends Component {
      state = {
        data: Items,
        filters: {}
      };
    
      toggleFilter = (e) => {
        const { name, checked } = e.target;
        this.setState((prevState) => ({
          filters: {
            ...prevState.filters,
            [name]: checked
          }
        }));
      };
    
      clearFilters = () => this.setState({ filters: {} });
    
      render() {
        return (
          <div>
            {filterOptions.map(({ filter, label }) => (
              <label key={filter}>
                {label}
                <input
                  type="checkbox"
                  name={filter}
                  onChange={this.toggleFilter}
                  checked={!!this.state.filters[filter]}
                />
              </label>
            ))}
            <button type="button" onClick={this.clearFilters}>
              Clear Filters
            </button>
    
            <br />
            {this.state.data
              .filter((el) => {
                const { filters } = this.state;
                if (Object.values(filters).some(Boolean)) {
                  if (filters.NONE) return false;
                  return filters.ALL || filters[el.category];
                }
                return true;
              })
              .map((elem) => (
                <li key={elem.id}>
                  Id: {elem.id} Category: {elem.category}
                </li>
              ))}
          </div>
        );
      }
    }