Using noUiSlider as range slider to filter between two numbers

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.

Martin Muzatko Written by Martin Muzatko
on

It feels so effortless to use range sliders. They help to have a custom overview of what you can actually buy from your money, or you want to have only the recently added items displayed. But implementing this functionality requires you to know how to wire together the logic with your data. And then you still have to push the results to the DOM somehow, while taking care of performance.

You do not have to get lost in the event handlers and DOM manipulation functions of your dynamic web-app. You can learn how to build range sliders into your projects today.

By completing this tutorial, you will learn

  • How to configure noUiSlider to work as live filter for your items
  • How to filter your data based on a range between two numbers
  • How to integrate the slider with your existing codebase
  • Bonus: How to calculate min and max values for your slider based on your data

To give you an idea how the range slider itself will work, move around the handles in the example below.

<script src="https://unpkg.com/nouislider@10.0.0/distribute/nouislider.min.js"></script> <script src="https://unpkg.com/wnumb@1.1.0"></script> <link rel="stylesheet" href="https://unpkg.com/nouislider@10.0.0/distribute/nouislider.min.css"> <div style="margin: 2em" id="slider"></div> <script> var slider = document.querySelector('#slider') var dollarPrefixFormat = wNumb(\{prefix: '$', decimals: 0\}) noUiSlider.create(slider, \{ start: [20, 60], connect: true, margin: 5, tooltips: [dollarPrefixFormat, dollarPrefixFormat], pips: \{ mode: 'steps', density: 5, format: dollarPrefixFormat \}, range: \{min: 0, max: 100\} \}) </script>

Get noUiSlider and set it up

Grab a copy of noUiSlider if you haven't already. Use your favorite package manager or include the sources as script and link element.
If you haven't checked their documentation already, you will see that a slider is set up in almost no time. Don't get distracted by the many possibilities with this tool, we will focus on the price range filter for now.

BASH
yarn add nouislider wnumb

npm i -S nouislider wnumb
HTML
<script src="https://unpkg.com/nouislider@10.0.0/distribute/nouislider.min.js"></script>
<script src="https://unpkg.com/wnumb@1.1.0"></script>
<link rel="stylesheet" href="https://unpkg.com/nouislider@10.0.0/distribute/nouislider.min.css">

Create an element (it can be a <div> with a class or an ID) all configuration is done using javascript. The value for start can be used to place multiple handles to create a range slider. For now, we set them to fixed values, so we can see the slider in action. With this, you will get a range slider that looks like the one in the top of the article.

If you load the resources before the following script, you don't need a document ready event.

HTML
<div style="margin: 2em" class="regular-slider"></div>
<script>
var regularSlider = document.querySelector('.regular-slider')
// wNumb is their tool to format the number. We us it to format the numbers that appear in the handles
var dollarPrefixFormat = wNumb({prefix: '$', decimals: 0})
var slider = noUiSlider.create(regularSlider, {
    // two handles
    start: [20, 60],
    // they are connected
    connect: true,
    // their minimal difference is 5 - this makes sense, because we want the user to always find items
    margin: 5,
    // tooltip for handle 1 and handle 2
    tooltips: [dollarPrefixFormat, dollarPrefixFormat],
    pips: {
        mode: 'steps',
        density: 5,
        format: dollarPrefixFormat
    },
    // start and end point of the slider - we are going to calculate that later based on a set of items
    range: {min: 0, max: 100}
})
</script>
<div style="margin: 2em" class="regular-slider"></div> <script> var regularSlider = document.querySelector('.regular-slider') // wNumb is their tool to format the number. We us it to format the numbers that appear in the handles var dollarPrefixFormat = wNumb(\{prefix: '$', decimals: 0\}) var slider = noUiSlider.create(regularSlider, \{ // two handles start: [20, 60], // they are connected connect: true, // their minimal difference is 5 - this makes sense, because we want the user to always find items margin: 5, // tooltip for handle 1 and handle 2 tooltips: [dollarPrefixFormat, dollarPrefixFormat], pips: \{ mode: 'steps', density: 5, format: dollarPrefixFormat \}, // start and end point of the slider - we are going to calculate that later based on a set of items range: \{min: 0, max: 100\} \}) </script>

How does the data look you will have to filter? Ideally, you will have an array of objects. With each object containing properties to describe how the item is named, how much it is and other chracteristics.

Note: This is what comes from the server, so you only have to care about how to process and display the items. I understand that this is not always true, because in most applications and hobby projects, you'll have to take care abouth both. So feel free to shoot me an email to talk about how you can implement this tutorial code with yours.

Get in touch, it's free

Depending on your backend architecture, you should fetch the data using AJAX and then start processing and filtering the data. Below you can find how this data could look like.

JAVASCRIPT
var items = [
    {
        name: 'Mediocre GPU - Lasercookie',
        vendor: 'Just stuff',
        build_year: 2017,
        price: 79,
        availability: false
    },
    {
        name: 'Gaming Ferret 3872 - Computer mouse',
        vendor: 'Laughterhouse',
        build_year: 2014,
        price: 44.5,
        availability: true
    },
    // ...
]

So how do you filter these items? This is as simple as calling the Array.filter function together with what we already know. Our range that consists out of two values: start of range and end of range. We will call that function when we obtain the values from the range slider, which is explained after this code.

JAVASCRIPT
function filterItems(items, price) {
    return items.filter(item => {
        return item.price >= price[0] && item.price <= price[1]
    })
}

So now that you got the filter function and the slider set up, you need to interact with the values.

You can use the element we already set up. The variable you set when you initialize the slider has all the functions you need to listen for updates. Actually, you can listen to update, slide, set, change, start or end events. Change gets triggered only after you dropped the handle again. But you want to have changes happen immediately - you want to have a live search. Which is why you can use the update event.You can find out when which event fires, by having a look at the official documentation for events.

JAVASCRIPT
slider.on('update', function(values){
    let filteredItems = filterItems(items, values)
    renderItems(filteredItems)
})

There is a function we haven't covered so far: renderItems(). Before we get to that, lets have a look on the potential HTML structure where we render the items to.

HTML
<h2>Price</h2>
<div class="slider"></div>
<span class="counter">Your search matches <b>0</b> results</span>
<table>
    <thead>
        <tr>
            <th>name</th>
            <th>vendor</th>
            <th>build year</th>
            <th>price</th>
            <th>availability</th>
        </tr>
    </thead>
    <tbody class="results">
    </tbody>
</table>

To pull everything together, this is what the range slider together with the results has to perform:

  • Step 1: Show all items
  • Step 2: Detect slider update
  • Step 3: Filter based on range and a property of the items (price)
  • Step 4: Render new items

We covered 2. and 3. so far.

You should work with a template you can re-use instead of manually manipulating the DOM everytime. Because interaction is a cycle, the user can start from step 1 again. If you are coming from jQuery, this is a fundamental shift in the mindset how to create dynamic content. I'm showing you how.

JAVASCRIPT
function renderItems(items) {
    var counter = document.querySelector('.counter')
    counter.innerHTML = `Your search matches <b>${items.length}</b> result${items.length == 1 ? '' : 's'}`
    var table = document.querySelector('.results')
        table.innerHTML = items.map(item=>{return `
        <tr>
            <td><span>Name</span>${item.name}</td>
            <td><span>Vendor</span>${item.vendor}</td>
            <td><span>Build year</span>${item.build_year}</td>
            <td><span>Price</span>${priceFormat.to(item.price)}</td>
            <td style="background-color: ${item.availability ? '#58D288' : '#C43828'}"><span hide-gt-sm>Available</span>${''+item.availability}</td>
        </tr>
        `
    }).join('')
}

With this, we have everything we need to dynamically show the filtered content. Live! No forms to submit.

Now take a deep breath, use the interactive slider with the table below and switch to "See code".

<h2>Price</h2> <div class="slider"></div> <span class="counter">Your search matches <b>0</b> results</span> <table> <thead> <tr> <th>name</th> <th>vendor</th> <th>build year</th> <th>price</th> <th>availability</th> </tr> </thead> <tbody class="results"> </tbody> </table> <script> var priceFormat = wNumb(\{prefix: '$', decimals: 2\}) var regularSlider = document.querySelector('.slider') var price = [] // define items or get them via AJAX and then call the function renderItems() var items = [ \{ name: 'awesome GPU 3000', vendor: 'Misan\'s goods', build_year: 2014, price: 120.59, availability: true \}, \{ name: 'splendid GPU 5.3', vendor: 'Just stuff', build_year: 2015, price: 199.59, availability: true \}, \{ name: 'cooling fan - Muffin!', vendor: 'Just stuff', build_year: 2014, price: 32.29, availability: true \}, \{ name: 'cooling fan - Fridgeboy', vendor: 'Misan\'s goods', build_year: 2017, price: 41, availability: true \}, \{ name: 'PC Case - garage', vendor: 'Laughterhouse', build_year: 2015, price: 129, availability: false \}, \{ name: 'Mothershipboard Duck', vendor: 'Misan\'s goods', build_year: 2016, price: 87.50, availability: true \}, \{ name: 'Mediocre GPU - Lasercookie', vendor: 'Just stuff', build_year: 2017, price: 79, availability: false \}, \{ name: 'Duststorm - Fan', vendor: 'Laughterhouse', build_year: 2017, price: 8.55, availability: true \}, \{ name: 'Gaming Ferret 3872 - Computer mouse', vendor: 'Laughterhouse', build_year: 2014, price: 44.5, availability: true \}, ] // Initial render with the items // The function renderItems is defined later (which is no problem in javascript) renderItems(items) // slider setup var slider = noUiSlider.create(regularSlider, \{ start: [-Infinity, Infinity], // always start and end of the range connect: true, tooltips: [wNumb(\{prefix: '$', decimals: 0\}), wNumb(\{prefix: '$', decimals: 0\})], pips: \{ mode: 'steps', density: 5 \}, range: this.getPriceRange(this.items) \}) // on slider update, call filterItems and render them slider.on('update', function(values)\{ let filteredItems = filterItems(items, values) renderItems(filteredItems) \}) function getPriceRange(items) \{ let min = items.reduce(function(acc, value)\{ return acc < value.price ? acc : value.price \}) let max = items.reduce(function(acc, value)\{ return acc > value.price ? acc : value.price \}) return \{min: min, max: max\} \} function filterItems(items, price) \{ return items.filter(item => \{ return item.price >= price[0] && item.price <= price[1] \}) \} function renderItems(items) \{ var counter = document.querySelector('.counter') counter.innerHTML = `Your search matches <b>$\{items.length\}</b> result$\{items.length == 1 ? '' : 's'\}` var table = document.querySelector('.results') table.innerHTML = items.map(item=>\{return ` <tr> <td><span>Name</span>$\{item.name\}</td> <td><span>Vendor</span>$\{item.vendor\}</td> <td><span>Build year</span>$\{item.build_year\}</td> <td><span>Price</span>$\{priceFormat.to(item.price)\}</td> <td style="background-color: $\{item.availability ? '#58D288' : '#C43828'\}"><span hide-gt-sm>Available</span>$\{''+item.availability\}</td> </tr> ` \}).join('') // we need join(''), because we do an array to string conversion here. // otherwise, the items would be connected with "," \} </script>
<style> .slider \{ margin: 3em 1em; \} @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 \{ table-layout: fixed; 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%); \} .noUi-value-large \{ margin-top: .5em; \} .noUi-handle:focus \{ outline: none; \} </style>

In the code above, you can also see the bonus I talked about in the beginning. With the getPriceRange function, you can determine the mininmum and maximum price of your array.

There are so many other aspects to make this component useful in real world applications. A lot of options can be fine-tuned to make them work for any situation and set of items. You can combine this example with so many other features to offer an effortless user experience. Sortable columns, filters for every column, full text search. Get on my email list to learn how to combine all these and a lot more with your forms to avoid unnecessary interactions that slow down your users.

See other articles

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.

Becoming a zombie

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

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

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