5

I'm not able to do multi cross filtering, when I click on Apply (to apply all my selected options from my dropdowns) or Cancel button (to reset the selected options). For example filter by taste and availability (please see the picture). But I'm not able to render the filtered rows/updated table.

export default function MenuDisplay() {
  const { menuId } = useParams();
  const { match } = JsonData;
  const [selected, setSelected] = useState({});
  const [hidden, setHidden] = useState({});
  const [taste, setTaste] = useState([
    { label: "Good", value: "Good", name: "Good", selected: false },
    { label: "Medium", value: "Medium", name: "Medium", selected: false },
    { label: "Bad", value: "Bad", name: "Bad", selected: false }
  ]);

  const [comments, setComments] = useState([
    { label: "0", value: "0", name: "0", selected: false },
    { label: "1", value: "1", name: "1", selected: false },
    { label: "2", value: "2", name: "2", selected: false },
    { label: "3", value: "3", name: "3", selected: false },
    { label: "4", value: "4", name: "4", selected: false },
    { label: "5", value: "5", name: "5", selected: false }
  ]);

  const [availability, setAvailability] = useState([
    {
      label: "availability",
      value: "availability",
      name: "Availability",
      selected: false
    },
    { label: "trust", value: "trust", name: "Trust", selected: false }
  ]);

  function selectionOpt(setItems) {
    return (selection) => {
      setItems(selection);
    };
  }

  const impact = (value) => {
    if (value === 1) {
      return (
        <div>
          <TaskAltIcon />
        </div>
      );
    } else {
      return (
        <div>
          <CancelIcon />
        </div>
      );
    }
  };

  // If any row is selected, the button should be in the Apply state
  // else it should be in the Cancel state
  const buttonMode = Object.values(selected).some((isSelected) => isSelected)
    ? "apply"
    : "cancel";

  const rowSelectHandler = (id) => (checked) => {
    setSelected((selected) => ({
      ...selected,
      [id]: checked
    }));
  };

  const handleClick = () => {
    if (buttonMode === "apply") {
      // Hide currently selected items
      const currentlySelected = {};
      Object.entries(selected).forEach(([id, isSelected]) => {
        if (isSelected) {
          currentlySelected[id] = isSelected;
        }
      });
      setHidden({ ...hidden, ...currentlySelected });

      // Clear all selection
      const newSelected = {};
      Object.keys(selected).forEach((id) => {
        newSelected[id] = false;
      });
      setSelected(newSelected);
    } else {
      // Select all currently hidden items
      const currentlyHidden = {};
      Object.entries(hidden).forEach(([id, isHidden]) => {
        if (isHidden) {
          currentlyHidden[id] = isHidden;
        }
      });
      setSelected({ ...selected, ...currentlyHidden });

      // Clear all hidden items
      const newHidden = {};
      Object.keys(hidden).forEach((id) => {
        newHidden[id] = false;
      });
      setHidden(newHidden);
    }
  };

  const matchData = (
    match.find((el) => el._id_menu === menuId)?._ids ?? []
  ).filter(({ _id }) => {
    return !hidden[_id];
  });

  const getRowProps = (row) => {
    return {
      style: {
        backgroundColor: selected[row.values.id] ? "lightgrey" : "white"
      }
    };
  };

  const data = [
    {
      Header: "id",
      accessor: (row) => row._id
    },
    {
      Header: "Name",
      accessor: (row) => (
        <Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>{row.name}</Link>
      )
    },
    {
      Header: "Taste",
      accessor: (row) => row.taste
    },
    {
      Header: "Comments",
      //check current row is in hidden rows or not
      accessor: (row) => {
        const comments = parseInt(row.comments, 10);

        return <Counter count={comments} />;
      }
    },
    {
      Header: "Price",
      accessor: (row) => row.price,
      id: "price"
    },
    {
      Header: "Status",
      accessor: (row) => row.status
    },
    {
      Header: "Availability",
      accessor: (row) => row.availability,
      id: "availability",
      Cell: (props) => impact(props.value)
    },
    {
      Header: "Trust",
      accessor: (row) => row.trust,
      id: "trust",
      Cell: (props) => impact(props.value)
    },
    {
      Header: "Show",
      accessor: (row) => (
        <Toggle
          value={selected[row._id]}
          onChange={rowSelectHandler(row._id)}
        />
      )
    }
  ];

  const initialState = {
    sortBy: [
      { desc: false, id: "id" },
      { desc: false, id: "description" }
    ],
    hiddenColumns: ["dishes", "id"]
  };

  return (
    <div>
      <button type="button" onClick={handleClick}>
        {buttonMode === "cancel" ? "Cancel" : "Apply"}
      </button>
      <div className="flex justify-end gap-4 ">
        <div>
          <Button>Apply</Button>
        </div>
        <div>
          <Button>Cancel</Button>
        </div>
      </div>
      Taste
      <ListDrop
        placeholder={"Select"}
        items={taste}
        onSelect={selectionOpt(setTaste)}
        hasAll
      />
      Comments
      <ListDrop
        placeholder={"Select"}
        items={comments}
        onSelect={selectionOpt(setComments)}
        hasAll
      />
      <p>Availability & Trust </p>
      {/* I would like to have in my dropdown Availability and Trust as 
           options in my dropdown and it refers to the cross where availaibility: 1 and trust:1 ) */}
      <ListDrop
        placeholder={"Select"}
        items={availability}
        onSelect={selectionOpt(setAvailability)}
        hasAll
      />
      <Table
        data={matchData}
        columns={data}
        initialState={initialState}
        withCellBorder
        withRowBorder
        withSorting
        withPagination
        rowProps={getRowProps}
      />
    </div>
  );
}

Please check my codeSandbox

Please check the picture to get an idea :

enter image description here

Zokulko
  • 211
  • 4
  • 25
  • In the codesandbox you linked, you are using version 1.4.1 of `@headlessui/react` with version 18.0.0 of `React`. I switched to the latest version 1.6.5 and the popover appears to be working. It looks like React 18 compatibility for headlessui was added in [version 1.6.0](https://github.com/tailwindlabs/headlessui/releases/tag/%40headlessui%2Freact%40v1.6.0) – gloo Jun 23 '22 at 21:03
  • @gloo Why do I always get the menus name as options for all dropdowns ? – Zokulko Jun 26 '22 at 12:12
  • @Onur Gelmez is there a way to have the forked sandbox please ? – Zokulko Jun 29 '22 at 22:24
  • Are you using any UI frameworks like Antd or MUI etc .., – Sujith Sandeep Jul 12 '22 at 05:35
  • @SujithSandeep, only react-table – Zokulko Jul 13 '22 at 12:38

2 Answers2

0

I choose some of the fields and create a simple App for you, if you read the code carefully, you will figure out how it works and then you can change your code similarly :

table {
  font-family: arial, sans-serif;
  border-collapse: collapse;
  width: 100%;
}

td,
th {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}

tr:nth-child(even) {
  background-color: #dddddd;
}

import React, { useEffect } from "react";
import "./styles.css";

const soupsData = [
  {
    name: "Pea Soup",
    taste: "Good",
    trust: 1,
    availability: 0
  },
  {
    name: "Potato Soup ",
    taste: "Medium",
    trust: 1,
    availability: 1
  },
  {
    name: "Cucumber Soup ",
    taste: "Medium",
    trust: 1,
    availability: 1
  }
];

const tasteOptions = [
  { label: "Good", name: "Good" },
  { label: "Medium", name: "Medium" },
  { label: "Bad", name: "Bad" }
];

const availabilityAndTrustOptions = [
  {
    name: "all",
    label: "all"
  },
  {
    name: "availability",
    label: "availability"
  },
  {
    name: "trust",
    label: "trust"
  }
];

function App() {
  const [soups, setSoups] = React.useState([]);
  const [tastes, setTastes] = React.useState([]);
  const [availabilityAndTrust, setAvailabilityAndTrust] = React.useState({
    trust: 0,
    availability: 0
  });

  useEffect(() => {

    const filteredSoups = soupsData.filter((soup) => {
      const availability = availabilityAndTrust.availability;
      const trust = availabilityAndTrust.trust;

      if (availability) {
        if (!soup.availability) return false;
      }

      if (trust) {
        if (!soup.trust) return false;
      }

      if (tastes.length > 0) {
        if (!tastes.includes(soup.taste)) return false;
      }

      return true;
    });

    setSoups(() => filteredSoups);
  }, [tastes, availabilityAndTrust]);

  const handleTasteChange = (e) => {
    const targetTaste = e.target.name;
    if (tastes.includes(targetTaste)) {
      setTastes(() => tastes.filter((taste) => taste !== targetTaste));
    } else setTastes((prevData) => [...prevData, targetTaste]);
  };

  const handleAvailabilityAndTrustChange = (e) => {
    const targetName = e.target.name;
    switch (targetName) {
      case "all":
        setAvailabilityAndTrust((prevData) => {
          if (prevData.trust && prevData.availability) {
            return {
              trust: 0,
              availability: 0
            };
          }
          if (prevData.trust || prevData.availability) {
            return {
              trust: 1,
              availability: 1
            };
          }
          return {
            trust: 1,
            availability: 1
          };
        });
        break;
      case "availability":
        setAvailabilityAndTrust((prevData) => {
          return {
            trust: prevData.trust,
            availability: prevData.availability ? 0 : 1
          };
        });
        break;
      case "trust":
        setAvailabilityAndTrust((prevData) => {
          return {
            trust: prevData.trust ? 0 : 1,
            availability: prevData.availability
          };
        });
        break;
      default:
    }
  };

  return (
    <div>
      <div>
        <div>Taste</div>
        {tasteOptions.map((taste) => {
          return (
            <div>
              <label for={taste.label}>{taste.label}</label>
              <input
                name={taste.name}
                onChange={handleTasteChange}
                id={taste.label}
                type="checkbox"
              />
            </div>
          );
        })}
      </div>
      <br />
      <div>
        <div>Availability & Trust</div>
        {availabilityAndTrustOptions.map((option) => {
          return (
            <div>
              <label for={option.label}>{option.label}</label>
              <input
                name={option.name}
                checked={(() => {
                  switch (option.label) {
                    case "all":
                      return (
                        availabilityAndTrust.availability &&
                        availabilityAndTrust.trust
                      );
                    case "availability":
                      return availabilityAndTrust.availability;
                    case "trust":
                      return availabilityAndTrust.trust;
                    default:
                      return false;
                  }
                })()}
                onChange={handleAvailabilityAndTrustChange}
                id={option.label}
                type="checkbox"
              />
            </div>
          );
        })}
      </div>
      <br />
      <table>
        <tr>
          <th>Name</th>
          <th>Taste</th>
          <th>Trust</th>
          <th>Availability</th>
        </tr>
        {soups.map((soup) => {
          return (
            <tr>
              <td>{soup.name}</td>
              <td>{soup.taste}</td>
              <td>{soup.trust}</td>
              <td>{soup.availability}</td>
            </tr>
          );
        })}
      </table>
    </div>
  );
}

you should use useState for storing data that might change during runtime and has a rendering effect.

I used useEffect to filter the table's content according to the options change.

codesandbox : https://codesandbox.io/s/serene-shamir-lns51i

Omid Poorali
  • 169
  • 4
  • is there a way to use my Component `ListDrop` instead of `checkboxes` – Zokulko Jul 01 '22 at 13:13
  • why not, I just write the code as simple as possible to show you how the filter mechanism works but you can do many things – Omid Poorali Jul 01 '22 at 13:42
  • check out the link : https://codesandbox.io/s/crazy-rain-duzfsm – Omid Poorali Jul 01 '22 at 16:09
  • But I've already done that (see my [codesandox](https://codesandbox.io/s/filters-frpjw8?file=/src/MenuDisplay.jsx:0-5532)). Here you're displaying the options in a dropdown and I've already done that. My issue is to update my table by rendering the filtered data rows, with `Apply` and `Cancel` buttons; which is not the case with your [codesandox](https://codesandbox.io/s/crazy-rain-duzfsm?file=/src/App.js) – Zokulko Jul 02 '22 at 12:07
  • I just wanted to have an example with my `ListDrop` instead of `checkboxes` and trying to render the updated table please. How can I do that?? – Zokulko Jul 02 '22 at 22:27
0

I wrote three function for Apply and Cancel Functionality

`const applyFilters = () => {
    const allData = match.find((el) => el._id_menu === menuId)?._ids ?? [];
    let temp = allData.filter((entity) => {
      const selectedTaste = taste.find((entity) => entity.selected);
      const selectedComments = comments.find((entity) => entity.selected);
      let condition = true;
      if (selectedTaste && selectedTaste.value !== entity.taste) {
        condition = false;
      }
      if (selectedComments && selectedComments.value !== entity.comments) {
        condition = false;
      }
      return condition;
    });
    setFilteredData(temp);
  };

  const resetFilters = (filterList) =>
    filterList.map((entity) => ({
      ...entity,
      selected: false
    }));

  const cancelClickhandler = () => {
    setTaste(resetFilters(taste));
    setAvailability(resetFilters(availability));
    setComments(resetFilters(comments));
    setFilteredData(match.find((el) => el._id_menu === menuId)?._ids ?? []);
  };`

Apply and Cancel Functionaly is working as expected. I have also made few more changes in your codesandBox, 1- added onclick prop in Button Component

<div>
   <Button onClick={applyFilters}>Apply</Button>
 </div>
 <div>
   <Button onClick={cancelClickhandler}>Cancel</Button>
 </div>

2- added on state variable for storing filteredData

  `const [filteredData, setFilteredData] = useState(
    match.find((el) => el._id_menu === menuId)?._ids ?? []
  );`

3- modified matchdata return value

`const matchData = filteredData.filter(({ _id }) => {
  return !hidden[_id];
});`

Please look into modified sandbox https://codesandbox.io/s/filters-forked-lq0ni4

Mr. Dev
  • 71
  • 7
  • Hello, what is the difference between `resetFilters` and `cancelClickhandler` – Zokulko Jul 08 '22 at 12:11
  • Since I receive the json (`menus`) from my api already filtered i.e the json for a particular menuId (stored in a state `const [menus, setMenus] =useState([])` . Then, the added on state variable for storing `filteredData` is not necessary (i.e `const match.find((el) => el._id_menu === menuId)?._ids ?? [] );` So I've keeped :`const matchData = menus.filter(({ _id }) => {return !hidden[_id];});` but do I need to write `setMenus(temp)` inside `applyFilters`; and remove `setFilteredData` inside `cancelClickHandler`?? – Zokulko Jul 08 '22 at 20:31
  • Filter for availability is not working with your actual code, is it normal? – Zokulko Jul 09 '22 at 19:39
  • I have not written code for availability filter, you can write it just like others two. And yes if you can reduce complexity and few lines of codes then welcome. – Mr. Dev Jul 10 '22 at 20:07
  • I've tried to write for `availability` for example, but since it's an integer 0 or 1. Writing if `(selectedAvailability && selectedAvailability .value !== entity.availability) {condition = false; }`, will not work since I'm comparing a string with an integer, right? And if I click on filter by `availability` and /or `trust` it means only filter when their value is 1 – Zokulko Jul 10 '22 at 21:19
  • Hello @Zokulko, you can't just copy that logic and paste for availability filter, it will not work like that, because in your match.json you are not string data in same way, you got my point, so first you need to write it like this if (selectedAvailability && !entity[selectedAvailability .value]) { condition = false; } – Mr. Dev Jul 20 '22 at 13:59