0

So, I have 3 filter boxes. The first is named room, second is named color and the third is named staff.

For every filter box there are multiple values which I can choose, for example for color I can choose the value "blue" or for room "presidential". I pass the name of the filterbox and the chosen value in a function changeBox (for example name: color, value: blue).

With that information I want to filter an array. Of course I want to have multiple filters active, which only show the wanted results which are compatible to my activated filters.

So what I did was using a switch case for every possibility which looked like this:

changeBox(name, value) {
    if (name === "color") {
        switch (value) {
            case "blue":
                myArray = myArray.filter(item => item.color === "blue");
                break;
        }
    }
    if (name === "room") {
        switch (value) {
            case "presidential":
                myArray = myArray.filter(item => item.room === "presidential");
                break;
        }
    }
}

This works If I have only one filter active. The problem is now, if I activate a second filter it works too. But if I switch the second filter to another value, it doesn't work. I know it's because of the myArray.filter. It deletes every value in the array which doesn't fit to my filter. How can I keep the data without actually overwrite it every time?

SteeveDroz
  • 6,006
  • 6
  • 33
  • 65
swigswag
  • 23
  • 4
  • 1
    You will need to start your filtering on the original array, not on what has already been filtered previously. – CBroe Jun 23 '23 at 11:55
  • Does this answer your question? [Filter array of objects by multiple properties and values](https://stackoverflow.com/questions/44330952/filter-array-of-objects-by-multiple-properties-and-values) – Heretic Monkey Jun 23 '23 at 11:59
  • `filter` is a method that creates a new array for you without mutating the original so don't overwrite it like that, create a new array with a different name `myNewArray = myArray.filter(item => item.color === "blue");` – Chris G Jun 23 '23 at 12:00
  • 1
    By the way, your use of the switch statements looks pretty pointless there. Why are you not just doing `myArray = myArray.filter(item => item.color === value);` instead? – CBroe Jun 23 '23 at 12:01
  • 1
    And you could do away with the ifs as well, using [bracket notation for dynamic property access](https://stackoverflow.com/a/4244912/1427878) - `myArray = myArray.filter(item => item[name] === value);` (this of course requires that you only call `changeBox` with names of actually existing properties) – CBroe Jun 23 '23 at 12:03
  • I know that .filter creates a new Array. But if I have for every filter box an own array, how can I combine them at the end? So that only results are shown which are equivalent to my filter? – swigswag Jun 23 '23 at 12:18
  • _"But if I have for every filter box an own array"_ - why would you have that? It is all one and the same data set that you want to filter here, no? – CBroe Jun 23 '23 at 12:22
  • *It deletes every value in the array which doesn't fit to my filter. How can I keep the data without actually overwrite it every time?* Copy your original data into a filtered variable. Only show this filtered variable but always keep your data in the original :) – Wimanicesir Jun 23 '23 at 12:28
  • Okay lets say for the color: ```myNewArrayColor = myArray.filter(item => item.color === "blue"); ``` For the room: ```myNewArrayRoom = myArray.filter(item => item.room === "presidential");``` and for the staff: ```myNewArrayStaff = myArray.filter(item => item.staff === "security");``` So now I have 3 arrays for each filter. How can I show all results which are represented in all 3 arrays? – swigswag Jun 23 '23 at 13:25

3 Answers3

2

Here is the complete flow of filtering.

  1. Collect the state of the filter by grabbing the values of the <select> elements
  2. Filter the data based on those filters
  3. Render the filtered data

Note: I added "Green" option to show "No results". You can modify the function to pass this as a custom message.

const
  filterEl = document.querySelector('.filter'),
  resultsEl = document.querySelector('.results');

const allData = [
  { color: 'red'  ,  room: 'presidential' },
  { color: 'red'  ,  room: 'unknown'      },
  { color: 'blue' ,  room: 'presidential' },
  { color: 'blue' ,  room: 'unknown'      }
];

const rendererFn = (item, targetEl) => {
  targetEl.append(Object.assign(document.createElement('li'), {
    textContent: JSON.stringify(item)
  }));
};

const filterData = (data, filter) => {
  const entries = Object.entries(filter);
  return data.filter(item =>
      entries.length === 0 ||
      entries.every(([name, value]) => item[name] === value));
};

const collectFilters = (selectElArr) =>
  selectElArr.reduce((filter, { name, value }) => {
    if (value) {
      filter[name] = value;
    }
    return filter;
  }, {});

const renderData = (data, callbackFn, targetEl) => {
  targetEl.innerHTML = '';
  if (data.length === 0) {
    targetEl.append(Object.assign(document.createElement('p'), {
      textContent: 'No results'
    }));
  }
  for (let item of data) {
    callbackFn(item, targetEl);
  }
};

const onFilterChange = () => {
  const filter = collectFilters([...filterEl.querySelectorAll('select')]);
  const filteredData = filterData(allData, filter);
  renderData(filteredData, rendererFn, resultsEl);
}

filterEl.addEventListener('change', onFilterChange);
onFilterChange(); // Cause render of all items
.results li { font-family: monospace; }
<div class="filter">
  <select name="color">
    <option value="">-</option>
    <option value="red">Red</option>
    <option value="blue">Blue</option>
    <option value="green">Green</option>
  </select>
  <select name="room">
    <option value="">-</option>
    <option value="presidential">Presidential</option>
    <option value="unknown">Unknown</option>
  </select>
</div>
<hr />
<h2>Results</h2>
<ul class="results"></ul>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
0

You should have all of your filter values at once and filter with them:

const myArray = [{color: 'blue', room: 'presidential'}, {color: 'red', room: 'unknown'}];

const $filter = document.querySelector('.filter');
$filter.addEventListener('change', changeBox);

function changeBox() {
  const filter = [...$filter.querySelectorAll('select')].reduce((filter, $elem) => {
    if($elem.value){
      filter[$elem.name] = $elem.value;
    }
    
    return filter;
        
  }, {});
  
  console.log('filter:', JSON.stringify(filter));
  
  const filtered = myArray.filter(item => Object.entries(filter).every(([name, value]) => item[name] === value));
  console.log('filtered:', JSON.stringify(filtered));
  
  // use your filtered array and keep myArray to filter next time
}
<div class="filter">
<select name="color">
<option value="">-</option>
<option value="blue">Blue</option>
<option value="red">Red</option>
</select>
<select name="room">
<option value="">-</option>
<option value="presidential">Presidentail</option>
<option value="unknown">Unknown</option>
</select>
</div>
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
0

If you consider to decouple actual filtering logic from UI, you can design some sort of generic multiple filter that can be arbitrarily customized and used on any data.

For example, you can implement it as a function that accepts:

  • collection of filters
  • collection of options with which those filters should be used to filter the array
  • boolean option or flag to specify whether the item should satisfy every filter (logical AND), or just one is enough (logical OR)

function filterMultiple (array, filters, settings, some) {
    filters = Object

        // convert settings into [filterName, options] pairs
        .entries(settings)

        // select those for which filters[filterName] exists
        .filter(([filterName]) => typeof filters[filterName] == 'function')

        // convert selected pairs into shortcut filter functions
        // with the options implicitly applied to them
        .map(([filterName, options]) => {
            let fn = filters[filterName];
            return item => fn(item, options);
        });

    // filter input array by testing each item
    // against some/every shortcut filter
    return array.filter(
        some
        ? item => filters.some(fn => fn(item))
        : item => filters.every(fn => fn(item))
    );
}

const array = [
    { id: 0,  room: 'presidential', color: 'white'   },
    { id: 1,  room: 'presidential', color: 'black'   },
    { id: 2,  room: 'presidential', color: 'black'   },
    { id: 3,  room: 'presidential', color: 'black'   },
    { id: 4,  room: 'honeymoon',    color: 'red'     },
    { id: 5,  room: 'honeymoon',    color: 'yellow'  },
    { id: 6,  room: 'honeymoon',    color: 'green'   },
    { id: 7,  room: 'honeymoon',    color: 'blue'    },
    { id: 8,  room: 'honeymoon',    color: 'purple'  },
    { id: 9,  room: 'honeymoon',    color: 'magenta' },
    { id: 10, room: 'penthouse',    color: 'red'     },
    { id: 11, room: 'penthouse',    color: 'yellow'  },
    { id: 12, room: 'penthouse',    color: 'green'   },
    { id: 13, room: 'penthouse',    color: 'blue'    },
    { id: 14, room: 'penthouse',    color: 'white'   },
    { id: 15, room: 'penthouse',    color: 'black'   }
];

const filters = {
    roomEqual:  (item, value) => item.room === value,
    roomSome:   (item, values) => values.some(value => item.room === value),
    colorEqual: (item, value) => item.color === value,
    colorSome:  (item, values) => values.some(value => item.color === value)
};

console.log(filterMultiple(array, filters, {
    colorEqual: 'white'
}));

console.log(filterMultiple(array, filters, {
    colorSome: ['red', 'yellow']
}));

console.log(filterMultiple(array, filters, {
    roomEqual: 'presidential',
    colorSome: ['purple', 'magenta', 'green', 'black']
}));

console.log(filterMultiple(array, filters, {
    roomEqual: 'presidential',
    colorSome: ['purple', 'magenta', 'green', 'black']
}, true));
.as-console-wrapper { max-height: 100% !important; }
blakkwater
  • 1,133
  • 7
  • 13