Alani
Alani

Reputation: 151

Isotope: combination filtering + multiple selection

To all jQuery heros out there, I need help! (again)

I found similar questions to mine on the internet but there were no answers so far :-/

I want to filter a bunch of items with jquery isotope. I adapted the combination filter example from the plugin homepage, which works fine and looks like so:

$(function () {
var $container = $('#container'),
    filters = {};
$container.isotope({
    itemSelector: '.item',
    filter: '',
});

// filter links
$('.filter a').click(function () {
    var $this = $(this);
    // don't proceed if already selected
    if ($this.hasClass('selected')) {
        return;
    }

    var $optionSet = $this.parents('.option-set');
    // change selected class
    $optionSet.find('.selected').removeClass('selected');
    $this.addClass('selected');

    // store filter value in object
    // i.e. filters.color = 'red'
    var group = $optionSet.attr('data-filter-group');
    filters[group] = $this.attr('data-filter-value');

    // convert object into array
    var isoFilters = [];
    for (var prop in filters) {
        isoFilters.push(filters[prop])
    }
    var selector = isoFilters.join('');
    $container.isotope({
        filter: selector
    });
    return false;
}); });

and some html of the filter menu:

<div id="nav">
    <ul class="filter option-set" data-filter-group="who">
        <li><a href="#filter-who-any" data-filter-value="" class="selected">Any</a></li>
        <li><a href="#filter-who-boys" data-filter-value=".boys">Boys</a></li>
        <li><a href="#filter-who-girls" data-filter-value=".girls">Girls</a></li>
    </ul>

</div>

I set up a fiddle demo with 3 filter categories which can be combined: jsFiddle

Now I'd like to make it possible to select multiple tags from each category:

Category 1: a OR b OR c

AND

Categoy 2: a OR b OR c

AND

Categoy 3: a OR b OR c

There's a check-box-example by desandro, but this is not quite the thing I'm searching for, because I want to keep the AND condition between the categories. Basically I want 3 filter categories with some check-boxes for each of them (or links but I guess check-boxes are more functional). I think the methods used in both examples are completely different and I don't know how to combine their functionality.

I hope you get my point and that anyone could help me find a solution.

Thanks in advance!

Upvotes: 3

Views: 40218

Answers (6)

DrewT
DrewT

Reputation: 5072

Using multiple filter types with metafizzy isotope filtering requires a multi-dimensional array to hold an array of each filter type.

For a full code explanation see my answer to this question

Upvotes: 13

Frizzant
Frizzant

Reputation: 768

Don't like jQuery? I modified the code from http://codepen.io/desandro/pen/btFfG which @Luke posted as an answer to ESNext/Vanilla.

I figured I'd post this, since it is a fairly looked up question.

The only difference here is, that you have to create the HTML yourself.

class Metafizzy {
constructor(isotope) {
    this.isotope = isotope; // pass Isotope
    this.filters = {};
}

run(optionsContainerID, filterDisplayID) {
    let options = document.getElementById(optionsContainerID);
    let filterDisplay = document.getElementById(filterDisplayID);
    
    // do stuff when checkbox change
    options.addEventListener('change', (event) => {
        let inputElem = event.target;
        this.manageInputElement(inputElem);
        
        let comboFilter = this.getComboFilter(this.filters);
        
        this.isotope.arrange({ filter: comboFilter });
        
        //console.log(comboFilter); // uncomment to see the current filter output
    });
}

manageInputElement(inputElem) {
    let _this = this;
    // get the closest matching parent, and it's attribute value
    let parent = inputElem.closest('.iso-option-set');
    let group = parent.getAttribute('data-group');
    // create array for filter group, if not there yet
    let filterGroup = this.filters[group];
    if ( ! filterGroup) {
        filterGroup = this.filters[group] = [];
    }
    
    let isAll = inputElem.classList.contains('iso-all');
    // reset filter group if the all box was checked
    
    if (inputElem.type === 'checkbox') {
        this.doCheckBoxStuff(inputElem, parent, group, filterGroup, isAll)
    } else if (inputElem.type === 'range') {
        console.log('is element type "range"')
    }
    
}

doCheckBoxStuff(checkbox, parent, group, filterGroup, isAll) {
    if (isAll) {
        delete this.filters[group];
        if ( ! checkbox.checked) {
            checkbox.checked = true;
        }
    }
    // index of
    let hasCheckboxValue = filterGroup.includes(checkbox.value);
    let index = hasCheckboxValue ? 1 : -1;
    
    if (checkbox.checked) {
        let selector = isAll ? 'input' : 'input.iso-all';
        let allButton = parent.querySelector(selector);
        if (allButton) {
            allButton.checked = false;
        }
        
        if ( ! isAll && hasCheckboxValue === false) {
            // add filter to group
            this.filters[group].push(checkbox.value);
        }
        
    } else if ( ! isAll) {
        // remove filter from group
        // gets the index of the property by value
        function findWithAttr(array, value) {
            for(let i = 0; i < array.length; i += 1) {
                if(array[i] === value) {
                    return i;
                }
            }
            return -1;
        }
        let startIndex = findWithAttr(this.filters[ group ], checkbox.value)
        
        this.filters[ group ].splice( startIndex, 1 );
        // if all boxes unchecked, check the iso-all
        if ( ! parent.querySelectorAll('input:not(.iso-all):checked').length) {
            parent.querySelector('input.iso-all').checked = true;
        }
    }
}

getComboFilter(filters) {
    let i = 0;
    let comboFilters = [];
    let message = [];
    
    for (let prop in filters) {
        message.push(filters[prop].join(' '));
        let filterGroup = filters[prop];
        // skip to next filter group if it doesn't have any values
        if ( ! filterGroup.length) {
            continue;
        }
        if (i === 0) {
            // copy to new array
            comboFilters = filterGroup.slice(0);
        } else {
            let filterSelectors = [];
            // copy to fresh array
            let groupCombo = comboFilters.slice(0); // [ A, B ]
            // merge filter Groups
            for (let k = 0, len3 = filterGroup.length; k < len3; k++) {
                for (let j = 0, len2 = groupCombo.length; j < len2; j++) {
                    filterSelectors.push(groupCombo[j] + filterGroup[k]); // [ 1, 2 ]
                }
                
            }
            // apply filter selectors to combo filters for next group
            comboFilters = filterSelectors;
        }
        i++;
    }
    
    let comboFilter = comboFilters.join(', ');
    return comboFilter;
 }
}

This is how you instantiate it:

 <script>
    window.onload = function () {
        let element = document.getElementById('iso-filter-display');
        let iso = new Isotope(element, {
            // options
            itemSelector: '.iso-display-element'
        });
        let isoFilter = new Metafizzy(iso);
        isoFilter.run('iso-options-container','iso-filter-display')
    };
  </script>

Upvotes: 0

baaroz
baaroz

Reputation: 19587

have a look here

http://codepen.io/desandro/pen/btFfG

it's the same answer by DrewT but from the Isotope team.

all you have to do in order to filter is put every group under same object

var filters = {}; // should be outside the scope of the filtering function
 
//filtering function
$a.click(function() {       
    var group = $optionSet.attr('data-filter-group');
    var filterGroup = filters[group];
    if (!filterGroup) {
        filterGroup = filters[group] = [];
    }
    filters[group].push($this.attr('data-filter-value'));
})
       

now all you have to do is call getComboFilter function (you don't have to understand the code inside getComboFilter, think of it as a part of isotope)

var comboFilter = getComboFilter( filters );
$container.isotope({ filter: comboFilter});
 
 function getComboFilter( filters ) {
   var i = 0;
   var comboFilters = [];
   var message = [];
 
   for ( var prop in filters ) {
     message.push( filters[ prop ].join(' ') );
     var filterGroup = filters[ prop ];
     // skip to next filter group if it doesn't have any values
     if ( !filterGroup.length ) {
       continue;
     }
     if ( i === 0 ) {
       // copy to new array
       comboFilters = filterGroup.slice(0);
     } else {
       var filterSelectors = [];
       // copy to fresh array
       var groupCombo = comboFilters.slice(0); // [ A, B ]
       // merge filter Groups
       for (var k=0, len3 = filterGroup.length; k < len3; k++) {
         for (var j=0, len2 = groupCombo.length; j < len2; j++) {
           filterSelectors.push( groupCombo[j] + filterGroup[k] ); // [ 1, 2 ]
         }
 
       }
       // apply filter selectors to combo filters for next group
       comboFilters = filterSelectors;
     }
     i++;
   }
 
   var comboFilter = comboFilters.join(', ');
   return comboFilter;
 }

Upvotes: 11

DevsLounge
DevsLounge

Reputation: 53

Here is the JS Fiddle code you can follow for Isotope Combination Filter with multiple select dropdown

$(document).ready(function(){
  // init Isotope
  var $grid = $('.grid').isotope({
    itemSelector: '.item'
  });

  // store filter for each group
  var filters = {};

  $('.filters').on('change', '.filters-select', function(){
    var $this = $(this);
    var filterGroup = $this.attr('data-filter-group');
    filters[ filterGroup ] = $this.val();
    var filterValue = concatValues( filters );
    $grid.isotope({ filter: filterValue });
  });

  // flatten object by concatting values
  function concatValues( obj ) {
    var value = '';
    for ( var prop in obj ) {
      value += obj[ prop ];
    }
    return value;
  }
});

https://jsfiddle.net/srikanthLavudia/vhrbqn6p/

Upvotes: 4

Christoph Burschka
Christoph Burschka

Reputation: 4689

Sorry to dredge this question up, but I recently ran into the same problem and (imho) wasted a lot of time solving this with combined selectors. (Combine every class from array A with every class from array B, etc.)

Use a filtering function. It allows arbitrary complexity and is probably the right choice for all non-trivial cases.

Most importantly, there is no performance benefit to using selectors (which you might expect if the selector were fed directly into a querySelector or jQuery call). Isotope iterates through each item and applies the selector as a function regardless:

return function( item ) {
  return matchesSelector( item.element, filter );
};

Therefore, you might as well just put your filtering logic into a function instead of trying to generate a selector string.

Upvotes: 0

Joar
Joar

Reputation: 224

I could make it work but it was not as straightforward as I thought in a firts instance. I think you need to bear on mind a couple of things:

  1. If you want to use a restrictive AND condition, then I suggest don´t use the "data-filter-value" option. Instead, put your filter variables as classes. Then, you will need to fire the isotope function by yourself in the onclick event. This is because otherwise you will fire the "default" isotope function when the user clicks the button and you won´t be able to achieve the restrictive AND mentioned.

  2. In the same direction, you will need to use new variable names (so new classes) for each combination of nested filter options you are going to work with. Otherwise, I don´t see other way (at the moment) to get a restrictive AND condition.

This concept about restrictive and nonrestrictive AND clauses is like the difference between an outer and an inner join. It's the concept you should use if you want to handle two filters, one subfilter of the other one.
One example would be something like a list of ebooks which could be filtered by brand, an other subfilter based on the different range prices they belong to.

I show you below a code example where I implement this idea, it can give you a hint of what i mean:

HTML:

<!--Filters section -->
<div id="filters">
    <ul id="optionSet1" class="option-set" data-option-key="filter">
           <li><a id="filter10" href="#filter" class="selected">Everything</a></li>
        <c:forEach var="brand" items="${brands}" varStatus="status" >
             <li><a id="filter1${status.count}" href="#filter" class='${brand}'>${brand}</a></li>
        </c:forEach>
    </ul>
    <ul id="optionSet2" class="option-set" data-option-key="filter">
        <li><a id="filter20" href="#filter" class="selected">Any Price</a>  </li>
    <c:forEach var="brandPrice" items="${brandPrices}" varStatus="status" >
        <li><a id="filter2${status.count}" href="#filter" class='${brandPrice}'>${brandPrice}</a></li>
    </c:forEach>
    </ul>
</div>  


<!--Elements to filter -->
<div id="portfolio-wrapper" class="row">
    <c:forEach var="publication" items="${publications}" varStatus="status" >               
        <div class='span4 portfolio-item ${publication.filterOption1} ${publication.filterOption2} ${publication.filterOption3}...'>
<!-- filterOption3 would be the combination of filterOption1 and filterOption2, you can make this building a string as the addition of filterOption1 and filterOption2 strings-->
            <!--Content to display-->           
    </c:forEach>
</div>

javascript:

$(function(){

        $("a[id^='filter1']").on('click', function() {
//          alert($(this).attr('class'));
//          alert($('#optionSet2 .selected').attr('class'));
            var myclass = $('#optionSet2 .selected').attr('class');
            myclass = myclass.replace(' selected','');
            myclass = myclass.replace('selected','');
            var myclass2 = $(this).attr('class');
            myclass2 = myclass2.replace(' selected','');
            myclass2 = myclass2.replace('selected','');
            if($(myclass=='' && myclass2==''){
                $('#portfolio-wrapper').isotope({ filter: '*'});
            }else if(myclass==''){
                $('#portfolio-wrapper').isotope({ filter: '.'+ myclass2+'' });
            }else if(myclass2==''){
                $('#portfolio-wrapper').isotope({ filter: '.'+myclass+'' });
            }else{

                $('#portfolio-wrapper').isotope({ filter: '.'+myclass2+myclass+''});
            }   
        });

        $("a[id^='filter2']").on('click', function() {
//          alert($(this).attr('class'));
//          alert($('#optionSet1 .selected').attr('class'));
            var myclass = $('#optionSet1 .selected').attr('class');
            myclass = myclass.replace(' selected','');
            myclass = myclass.replace('selected','');
            var myclass2 = $(this).attr('class');
            myclass2 = myclass2.replace(' selected','');
            myclass2 = myclass2.replace('selected','');
            if(myclass=='' && myclass2==''){
                $('#portfolio-wrapper').isotope({ filter: '*'});
            }else if(myclass==''){
                $('#portfolio-wrapper').isotope({ filter: '.'+ myclass2+'' });
            }else if(myclass2==''){
                $('#portfolio-wrapper').isotope({ filter: '.'+myclass+'' });
            }else{

                $('#portfolio-wrapper').isotope({ filter: '.'+myclass+myclass2+''});
            }
        });

});

Upvotes: 0

Related Questions