javascriptreactjsfilteronclicklistener

React - Creating custom filters


I've technically created the functionality I want to achieve with a dropdown select field for picking the filter to use for filtering a list of products by category and brand. I've built the below snippet so you can see an example of the working code (however I can't get it to run properly in the code snippet tool although the exact same code is working here in this CodePen I made: https://codepen.io/Chobbit/pen/qBzrPLe)

const {useState, useEffect}  = React

const productDataList = {
  "products": [
    {
      "id": 1,
      "title": "Essence Mascara Lash Princess",
      "category": "beauty",
      "price": 9.99,
      "brand": "Essence",
      "thumbnail": "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png"
    },
    {
      "id": 2,
      "title": "Eyeshadow Palette with Mirror",
      "category": "beauty",
      "price": 19.99,
      "brand": "Glamour Beauty",
      "thumbnail": "https://cdn.dummyjson.com/products/images/beauty/Eyeshadow%20Palette%20with%20Mirror/thumbnail.png"
    },
    {
      "id": 3,
      "title": "Powder Canister",
      "category": "beauty",
      "price": 14.99,
      "brand": "Velvet Touch",
      "thumbnail": "https://cdn.dummyjson.com/products/images/beauty/Powder%20Canister/thumbnail.png"
    },
    {
      "id": 4,
      "title": "Dior J'adore",
      "category": "fragrances",
      "price": 89.99,
      "brand": "Dior",
      "thumbnail": "https://cdn.dummyjson.com/products/images/fragrances/Dior%20J'adore/thumbnail.png"
    },
    {
      "id": 5,
      "title": "Dolce Shine Eau de",
      "category": "fragrances",
      "price": 69.99,
      "brand": "Dolce & Gabbana",
      "thumbnail": "https://cdn.dummyjson.com/products/images/fragrances/Dolce%20Shine%20Eau%20de/thumbnail.png"
    },
    {
      "id": 6,
      "title": "Annibale Colombo Sofa",
      "category": "furniture",
      "price": 2499.99,
      "brand": "Annibale Colombo",
      "thumbnail": "https://cdn.dummyjson.com/products/images/furniture/Annibale%20Colombo%20Sofa/thumbnail.png"
    },
  ],
  "total": 194,
  "skip": 0,
  "limit": 36
}

const App = () => {

  const [productsLimit, setProductsLimit] = useState(productDataList.limit)
  const [filterData, setFilterData] = useState(productDataList.products || []);
  const [categories, setCategories] = useState([]);
  const [brands, setBrands] = useState([]);

  useEffect(() => {
    let filteredCategories = [],
        filteredBrands = [];
        productDataList.products.forEach((product) => {
      if (!filteredCategories.includes(product.category)) {
        filteredCategories.push(product.category);
      }
      if (!filteredCategories.includes(product.brand)) {
        filteredBrands.indexOf(product.brand) == -1 && product.brand != '' && product.brand != undefined && product.brand != null ? filteredBrands.push(product.brand) : null;
      }
    });
    setCategories(filteredCategories);
    setBrands(filteredBrands);
  }, []);

  const productFilters = (products, filt) => {
    let filteredProducts = [];
    if (categories.includes(filt)) {
      filteredProducts = products.category.toLowerCase() === filt.toLowerCase();
    } else if (brands.includes(filt)) {
      filteredProducts = products.brand === filt;
    }
    return filteredProducts;
  };

  const filter = (byFilter) => {
    if (byFilter) {
      let productsData = [...productDataList.products];
      productsData = productsData.filter((filter) => {
        return productFilters(filter, byFilter);
      });
      setFilterData(productsData);
    } else {
      setFilterData(productDataList.products);
    }
  };

  return (

    <div>
      <select onChange={(e) => filter(e.target.value)}>
        <option value="Filter Categories" selected="selected">Filter by categories</option>
        {categories.map((catFilter) => (
          console.log(catFilter),
          <option value={catFilter}>{catFilter}</option>
        ))}
      </select>
      <select onChange={(e) => filter(e.target.value)}>
        <option value="Filter Categories" selected="selected">Filter by brands</option>
        {brands.map((item) => (
          console.log(item),
          <option value={item}>{item}</option>
        ))}
      </select>
      <button onClick={filter}>Clear filter</button>
    


      <div className='products'>

        {/* checks the product data exists before mapping it */}
        {filterData?.slice(0, productsLimit).map(product => (
          <div className='product' key={product.id}>
            <div className='productImage'>
              <img src={product.thumbnail} />
            </div>
            <div className='productInfo'>
              <div className='productTitle'>
                <div className='productBrand'>{product.brand}</div> 
                <div className='productName'>{product.title}</div>
              </div>
              <div className='productPrice'>Price: £{product.price}</div>
              {}
            </div>

          </div>
        ))}
      </div>
    </div>
  )
}

// ReactDOM.render(<App />, document.querySelector('#root'));
.products {
    display: flex;
    flex-wrap: wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

**

What I want to do now is achieve the same filtering results but using a more visible list of filters a user can click instead of dropdown menus.

**

I've tried two methods already, the first was with a function that created an element for each different category/brand option found in the mapped product data json and appended them to filter fields. The second method was just mapping through the category/brand options found in the return() JSX this time. Both methods produce the filter list on the page the way I want them too appear with the filter and the amount of products that filter would return:

Filters appearance on the page

However the problem I'm having is method 1 does nothing on click when I try adding an onClick to each filter created that will call the filter() function. So I feel like I'm not adding this bit correctly onClick=${(e) => filter(e.target.textContent.replace(/\s+$/, ''))} to trigger the function when creating the new elements with:

dataFilters.innerHTML = Object.entries(numberOfFilters).map(([mapFilterName,numberFound])=>`<div class="filter"><div class="name" onClick=${(e) => filter(e.target.textContent.replace(/\s+$/, ''))}>${mapFilterName} </div> <div class="count"> (${numberFound})</div></div>`).join("\n");

The second method when I set it up in the jsx like this:

<div className="catagoryFilters f_rmc f_sb">
  <div className="filterTitle">Categories</div>
  <div className="filtersList">
    {categories.map((filter) => (
      <div className="filter">
        <div className="name" onClick={(e) => filter(e.target.textContent.replace(/\s+$/, ''))}>
          {filter} 
        </div> 
        <div className="count"> 
          ({filter.length})
        </div>
      </div>
    ))}
  </div>
</div>

It does try to call the filter() function but all I get is this error message and I don't understand why?: Function call error

I've tried a few things to get this working this way and despite getting it working using dropdowns, I'm obviously doing something wrong which I'm not quite understanding, to correctly call the filter() function when clicking a filter in the mapped lists.

So any help getting this working and understanding what I'm doing wrong would be greatly appreciated thanks.


Solution

  • Check the filtering function and make sure it is set up properly, here is some example as a sample code,

    const { useState, useEffect } = React;
    
    const productDataList = {
      products: [
        {
          id: 1,
          title: "Essence Mascara Lash Princess",
          category: "beauty",
          price: 9.99,
          brand: "Essence",
          thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png",
        },
        {
          id: 2,
          title: "Eyeshadow Palette with Mirror",
          category: "beauty",
          price: 19.99,
          brand: "Glamour Beauty",
          thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Eyeshadow%20Palette%20with%20Mirror/thumbnail.png",
        },
        {
          id: 3,
          title: "Powder Canister",
          category: "beauty",
          price: 14.99,
          brand: "Velvet Touch",
          thumbnail: "https://cdn.dummyjson.com/products/images/beauty/Powder%20Canister/thumbnail.png",
        },
        {
          id: 4,
          title: "Dior J'adore",
          category: "fragrances",
          price: 89.99,
          brand: "Dior",
          thumbnail: "https://cdn.dummyjson.com/products/images/fragrances/Dior%20J'adore/thumbnail.png",
        },
        {
          id: 5,
          title: "Dolce Shine Eau de",
          category: "fragrances",
          price: 69.99,
          brand: "Dolce & Gabbana",
          thumbnail: "https://cdn.dummyjson.com/products/images/fragrances/Dolce%20Shine%20Eau%20de/thumbnail.png",
        },
        {
          id: 6,
          title: "Annibale Colombo Sofa",
          category: "furniture",
          price: 2499.99,
          brand: "Annibale Colombo",
          thumbnail: "https://cdn.dummyjson.com/products/images/furniture/Annibale%20Colombo%20Sofa/thumbnail.png",
        },
      ],
      total: 194,
      skip: 0,
      limit: 36,
    };
    
    const App = () => {
      const [productsLimit, setProductsLimit] = useState(productDataList.limit);
      const [filterData, setFilterData] = useState(productDataList.products || []);
      const [categories, setCategories] = useState([]);
      const [brands, setBrands] = useState([]);
    
      useEffect(() => {
        let filteredCategories = [];
        let filteredBrands = [];
        productDataList.products.forEach((product) => {
          if (!filteredCategories.includes(product.category)) {
            filteredCategories.push(product.category);
          }
          if (!filteredBrands.includes(product.brand)) {
            filteredBrands.push(product.brand);
          }
        });
        setCategories(filteredCategories);
        setBrands(filteredBrands);
      }, []);
    
      const productFilters = (product, filter) => {
        if (categories.includes(filter)) {
          return product.category.toLowerCase() === filter.toLowerCase();
        } else if (brands.includes(filter)) {
          return product.brand === filter;
        }
        return true;
      };
    
      const filter = (byFilter) => {
        if (byFilter) {
          let productsData = [...productDataList.products];
          productsData = productsData.filter((product) => productFilters(product, byFilter));
          setFilterData(productsData);
        } else {
          setFilterData(productDataList.products);
        }
      };
    
      const clearFilter = () => {
        setFilterData(productDataList.products);
      };
    
      return (
        <div>
          <div className="filterSection">
            <div className="categoryFilters">
              <div className="filterTitle">Categories</div>
              <div className="filtersList">
                {categories.map((category) => (
                  <div className="filter" key={category}>
                    <div className="name" onClick={() => filter(category)}>
                      {category}
                    </div>
                    <div className="count">({filterData.filter(product => product.category === category).length})</div>
                  </div>
                ))}
              </div>
            </div>
            <div className="brandFilters">
              <div className="filterTitle">Brands</div>
              <div className="filtersList">
                {brands.map((brand) => (
                  <div className="filter" key={brand}>
                    <div className="name" onClick={() => filter(brand)}>
                      {brand}
                    </div>
                    <div className="count">({filterData.filter(product => product.brand === brand).length})</div>
                  </div>
                ))}
              </div>
            </div>
            <button onClick={clearFilter}>Clear filter</button>
          </div>
          <div className="products">
            {filterData.slice(0, productsLimit).map((product) => (
              <div className="product" key={product.id}>
                <div className="productImage">
                  <img src={product.thumbnail} alt={product.title} />
                </div>
                <div className="productInfo">
                  <div className="productTitle">
                    <div className="productBrand">{product.brand}</div>
                    <div className="productName">{product.title}</div>
                  </div>
                  <div className="productPrice">Price: £{product.price}</div>
                </div>
              </div>
            ))}
          </div>
        </div>
      );
    };
    
    // ReactDOM.render(<App />, document.querySelector("#root"));
    

    Funny enough I was working on something similar like this, so this is my adaptation to the application for your example, try this out n lemme know what happens.