JAVASCRIPT

Multi-Filter

This code sample shows how to create a multiple list filter using vanilla JavaScript (no jQuery). Filtering is done by counting matches of filter selections against matches of the same for each of the element data attributes. Match counts are displayed in each filter label and in the app heading. demo.

Markup

<div class="filters">
    <label>
        <input type="checkbox" name="make" value="chevy">Chevy
    </label>
    <label>
        <input type="checkbox" name="make" value="dodge">Dodge
    </label>
    <label>
        <input type="checkbox" name="model" value="camaro">Camaro
    </label>
    <label>
        <input type="checkbox" name="model" value="dart">Dart
    </label>
    <label>
        <input type="checkbox" name="price" value="40000-60000">40-60k
    </label>
    <label>
        <input type="checkbox" name="price" value="20000-40000">20-40k
    </label>
    <label>
        <input type="checkbox" name="price" value="0-20000">20k max
    </label>
    <label>
        <input type="checkbox" name="year" value="1969">1969
    </label>
    <label>
        <input type="checkbox" name="year" value="1970">1970
    </label>
</div>

<ul class="cars">
    <li data-year="1969" data-make="chevy" data-model="chevelle" data-price="55000">
        1969 Chevy Chevelle<br />
        $55,000
    </li>
    <li data-year="1969" data-make="dodge" data-model="dart" data-price="19900">
        1969 Dodge Dart<br />
        $19,900
    </li>
    <li data-year="1969" data-make="dodge" data-model="charger" data-price="49995">
        1969 Dodge Charger<br />
        $49,995
    </li>
    <li data-year="1970" data-make="dodge" data-model="dart" data-price="18900">
        1970 Dodge Dart<br />
        $18,900
    </li>
    <li data-year="1969" data-make="chevy" data-model="camaro" data-price="60500">
        1969 Chevy Camaro<br />
        $60,500
    </li>
    <li data-year="1970" data-make="chevy" data-model="camaro" data-price="39900">
        1970 Chevy Camaro<br />
        $39,900
    </li>
    ...
</ul>

Style

.cars-heading {
    text-align: center;
}

.filters {
    display: flex;
    flex-wrap: wrap;

    label {
        border: 1px solid #ccc;
        padding: 10px;
        margin: 0 10px 10px;
        display: block;
        white-space: nowrap;

        &:hover {
            background: #eee;
            cursor: pointer;
        }
    }
}

.cars {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    background-color: #f2f2f2;
    padding: 20px 0 0 20px;

    li {
        width: 175px;
        display: block;
        padding: 20px;
        background-color: white;
        margin-right: 20px;
        margin-bottom: 20px;
    }
}

.cars.filtered {
    li:not(.selected) {
        display: none;
    }
}

JavaScript

function inRange(num, range) {
    return (num >= range.split('-')[0] && num <= range.split('-')[1]);
}

function matches(key, value) {
    var count = 0;
    Array.from(el.items).forEach(item => {
        switch(key) {
            case 'make':
                if (item.dataset.make === value) {
                    count ++;
                }
                break;
            case 'model':
                if (item.dataset.model === value) {
                    count ++;
                }
                break;
            case 'price':
                if (inRange(item.dataset.price, value)) {
                    count ++;
                }
                break;
            case 'year':
                if (item.dataset.year === value) {
                    count ++;
                }
                break;
        }
    });
    return count;
}

function match(item) {
    var match = {
        "make": [],
        "model": [],
        "price": [],
        "year": []
    };
    Array.from(el.filtersList).forEach(input => {
        if (input.checked) {
        switch(input.name) {
            case 'make':
                match.make.push(item.dataset.make === input.value);
                break;
            case 'model':
                match.model.push(item.dataset.model === input.value);
                break;
            case 'price':
                match.price.push(inRange(item.dataset.price, input.value));
                break;
            case 'year':
                match.year.push(item.dataset.year === input.value);
                break;
        }}
    });
    return match;
}

function renderCount(count) {
    el.heading.innerHTML = `${count} Matches`;
}

function applyFilter() {
    Array.from(el.items).forEach(item => {
        var result = match(item),
            matches = [];
        item.classList.remove('selected');

        // console.log(result);
        if (result.make.length) {
            if (result.make.includes(true)) {
                matches.push(true);
            } else { matches.push(false); }
        }

        if (result.model.length) {
            if (result.model.includes(true)) {
                matches.push(true);
            } else { matches.push(false); }
        }

        if (result.price.length) {
            if (result.price.includes(true)) {
                matches.push(true);
            } else { matches.push(false); }
        }

        if (result.year.length) {
            if (result.year.includes(true)) {
                matches.push(true);
            } else { matches.push(false); }
        }

        var count = 0;
        for(var i = 0; i < matches.length; ++i){
            if(matches[i] == true)
                count++;
        }

        if (matches.length && matches.length == count) {
            item.classList.add('selected');
        } else {
            item.classList.remove('selected');
        }
    });

    renderCount(el.list.querySelectorAll('.selected').length);
}

function isFilter() {
    var filter = false;
    /**
     * some returns true as soon as any of the callbacks return true,
     * short-circuiting the execution of the rest. e.g., break;
     */
    Array.from(el.filtersList).some(input => {
        if (input.checked) {
            filter = true;
        }
    });
    return filter;
}

function onFilterChange(input) {
    var filtered = false;
    if (input.checked) {
        filtered = true;
    } else {
        filtered = isFilter();
    }

    if (filtered) {
        el.list.classList.add('filtered');
        applyFilter();

    } else {
        el.list.classList.remove('filtered');
        renderCount(el.items.length);
    }
}

/**
 * This is the app entry point
 */
function onDocumentReady() {
    el.heading = document.querySelector('.cars-heading');
    el.filters = document.querySelector('.filters');
    el.filtersList = el.filters.querySelectorAll('input');
    el.list = document.querySelector('ul.cars');
    el.items = el.list.querySelectorAll('li');

    renderCount(el.items.length);

    Array.from(el.filtersList).forEach(input => {
        // add match count to the label
        input.parentNode.append(` (${matches(input.name, input.value)})`);

        input.addEventListener('change', (event) => {
            onFilterChange(event.target);
        });
    });

    ...
}

...

This function example shows how to call onDocumentReady when the DOMContentLoaded Event has fired.


Source Code
comments powered by Disqus