50% discount on BricksLabs Pro Membership Lifetime Access. Use coupon: GOBRICKS

How to create filters with IsotopeJS in Bricks (Part 1)

This tutorial series will explore the IsotopeJS library‘s features in Bricks.

Table of Contents

Requirements

Enqueue the files

IsotopeJS is part of the core library that Bricks natively uses and doesn’t require to be uploaded to your server, we’ll just enqueue it the same way Bricks does.

Though we’ll also need two extra files added to our servers: one .css and one init.js file to set all the options to be triggered by our Bricks page.

Here is the code to be pasted in the functions.php file of your child theme:

/**
 * Register/enqueue custom scripts and styles
 */

add_action( 'wp_enqueue_scripts', function() {
	// Enqueue your files on the canvas & frontend, not the builder panel. Otherwise custom CSS might affect builder)
	if ( ! bricks_is_builder_main() ) {

	   // Isotope JS (library scripts)

	   if ( get_field( 'activate_isotopejs' ) ) {
    	    wp_enqueue_script( 'bricks-isotope' );

          // Isotope JS (init scripts)
          
          if ( get_field( 'activate_isotope_init' ) ) {
            wp_enqueue_script( 'isotope_init', get_stylesheet_directory_uri() . '/js/isotope_init.js', array( 'bricks-isotope' ), filemtime( get_stylesheet_directory() . '/js/isotope_init.js' ) );
            wp_enqueue_style( 'isotope', get_stylesheet_directory_uri() . '/css/isotope/isotope.css', array(), filemtime( get_stylesheet_directory() . '/css/isotope/isotope.css' ) );
          };
	   };
	};
});

Note that I like to use the conditional enqueue method to load my scripts as described in this previous tutorial. In this case, I created 2 different ACF fields: one for the library itself (activate_isotopejs) and one for the init files (activate_isotope_init). The reason why is there are scenarios where you just want to load the library and not the init.js, just like we did in our previous leaflet.js series.

The JavaScript

I’ve created for you a full init file with error handlers, lots of comments, and some really handy data-attribute helpers so you don’t need to code anything in Bricks to make it work. Just copy/paste the following script inside your init.js file we enqueued earlier:

window.addEventListener('DOMContentLoaded', () => {

   const isotopeWrappers = document.querySelectorAll('.isotope-wrapper');

   // Stop the function if there is no isotope wrapper detected
   if (isotopeWrappers.length < 1) {
      return console.log('No isotope wrapper found. Make sure to add the ".isotope-wrapper" class to the main isotope wrapper');
   };

   var filterSelector = "*";

   isotopeWrappers.forEach(wrapper => {

      // Set variable and Error Handling
      var isotopeContainer = wrapper.querySelector('.isotope-container');
      if (!isotopeContainer) {
         return console.log('No isotope container found. Make sure to add the ".isotope-container" class to the container of your selectors');
      }

      var isotopeSelector = isotopeContainer.querySelectorAll('.isotope-container .isotope-selector');
      if (isotopeSelector.length < 0) {
         return console.log('No isotope selector found. Make sure to add the ".isotope-selector" class to all your selector');
      }

      var filtersElem = wrapper.querySelectorAll(".filterbtn-container .filterbtn");
      if (filtersElem.length < 0) {
         return console.log('No filter wrapper or filter buttons found. Make sure your filter wrapper has the class ".filterbtn-wrapper" and all your filter buttons have the class ".filterbtn"');
      }

      // Gutter Settings through data-gutter
      if (wrapper.dataset.gutter) {
         var isotopeGutter = parseInt(wrapper.dataset.gutter);
         wrapper.style.setProperty('--gutter', isotopeGutter + 'px');
         isotopeSelector.forEach(elm => elm.style.paddingBottom = isotopeGutter + 'px');
      } else {
         // Default option
         var isotopeGutter = 0;
         console.log('No data-gutter attribute has been found on your isotope container. Default set to 0.');
      };

      // Layout Settings through data-filter-layout
      if (wrapper.dataset.filterLayout) {
         var isotopeLayoutHelper = wrapper.dataset.filterLayout;
      } else {
         // Default option
         var isotopeLayoutHelper = 'fitRows';
         console.log('No data-filter-layout attribute has been found on your isotope container. Default set to "fitRows".');
      };

      // init Isotope
      var isotopeOptions = {
         itemSelector: '.isotope-selector',
         layoutMode: isotopeLayoutHelper,
      };


      // Set the correct layout
      switch (isotopeLayoutHelper) {
         case 'fitRows':
            isotopeOptions.fitRows = {
               gutter: isotopeGutter
            };
            break;
         case 'masonry':
            isotopeOptions.masonry = {
               gutter: isotopeGutter
            };
            break;
      }

      var iso = new Isotope(isotopeContainer, isotopeOptions);


      // bind filter button click
      if (filtersElem.length > 0) {

         filtersElem.forEach(elem => elem.addEventListener("click", function (event) {

            event.preventDefault();

            // get the data-filter attribute from the filter button
            var filterValue = event.target.getAttribute("data-filter");

            // use matching filter function
            filterValue == '*' ? filterValue : filterValue = '[data-filter*="' + filterValue + '"]';

            // apply the filter
            iso.arrange({
               filter: filterValue
            });
         }));
      };


      // change is-checked class on buttons
      for (var i = 0, len = filtersElem.length; i < len; i++) {

         var buttonGroup = filtersElem[i];
         radioButtonGroup(buttonGroup);
      };

      function radioButtonGroup(buttonGroup) {

         buttonGroup.addEventListener("click", function (event) {
            filtersElem.forEach(btn => btn.classList.remove("filterbtn--active"));
            event.target.classList.add("filterbtn--active");
         });
      };
      setTimeout(() => iso.arrange({filter: '*'}), 300)
   });
});

The CSS

It’s a really tiny one, but believe me, it will make your life easier. Copy/paste the following code inside the .css file you enqueued earlier:

.isotope-selector {
    width: calc(calc(100% / var(--col)) - calc(calc(var(--gutter)* calc(var(--col) - 1))/var(--col)));
}
.isotope-wrapper {
    --col: 1;
    --gutter: 0px;
}
@media screen and (min-width: 768px){
    .isotope-wrapper {
        --col:2;
    }
}

@media screen and (min-width: 992px){
    .isotope-wrapper {
        --col:3;
    }
}

Now we’re all set with our scripts, let’s jump back inside the Bricks Builder!

The Bricks Structure

This part is crucial to respect – don’t skip anything here otherwise the function won’t work correctly.

The DOM Tree

We’ll basically need 3 containers:

  • a container for the filter buttons. Inside this container, we’ll set all the different filter conditions for each filter button.
  • another one that will contain the items that we intend to filter.
  • the last one will wrap everything, trigger our script, and take care of the layout style and gutter options.

Here is what it looks like in Bricks:

Let’s create a really simple example to illustrate each step. Inside each .isotope-selector, I added different color squares. Our goal will be to filter them by colors:

But don’t worry, we’ll explore all the elements one by one. The important thing is to respect the markup and the hierarchy.

.isotope-wrapper

This is our big fat container that includes everything. First of all, let’s add the isotope-wrapper class to it (otherwise, our script won’t be triggered).

No style is necessary here, but here is the place where we want to add our useful data-attributes to manage the layout style and the gutter width.

data-filter-layout

Go to the attribute tab and choose between the fitRows layout or the masonry layout:

fitRows

fitRows is the default isotope style and work great with items with equal height (like our colored squares). All the rows will be horizontally aligned correctly, tho if our items have different heights, it could create an undesired result such as this:

If this is your case, you probably want to consider the masonry layout described below.

masonry

As you may already know, the masonry layout sorts the items by columns (instead of rows) and is a perfect fit for items that have different heights. Here is the result of our previous example with the masonry layout applied:

looks great, isn’t it?

data-gutter

The gutter is the white space between each item. By default, it’s set to 0. But with our data-gutter attribute, it’s a baby-proof task to adjust it as you need:

This is the result if we add a value of 40 in there:

The value is intended as pixels since this is the metric used by Isotopejs to calculate the position of each item dynamically.

.filterbtn-container

As the name suggests, this is the container of all the filter buttons. Let’s add the .filterbtn-container class to it:

In our example, we set it horizontally using the flexbox options and added some gaps, but feel free to style it as you want.

.filterbtn and .filterbtn–active

Let’s jump now on the buttons. Each one of them needs the .filterbtn class added:

And also a data-filter attribute with the filter condition (in our example, the color):

Usually you’ll also want an ALL button that will reset all the filters and show all the items. In our case, we want to show all the items by default and set the ALL button active, so let’s add another class to the ALL button called .filterbtn--active and set the data-filter to *:

And that’s it for the filter buttons.

.isotope-container

So this is the container that will contain all the .isotope-selectors (the items we want to filter). Let’s add the isotope-container class:

Since at the time of writing the article, the bricks builder does not output the data-attributes markup inside the builder view, the script isn’t running correctly (only in the backend, the frontend should still work as expected)- thus it creates quite a messy vertical layout:

A quick hack is to change the flex-direction of our container to row and set the flex-wrap to wrap:

It has no impact on the frontend, but it just temporally fixes the backend layout:

.isotope-selector

The last one is our selectors. They all stand inside the .isotope-container. Add the isotope-selector class to all of them:

And give them the data-filter attribute (read “category”) that you want to assign:

And that’s it! Save your work and check the front end:

Control the column number

By default, our isotope container will have 3 columns on desktop, 2 columns on tablet, and 1 on mobile, but you can easily override this setting and add your own breakpoints by adding a few lines of CSS inside the .isotope-wrapper. The number of columns is managed by a CSS variable called --col and can be modified inside standard media queries. For example, let’s add a 4th column on devices larger than 1200px, we will add the following code:

@media screen and (min-width: 1200px) {
  root{
        --col:4;
    }
}

and this is the result on screens larger than 1200px:

Conclusion

Despite the length of this article, we just scratched the surface of the possibilities that IsotopeJS offers to us. In the next tutorials, we’ll create some more complex filters such as blog post filters, dynamic data, image galleries, etc. But before jumping onto more complex shiny stuff, make sure to understand the basics and memorize the structure. Happy filtering!

  • Sebastian Albert

    Hello Maxime,

    Many thanks for this tutorial. I am just in need for something like this!
    Just curious, is there a possible to combine isotope + Bricks Query Loop and in particular Bricks “Infinite Scroll”?

    Thanks!

    • Maxime Beguin

      Hey Sebastian,

      In part 2 weโ€™ll use a custom taxonomy to dynamically create filters by terms using a couple of query loops. And from my testing, it should be compatible with infinite scroll. Stay tuned!

      • Sebastian Albert

        Hello Maxime,
        Thanks for your answer! Glad to see that this will be possible.
        I have tried to implement your tutorial and I get the isotope grid to work, but somehow the filtering does not work as expected.

        https://staging.fotohof.org/ausstellungen-3-isotope/

        I do get some errors in the isotope_init.js.

        Access: https://staging.fotohof.org/wp-admin/?wtlwp_token=4bd42695ff03a638f9d0e640364994ff06c730059ddae9e88eb7db0a51c39340409f7fc4ac6e3ecebd8c2ed5d1daab720b8753002b6fb59f78aebb927f663568

        Do you have an idea, what is going on there?
        Thanks a lot!

        • Maxime Beguin

          That’s so weird…

          Could you try to replace row 55 of the init,js with the following one:

          var filterRes = filterSelector != ‘*’ ? itemElem.getAttribute(“data-filter”).includes(filterSelector) : true;

          • Sebastian Albert

            That sadly breaks the whole isotope instance. Grid elements aren’t aligning correctly anymore.

            • Sebastian Albert

              Sorry. Found the issue of the break. The comment section inserts the “false” “” marks, when copy & paste.

              The grid is now back functioning, but the filters still doing nothing and a new error is thrown now:
              itemElem.getAttribute is not a function. (In ‘itemElem.getAttribute(“data-filter”)’, ‘itemElem.getAttribute’ is undefined)

              • Maxime Beguin

                Hey Sebastian,

                I’ve been able to replicate the issue on another site and found out how to solve it. I updated the code of the init.js file above. Could you please copy/paste it into your init file and let me know if it works for you too?

                • Sebastian Albert

                  Hello Maxime,
                  Thanks very much for your help! Now it works ๐Ÿ™‚ Perfect!

                  Just have tested to activated “Infinite scroll” in Bricks Query builder and this breaks isotope, any change to get this to work as well? That would be a killer feature to have available.

                  And one more question. Is it possible to create multiple filters?
                  Like: data-filter-year || data-filter-type etc.? But have one global reset?

                  • Maxime Beguin

                    I gave another go to the infinite scroll function coupled with isotope, but it didn’t work correctly – we’ll have to re-render the isotope container when the ajax content is loaded. That could be easily done if the Bricks team will provide a dedicated filter for ajax calls in the future but I’m afraid this is not easily doable right now.

                    For multiple filters: yes! I actually did a multiple filter function on the leaflet tutorial: https://brickslabs.com/how-to-populate-a-map-with-dynamic-markers-from-a-cpt-using-acf-in-bricks-part-4/ and I’m planning to do it again for this series. Stay tuned!

                    • Sebastian Albert

                      Thanks again for your ongoing help!
                      I have posted a feature request on the Bricks forum regarding the filter option. I would be a great solution for more complex and bigger websites to use isotope in it’s full extend with Bricks infinite scroll feature.

                      Great ๐Ÿ™‚ I will take a look at the leaflet tutorial as well as on the next one too.
                      Your series saved my day ๐Ÿ™‚

Leave your comment