JavaScript Facet Filtering

I’m currently working on adding faceted search to Vegan Realm. I struggled a bit while trying to find an elegant solution in plain JavaScript. I figured that sharing my concept might come in handy for someone trying to achieve a similar result. The complete code of the demo is available here.

RequireJS and its domReady module were used to organize code. No other JavaScript libraries were used. Everything could be refactored into a single file if you’re not interested in using modules.

To demo the feature, I created a bunch of fictional books authored by my cats and me. In a real application, these books would be served by an API. The data and its filtering were isolated inside a service module that could be swapped for something else entirely if you need to query an external api.

The FACET_MAPPINGS global

var FACET_MAPPINGS = {
 "author": {
 "facet_title": "Authors",
 "selected_class": "author_selected"
 },
 "year": {
 "facet_title": "Years",
 "selected_class": "year_selected"
 }
};

The application assumes that you have knowledge of the possible facet filters available to you in the documents. This knowledge is reflected in FACET_MAPPINGS where you assign a title for your facet (facet_title) and a CSS class for the selected state of your facet (selected_class). This mapping is used for two things:

  • Create the HTML of your facet.
  • Identify how to filter data based on the presence or absence of the selected_class.

Adding and removing facets from the UI is done through FACET_MAPPINGS. For example, you could add a new "title" facet to this project just by editing FACET_MAPPINGS (adding the “title” facet would not make much sense however).

The flow of the app

When you load the site, loadResults creates the HTML of the result list. Then, to create the HTML of the facets section, createFacet is called for each of the facets defined in your mapping.

The interesting stuff happens in handleFacetClicks:

  • The style of the clicked facet is toggled.
  • If the click action was performed on an unselected facet, the facet data is added to an array of selected facets:

var targetId = event.target.id.split("_")[0];
if (event.target.className.indexOf(FACET_MAPPINGS[targetId].selected_class) === -1) {
 resetClassOnFacetValues(facet);
 event.target.className += " " + FACET_MAPPINGS[facet].selected_class;
 selectedFacets.push({"facet": facet, "facetValue": event.target.innerText});
} else {
 event.target.className = facet;
}

  • A loop goes through FACET_MAPPINGS and search for currently selected facets (other than the one which was just clicked). Identified facets are added to the array of selected facets:

for (var key in FACET_MAPPINGS) {
 if (FACET_MAPPINGS.hasOwnProperty(key) && key !== facet) {
 var selectedFacet = document.getElementsByClassName(FACET_MAPPINGS[key].selected_class);
 if (selectedFacet.length > 0) {
 selectedFacets.push({"facet": key, "facetValue": selectedFacet[0].innerText});
 }
 }
}

  • filterResults is called with the array of selected facets and the result is passed to loadResults to re-render the result list.

Things to consider

All the facet values are shown, whether the result list contains results for a specific value of not. This is because the facet values are built once based on all the results when you load the page. It wouldn’t be complex to re-render the facets when the results are filtered though.

The facets are very simple. Selecting an author will unselect other authors for example. A facet will work correctly if there is only one value per field in your documents.

And that’s one simple way to create a facet UI in plain JavaScript.


Originally published on carlbolduc.com.

Like what you read? Give Carl Bolduc a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.