How to group together filter inputs to obtain and combine their values

When you want to filter a set of products or other lists, naturally you have to think how you could wire together the variety of filters. With this article, you will learn how you can retrieve the data and combine the values to filter your items in real-time.

Martin Muzatko Written by Martin Muzatko
on

You need to combine inputs to filter for a range of properties, attached to your objects. What looks like an easy task turns out to be messy very soon. How do you combine these filters? How do you extend the functionality for your filters? With each new filter input, not only do you need to define their functionality, but how they are going to play together nicely with the other inputs to finally filter an existing list of items.

In this step-by-step guide you are going to learn:

  • How to use a form element to group inputs and retrieve their data
  • How to use the combined FormData to filter your items
  • How to build a filter and reduce function to compare the data
  • Resources to polyfill FormData

1. Filter types

A form with a couple of filter inputs for products can contain a variety of filter types. Use the list below to find out which values you are going to encounter, when combinig your filters before you are setting a line of code to your editor.

  • A range between two numbers (price, published date)
  • Switches (is the item available)
  • Combinations (multiple vendors)
  • Full-Text (description or item name)

2. Use a form element to obtain data

No matter how many inputs you are going to use to filter your data. Always use a <form> to group together the inputs. The new standard for FormData makes it very easy, to quickly obtain all input values associated with a name.

HTML
<form id="product-filter-form">
    <label for="description">
        Description
        <input type="text" name="description">
    </label>
</form>
JAVASCRIPT
var formElement = document.querySelector('#product-filter-form')
var formData = new FormData(formElement)

There is no need to loop over the inputs and get each one individually. You can always use a form to group together the inputs to get all their values at any time.

Below, you can test what the data looks like, when filling in the inputs.

Note: When we get our data with a FormData object, we can iterate through it. But to display it as object or as array, we need to transform it. In this case, we turn it into an array using the spread operator ([...formdata]).

<script type="riot/tag"> <filter-preview> <form ref="form"> <label for="description"> Description <input value="fill me in" oninput=\{update\} type="text" name="description"> </label> <label for="size"> Size <input oninput=\{update\} type="range" name="size"> </label> </form> <pre>\{JSON.stringify(getJson())\}</pre> getJson() \{ return [...new FormData(this.refs.form)] \} </filter-preview> </script> <filter-preview></filter-preview>

3. Use the combined formdata to filter your items

How do you display your items? You should have your items in a JSON format - ideally an array of objects like shown below. This way, you can also easily loop through them and display them in your HTML with the layout of your choice.

JAVASCRIPT
[
    {
        name: 'awesome GPU 3000',
        vendor: 'Misan`s goods',
        build_year: 2014,
        availability: true
    },
    {
        name: 'splendid GPU 5.3',
        vendor: 'Just stuff',
        build_year: 2015,
        availability: true
    }
    // ... etc
]

Finally, we are going to filter both arrays - the items against the filters. Both of them arrays as shown above.

Now it is time to define the main logic.

Use items.filter((item)=>{return ...}) to filter the list. This is going to be the outer logic. Within that filter function, we need to loop through the inputs to figure out if we should return true or false.

I found that a reduce function is useful to tally the operations. We need to combine the filters - so we are using && to check that all properties of the item match the filter values.

JAVASCRIPT
var formdata = new FormData(this.refs.form)
return this.items.filter(item => {
    return [...formdata].reduce(
        (acc, cur) => 
        {
            return acc && ~(item[cur[0]]+'').toLowerCase().indexOf(cur[1].toLowerCase())
        },
        1
    )
})

Within the reduce function, I get the value of the filter and make it a string (item[cur[0]]+''). Then I look if the filter value is part of that item with indexOf. Lastly, I compare it with the previous value (&&). The initial value for the reducer is 1. This is because if only all results are true, then we allow the filter function to keep that item.

Note: If you wonder what ~ does. It is the invert operator, which inverts the value (from -1 to 0, from 2 to -3 and so on.) This is useful because if the string can be found with indexOf, any positive or negative value is interpreted as true, except for 0 - which is false.

Before we can see all that in action. We need to add eventlisteners to the inputs. The logic for the listeners heavily depends on which view rendering mechanism you are using. I recommend to use a templating engine, so you don't have to manually overwrite the HTML every time. You can toggle the view below to see the code how I implemented the filter.

<script type="riot/tag"> <combined-filter> <form ref="form" action="" method="get"> <label for="name"> name <input oninput=\{submit\} type="text" name="name" /> </label> <label for="vendor"> vendor <select oninput=\{submit\} name="vendor"> <option value="">-- all vendors --</option> <option>Misan`s goods</option> <option>Laughterhouse</option> <option>Just stuff</option> </select> </label> <label for="year"> build year <select oninput=\{submit\} name="build_year"> <option value="">-- all build years --</option> <option>2014</option> <option>2015</option> <option>2016</option> <option>2017</option> </select> </label> <label for="availability"> available <select oninput=\{submit\} name="availability"> <option value="">-- all items --</option> <option value="true">only available</option> </select> </label> </form> Your search matches <b>\{getItems().length\}</b> result\{getItems().length == 1 ? '' : 's'\} <table> <thead> <tr> <th each=\{key in Object.keys(items[0])\}>\{key\}</th> </tr> </thead> <tbody> <tr each=\{item in getItems()\}> <td><span>Name</span>\{item.name\}</td> <td><span>Vendor</span>\{item.vendor\}</td> <td><span>Build year</span>\{item.build_year\}</td> <td style="background-color: \{item.availability ? '#58D288' : '#C43828'\}"><span hide-gt-sm>Available</span>\{''+item.availability\}</td> </tr> </tbody> </table> <style> form \{ width: 50%; margin-bottom: 2em; \} label \{ margin: .5em 0; text-align: left; display: flex; justify-content: space-between; \} @media screen and (max-width: 960px) \{ tbody tr \{ display: flex; flex-direction: column; margin: 1em; \} tbody td \{ border-bottom: none; \} thead \{ display: none; \} td span \{ font-weight: 700; margin-right: .5em; display: inline-block; \} \} @media screen and (min-width: 960px) \{ td span \{ display: none; \} \} table \{ border-collapse: collapse; width: 100%; \} tr \{ border-bottom: 1px solid hsl(0, 0%, 80%); \} td,th \{ padding: .5em; text-align: left; \} thead \{ background-color: hsl(80, 50%, 80%); \} </style> this.items = [ \{ name: 'awesome GPU 3000', vendor: 'Misan`s goods', build_year: 2014, availability: true \}, \{ name: 'splendid GPU 5.3', vendor: 'Just stuff', build_year: 2015, availability: true \}, \{ name: 'cooling fan - Muffin!', vendor: 'Just stuff', build_year: 2014, availability: true \}, \{ name: 'cooling fan - Fridgeboy', vendor: 'Misan`s goods', build_year: 2017, availability: true \}, \{ name: 'PC Case - garage', vendor: 'Laughterhouse', build_year: 2015, availability: false \}, \{ name: 'Mothershipboard Duck', vendor: 'Misan`s goods', build_year: 2016, availability: true \}, \{ name: 'Mediocre GPU - Lasercookie', vendor: 'Just stuff', build_year: 2017, availability: false \}, \{ name: 'Duststorm - Fan', vendor: 'Laughterhouse', build_year: 2017, availability: true \}, \{ name: 'Gaming Ferret 3872 - Computer mouse', vendor: 'Laughterhouse', build_year: 2014, availability: true \}, ] getItems() \{ var formdata = new FormData(this.refs.form) return this.items.filter(item => \{ return [...formdata].reduce( (acc, cur) => \{ return acc && ~(item[cur[0]]+'').toLowerCase().indexOf(cur[1].toLowerCase()) \}, 1 ) \}) \} submit() \{ this.update() \} </combined-filter> </script> <combined-filter></combined-filter>

So what have you learned? You can always use a <form> to encapsulate the filter inputs to get the data with FormData. This saves a lot of time, since you do not have to manually traverse the inputs. Do you support older browsers? Use the formdata-polyfill.

Also, if you want to refresh your knowledge about the array functions I used, you can have a look on the documentation about filter and reduce. MDN also provides time-saving documentation about FormData.

You can extend the reduce function easily, if you use different types of filters, as outlined in point 1.

Next up, I'm writing about how you can use ranges and other types of filters to combine.

Need a little more hand-holding to combine different types of filters? Fill in your email below to get free advice on front-end development straight to your inbox. You will also learn how to further extend the example to deal with any combination of filters.

See other articles

Using noUiSlider as range slider to filter between two numbers


Article

Do you know these fancy range sliders that can filter items with the blink of an eye? You move around a handle and get the desired results right away. In this article, you are going to learn how to implement a range slider to filter your data.

Becoming a zombie


Article

Do you find yourself losing track of time again? Without really knowing, a year passed. As day for day you continue your work, you had some random bursts of motivation, but you know that these kind of motivation spikes won't get you anywhere in the long term.

Updating the riot cheatsheet


Article

It has been roughly a year since I created the riot cheatsheet. With the knowledge acquired by now, it is time to start from scratch. #news 4 #riot 12 #nodejs 2 #webpack 3

About being part of the community


Article

Hello there! A few months ago, I decided that I have to be more active in the community of web developers. Since then, I wanted to give talks about my favorite tools.

See all Articles