1

I have a list of cars for sale on my site, and five filters:

  1. Make
  2. Model
  3. Year
  4. Mileage
  5. Price

...each with multiple options that users can filter by.

When users check one or more options in the filter list, I need to hide the cars that don't match all of the selected filters.

Here's the HTML for one of the cars:

<div class="home-car-div">
    <div class="home-car-description-div">
        <p class="home-car-info" data-year="2018" data-make="Ford" data-model="Escape" data-mileage="10000" data-price="18900">
            2018 Ford Escape
        </p>
    </div>
</div>

And here is one of the filters:

<div class="car-filters">
    <div class="make-section">
        <p class="filter-title">Make</p>
        <div id="makeSection" data-group="make">
            <label class="filter-input">
                <input type="checkbox" name="make" class="make" value=“Ford”>Ford
            </label>
            <label class="filter-input">
                <input type="checkbox" name="make" class="make" value="Subaru"> Subaru
            </label>
        </div>
    </div>
</div>

I have followed some other questions on the site, and I seem to have most of this working. Whenever the user checks one of the filter checkboxes, I get all the values for each attribute in arrays of key/value pairs:

var $filterCheckboxes = $( '.filter-input input' );

$filterCheckboxes.on( 'change', function() {
        
    $('.home-car-div').show();
        
    var selectedFilters = {};
            
    $filterCheckboxes.filter( ':checked' ).each( function() {
        
        if ( ! selectedFilters.hasOwnProperty( this.name ) ) {
            selectedFilters[ this.name ] = [];
        }
            
        selectedFilters[ this.name ].push( this.value );
        
    });
          
    $('.home-car-info').each(function() {
        var carMake = $(this).data('make');
        var carModel = $(this).data('model');
        var carYear = $(this).data('year');
        var carPrice = $(this).data('price');
        var carMileage = $(this).data('mileage');
                    
        var filteredMake = selectedFilters.make;
        var filteredModel = selectedFilters.model;
        var filteredYear = selectedFilters.year;
        var filteredPrice = selectedFilters.price;
        var filteredMileage = selectedFilters.mileage;
                
    });
        
});

So now I have variables for each of the cars' attributes, and variables of each of the filtered selections, but I now need to compare each of these, and somehow hide cars that aren't matching ALL of the attributes.

I'd also like to show a message if there are no results.

Am I on the right track, and how do I do the filtering?

lamb321001
  • 47
  • 5
  • Assuming that `filterCheckboxes` holds a collection of ``, the `value` of each is going to be constant. This would lead to `selectedFilters[x]` always either being empty, or holding a single element. You could achieve the same thing by doing something akin to this: `selectedFilters = $('.filter-input input:checked').map( function() { return this.value })`. This changes somewhat if you actually have multiple checkboxes for each "filter". Would probably help to share that part of your UI. – Tibrogargan Jul 15 '21 at 01:32
  • Thanks. I do have multiple checkboxes for each filter. I've added an example of one of the filters (Make) to the original post. – lamb321001 Jul 15 '21 at 01:43
  • If there were only one type of filter (e.g. Make), I can do the filtering using if statements to check if (filteredMake.includes(carMake)), but I fall apart when trying to check for each filter type, and also to account for situations where no option is checked. – lamb321001 Jul 15 '21 at 01:48
  • Yeah, you could do this: `selectedFilters = $('.filter input:checked').get().reduce( function(a, c) { a[c.name] = (a[c.name] || []); a[c.name].push(c.value); return a } )` – Tibrogargan Jul 15 '21 at 02:00
  • Thanks. Where in the script does this go? Does it replace selectedFilters[ this.name ].push( this.value ); – lamb321001 Jul 15 '21 at 02:11
  • Note: You're using an editor that's replacing your quotes (`"`) with Unicode variants. Not good for coding with. `“Ford”` != `"Ford"` – Tibrogargan Jul 15 '21 at 03:06

1 Answers1

1

I believe that this is close to what you were after. I used functional programing to perform actions on categories of data instead of individual elements. Doesn't really adhere to this requirement: "hide cars that aren't matching ALL of the attributes", because that requirement doesn't make sense for mutually exclusive properties such as "make" and "model". Also removed some extraneous classes and used the "for" attribute on labels (see this).

The implementation depends the assumption that a car matches a category if nothing in that category is selected.

Note: There is an issue with JQuery that causes it's .data() method to be functionally different to the DOM's .dataset property, hence the use of this.dataset[category] instead of $(this).data(category). Technically this should also be better performance and is arguably more readable as well.

$(function() {
  $('#button').click(function() {
    // Create an map of category name to selected filters
    selectedFilters = $('.car-filters input:checked').get().reduce( function(a, c) { a[c.name] = (a[c.name] || []); a[c.name].push(c.value); return a }, {} )
  
    // filter the list of cars displayed
    match = 0
    unmatchedCars = $('.home-car-info').filter(function() {
      for (const category in selectedFilters) {
        // must match at least one in each category
        if (!selectedFilters[category].includes(this.dataset[category])) {
           console.log(`"${$(this).text().trim()}" does not match ${category} is in [${selectedFilters[category]}] (${category} is ${this.dataset[category]})`)
           return true
        }
        match++
      }
    });
    // do something with unmatchedCars
    unmatchedCars.hide()
    if (!match) {
      console.log("Nothing matches the selected filters")
    }        
  })
  $('#restore').click(function() {
    $('.home-car-info').show()
    $('input:checkbox').prop('checked', false)
  })
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="home-car-div">
  <div class="home-car-description-div">
    <p class="home-car-info" data-year="2018" data-make="Ford" data-model="Escape" data-mileage="10000" data-price="18900">
      2018 Ford Escape
    </p>
    <p class="home-car-info" data-year="1998" data-make="Porsche" data-model="911" data-mileage="10000" data-price="72500">
      1998 Porsche 911
    </p>
  </div>
</div>
<form>
  <div class="car-filters">
    <div class="make-section">
      <p >Make</p>
      <div id="makeSection" data-group="make">
        <input type="checkbox" id="iaa" name="make" value="Ford"><label for="iaa">Ford</label>
        <input type="checkbox" id="iab" name="make" value="Subaru"><label for="iab">Subaru</label>
        <input type="checkbox" id="iab" name="make" value="Porsche"><label for="iac">Porsche</label>
      </div>
      <p >Model</p>
      <div id="makeSection" data-group="model">
        <input type="checkbox" id="iba" name="model" value="T"><label for="iba">T</label>
        <input type="checkbox" id="ibb" name="model" value="Escape"><label for="ibb">Escape</label>
        <input type="checkbox" id="ibb" name="model" value="911"><label for="ibc">911</label>
      </div>
    </div>
  </div>
  <div>
    <button id="button" type="button">Filter</button>
    <button id="restore" type="button">Restore</button>
  </div>
</form><br />
Tibrogargan
  • 4,508
  • 3
  • 19
  • 38
  • Thank you! This looks just about perfect. There are two issues I'm having. 1) if you filter, then change your filters and try to filter again without ever clicking "Restore," then it doesn't seem to work. Also, with regards to unmatchedCars.hide(), I'm trying to hide a parent of .home-car-info, so could you show me how to hide the parent of a variable? – lamb321001 Jul 15 '21 at 03:37
  • In other words, previously I was doing **$('.home-car-info').parents('.home-car-div').hide();** - but I don't know how to do this the way you coded it. Thanks – lamb321001 Jul 15 '21 at 03:38
  • `.parent()`, not `.parents()` – Tibrogargan Jul 15 '21 at 03:39
  • How do I convert **unmatchedCars.hide()** to hide the parent div two levels up? – lamb321001 Jul 15 '21 at 03:45
  • `.parent().parent()` works. put a class or id on it you can deduce (i.e. "make-parent"). probably any number of ways. Oh, see [this](https://stackoverflow.com/a/19957433/2487517) for how to show() cars that might have been hidden by a previous filter. You could also do that when `match` is incremented, but side-effects are bad practice – Tibrogargan Jul 15 '21 at 03:51
  • Sorry, I think I'm not following you. I'm trying to change unmatchedCars.hide() so it hides the parent two levels up, not just the paragraph tag. Changing **unmatchedCars.hide()** to **unmatchedCars.hide().parent().parent()** doesn't seem to work. – lamb321001 Jul 15 '21 at 03:54
  • `unmatchedCars.parent().parent().hide()` Might be cleaner to do the filter from that level instead, but might be complicated – Tibrogargan Jul 15 '21 at 03:56
  • This all works. Thank you. I found that it works slightly better if I show all cars at the beginning of the button click function, but otherwise it's perfect. Thanks! – lamb321001 Jul 15 '21 at 12:51
  • 1
    Yeah, problem w/ showing them at the beginning is you get stuff appearing and disappearing during the search. Technically could produce a sub-par user experience, but practically this will probably not be an issue because things will probably happen so quickly no-one will notice. – Tibrogargan Jul 15 '21 at 20:15
  • I found another issue, which is that the method doesn't work with numbers, even if the number is the name of a car (e.g. "911"). It also doesn't work with ranges, like for mileage. I finally made a JSFiddle to make this clearer. Any chance you could help me figure this out? https://jsfiddle.net/Iamb321/Lwsvbd84/8/ – lamb321001 Jul 16 '21 at 01:08
  • Ok, that's very weird. Turns out that JQuery's `.data()` function is doing something to the values to make them not match. If you do this `selectedFilters[category].includes(this.dataset[category])` it works, but the JQuery "equivalent" (`selectedFilters[category].includes($(this).data(category))` does not. – Tibrogargan Jul 16 '21 at 20:59
  • I did not look at the range thing, I'm assuming you need to search the array for a value that is between two limits. That's going to be a `.filter()` instead of a simple `.includes()` and really should be a whole separate question. – Tibrogargan Jul 16 '21 at 21:01