Search code examples
javascriptreactjsjquery-isotope

Reactjs - Isotope layout - using data-attributes to filter/sort


I am trying to streamline the isotope handler in a react component -- I want to create filter options that would be like catergory[metal] or catergory[transition] -- and combo filters like catergory[metal, transition].

the sandbox for this. https://codesandbox.io/s/brave-sea-tnih7

So like filterFns[ filterValue, "param2" ] -- how to push 2 params into the filter functions - rather than these fixed greaterThan50 -- a "greaterThan(50)", "greaterThan(5) -- this dynamic filtering type of handler "

enter image description here

latest sandbox https://codesandbox.io/s/brave-sea-tnih7?file=/src/IstotopeWrapper.js enter image description here

import React, { Component } from 'react';

import Isotope from 'isotope-layout';

import Button from '@material-ui/core/Button';
import ButtonGroup from '@material-ui/core/ButtonGroup';

import GenericForm from '../GenericForm/GenericForm';

import './IsotopeHandler.scss';


class IsotopeHandler extends Component {
    constructor() {
      super();
      this.myRef = React.createRef();


      let items = [{
        "name": "Mercury",
        "category": "transition",
        "weight": "122",
        "symbol": "Hg",
        "number": 12,
        "custom": "a",
        "isgas": false
      }, {
        "name": "Tellurium",
        "category": "metal",
        "weight": "12",
        "symbol": "Te",
        "number": 232,
        "custom": "b",
        "isgas": false
      }, {
        "name": "Bismuth",
        "category": "rock",
        "weight": "2200.59",
        "symbol": "Bi",
        "number": 1666,
        "custom": "c",
        "isgas": false
      }, {
        "name": "Cadmium",
        "category": "metal",
        "weight": "1200",
        "symbol": "Cd",
        "number": 454,
        "custom": "d",
        "isgas": false
      }, {
        "name": "Bosim",
        "category": "gas",
        "weight": "100",
        "symbol": "Gs",
        "number": 454,
        "custom": "e",
        "isgas": true
      }, {
        "name": "xosim",
        "category": "gas",
        "weight": "100",
        "symbol": "xs",
        "number": 44,
        "custom": "f",
        "isgas": true
      }]

      this.state = { sortBy: 'original-order', items: items };

      this.onFilterHandler = this.onFilterHandler.bind(this);
      this.onSortHandler = this.onSortHandler.bind(this);
    }

    onSortHandler(sortValue){
      if(sortValue){
        console.log("sortValue", sortValue);
        let bool = this.state.bool;
        let options = { sortBy: sortValue };
        
        options = Object.assign(options, {
            sortAscending: (bool = !bool),
        });        

        this.setState({ bool: bool });
        this.setState({ sortBy: sortValue });

        this.state.iso.arrange(options);
      }
    }

    onFilterHandler(filterName, filterField, filterValue){

        var filterFns = {
          //match
          matches: function(itemElem) {
            var values = itemElem.getAttribute('data-'+filterField);
            return values.match(filterValue);
          },

          //greaterThan 
          greaterThan: function(itemElem) {
            var number = itemElem.getAttribute('data-'+filterField);
            return parseInt(number, 10) > filterValue;
          },

          //lessThan 
          lessThan: function(itemElem) {
            var number = itemElem.getAttribute('data-'+filterField);
            return parseInt(number, 10) < filterValue;
          }, 

          //Between 
          between: function(itemElem) {
            var number = itemElem.getAttribute('data-'+filterField);
            return parseInt(number, 10) > parseInt(filterValue.split(",")[0], 10) && parseInt(number, 10) < parseInt(filterValue.split(",")[1], 10);
          }, 

        };

        // use matching filter function
        let val = filterFns[filterName] || filterValue;

        this.state.iso.arrange({ filter: val });
    }

    submitFilterFormHandler(data){
      console.log("data now with parent", data);

      if(data){
        //open dialog box
        //console.log("this", this);
      }
    }

    componentDidMount(){
        // init Isotope
        var iso = new Isotope(this.myRef.current, {
          itemSelector: '.grid-item',
          layoutMode: 'fitRows',
          getSortData: this.getCustomSortAttributes(this.state.items[0])
        });

        this.setState({ iso: iso });
        console.log("iso",iso);
    }

    getCustomSortAttributes(item){
      let keys = Object.keys(item);
      let dataObj = {};
      
      for (let i = 0; i < keys.length; i++) {
        var obj = {
          [keys[i]]: '[data-'+keys[i]+']'
        }
        dataObj = {...obj, ...dataObj}
      }
      return dataObj;      
    }

    attributeGeneration(item){
      let keys = Object.keys(item);
      let dataObj = {};
      
      for (let i = 0; i < keys.length; i++) {
        var obj = {
          ["data-"+keys[i]]: item[keys[i]]
        }
        dataObj = {...obj, ...dataObj}
      }
      return dataObj;
    }

    render() {


        var sorts = [{
          "label": "Original Order",
          "value": "original-order"
        },{
          "label": "Name",
          "value": "name"
        },{
          "label": "Symbol",
          "value": "symbol"
        },{
          "label": "Number",
          "value": "number"
        },{
          "label": "Weight",
          "value": "weight"
        },{
          "label": "Category",
          "value": "category"
        },{
          "label": "Custom",
          "value": "custom"
        }]


        let filters = [{
            "label": "show all",
            "params": {
              "filter": "showAll",
              "field": "",
              "value": "*"
            }
          },{
            "label": "number > 50",
            "params": {
              "filter": "greaterThan",
              "field": "number",
              "value": 50
            }
          },{
            "label": "number < 350",
            "params": {
              "filter": "lessThan",
              "field": "number",
              "value": 350
            }
          },{
            "label": "between- number < 350 && number < 600",
            "params": {
              "filter": "between",
              "field": "number",
              "value": "350,600"
            }
          },{
            "label": "metal category",
            "params": {
              "filter": "matches",
              "field": "category",
              "value": "metal"
            }
          },{
            "label": "metal transition",
            "params": {
              "filter": "matches",
              "field": "category",
              "value": "transition"
            }
          },{
            "label": "metal gas and transition",
            "params": {
              "filter": "matches",
              "field": "category",
              "value": "gas|transition"
            }
          },{
            "label": "isGas",
            "params": {
              "filter": "matches",
              "field": "isgas",
              "value": true
            }
          }]



        let initialFilterFormValues = {
          "manual": "type to filter"
        }

        let fieldsFilterForm = [
            {
              "type": "text",
              "label": "Manual Entry",
              "name": ["manual"],
              "options": []
            },
            {
              "type": "buttons",
              "label": "Sweets2",
              "name": ["sweets2"],
              "options": [
                {
                  "label": "Sherbert",
                  "value": "0"
                },
                {
                  "label": "Peach Jam",
                  "value": "1"
                },
                {
                  "label": "Almond Butter",
                  "value": "2"
                }
              ],
              //"validate": ["required"],
            }            
          ];

        let buttonsFilterForm = [
          /*{
            "label": "Submit3",
            "variant": "contained",
            "color": "primary",
            "type": "submit",
            "disabled": ["submitting"],
            "onClick" : null
          }*/
        ];


        return (
          <div 
            className={"isotope"}
          >

            <div className="button-group filters-button-group">
              <ButtonGroup variant="outlined" color="primary" aria-label="outlined primary button group">
              {                
                filters.map((item, j) => {
                  return(
                    <Button 
                      key={j} 
                      className="button" 
                      onClick={() => this.onFilterHandler(item.params.filter, item.params.field, item.params.value)}
                    >
                      {item.label}
                    </Button>
                  )
                })
              }
              {/*
              <Button className="button" data-filter=".alkali, .alkaline-earth">alkali and alkaline-earth</Button>
              <Button className="button" data-filter=":not(.transition)">not transition</Button>
              <Button className="button" data-filter=".metal:not(.transition)">metal but not transition</Button>
              */}
              </ButtonGroup>   
            </div>

            <div className="button-group sort-by-button-group">
              <ButtonGroup variant="outlined" color="primary" aria-label="outlined primary button group">
                {                
                  sorts.map((item, j) => {
                    return(
                      <Button 
                        key={j} 
                        className="button" 
                        onClick={() => this.onSortHandler(item.value)}
                      >
                        {item.label}
                      </Button>
                    )
                  })
                }                
              </ButtonGroup>
            </div>


            {/*
            <GenericForm 
              initialValues={initialFilterFormValues} 
              fields={fieldsFilterForm}
              buttons={buttonsFilterForm}
              submitHandler={this.submitFilterFormHandler}
            />
            */}


            <div 
              className="grid" 
              ref={this.myRef}
            >
              {
                this.state.items.map((item, j) => {
                  return(
                    <div
                      key={j} 
                      className="grid-item"
                      {...this.attributeGeneration(item)}
                    >
                      <h3>{item.name}</h3>
                      <p>{item.weight}</p>
                      <p>{item.number}</p>
                      <p>{item.symbol}</p>
                      <p>{item.category}</p>
                    </div>
                  )
                })
              }
            </div>

          </div>
        );
    }
}

export default IsotopeHandler;

Solution

  • In you application you can stor filters inside an array and update filters array to either new filter (single filter mode) or push newly selected filter on top of existing filters list.

    Take a look at updated codesandbox here.

    First of all we are registering our filter method with Isotope:

    componentDidMount() {
      // init Isotope
      var iso = new Isotope(this.myRef.current, {
        itemSelector: ".grid-item",
        layoutMode: "fitRows",
        getSortData: this.getCustomSortAttributes(this.state.items[0]),
        filter: this.filterData  // <- custom filter method
      });
    
      this.setState({ iso: iso });
      console.log("iso", iso);
    }
    

    Updated onFilterHandler and it takes 3 arguments

    1. addFilter: boolean flag to indicate if new filter condition should be added or existing should be removed
    2. filterCondition: a string condition or an array of conditions in form rule|field|value
    3. type: string value that would be wither AND or OR to determine if all conditions in type should be matched or any single should be matched

    based on the areguments function either adds new filter or searches for existing one and removes it from global filters array.

    getFilterObject(condition) {
      const conditionItems = condition.split("|");
      const filterName = conditionItems[0];
      const filterField = conditionItems[1];
      const filterValue = conditionItems[2];
    
      return {
        condition: condition,
        name: filterName,
        field: filterField,
        value: filterValue,
        filterFunc: this.filterFns[filterName]
      };
    }
    
    onFilterHandler(addFilter, filterCondition, type = "AND") {
      // use matching filter function
      // let val = filterFns[filterName] || filterValue;
      const filterObj = {
        type: type,
        filters: Array.isArray(filterCondition)
          ? filterCondition.map((cnd) => this.getFilterObject(cnd))
          : [this.getFilterObject(filterCondition)]
      };
      const existingFilters = this.filters.filter(
        (f) =>
          f.filters.length === filterObj.filters.length &&
          f.filters.every((f) =>
            filterObj.filters.some(
              (fObjFilter) => fObjFilter.condition === f.condition
            )
          )
      );
    
      if (addFilter && existingFilters.length === 0) {
        this.filters = [...this.filters, filterObj];
      } else if (!addFilter) {
        this.filters = this.filters.filter((f) => !existingFilters.includes(f));
      }
    
      this.state.iso.arrange();
    }
    

    newly added filterData function will be responsible for filtering data based on current filters list:

    filterData(item) {
        if (this.filters.length <= 0) {
          return true;
        }
    
        const filterPredicate = (filterObj) => {
          return typeof filterObj.filterFunc === "function"
            ? filterObj.filterFunc(item, filterObj.field, filterObj.value)
            : filterObj.value === "*"
            ? true
            : item.classList.contains(filterObj.value);
        };
    
        for (let idx = 0; idx < this.filters.length; idx++) {
          const filterObj = this.filters[idx];
          const retVal =
            filterObj.type === "OR"
              ? filterObj.filters.some((fObjFilter) => filterPredicate(fObjFilter))
              : filterObj.filters.every((fObjFilter) =>
                  filterPredicate(fObjFilter)
                );
    
          if (retVal) {
            return retVal;
          }
        }
        return false;
    }