How to populate a map with dynamic markers from a CPT using ACF in Bricks (PART 4)

This tutorial provides the PHP & JS codes that can be pasted in order to filter your markers on a map based on terms of a taxonomy inside the Bricks Builder.

Table of Contents

Requirements

  • All the codes running from PART 1, PART 2 and PART 3 of the map series.
  • Create a custom taxonomy (in our example we used store_categories) and assign each custom post a specific term (in our example we used mobile and internet).

Bricks structure

Since we are willing to filter the markers based on a term filter, the first thing to do is to create a list of buttons with each term name:

To achieve it, we created a button container just after the Input Search code element that contains one static button – the “ALL” button that will erase the filters – and a query loop div that will loop through the terms and print a button for each of them.

On the Button wrapper, make sure to add the filterbtn-wrapper class:

On the Button “ALL” we’ll add two classes: .filterbtn and .filterbtn--active:

The .filterbtn class is shared with all the buttons – including the term ones. The .filterbtn--active is here for two reasons:

  • it makes the “ALL” button active by default on load
  • it consents us to style the active state as we want, inside the builder.

Let’s also change the link type to External URL and set # as value:

And add a data-attribute called data-filter with value of *:

Now let’s jump on the Terms Query loop and fill the following setting:

Let’s move on to our last new element: the button inside our query loop. The text will be dynamically generated by the term_name function. We’ll also add the filterbtn class and set the external link as #:

And add a data-filter attribute with the term_name function as a value:

Now that our filter buttons are all set, we need to add the filter condition for each custom post. To do that let’s jump back inside our Query Loop Stores element (the one below the isotope wrapper):

And let’s add a data-filter attribute as well with a custom function (that we’ll create later inside our functions.php file) that will output all the terms used in each custom post:

We’re all set, let’s code some cool stuff!

The PHP

The code from the previous tutorial doesn’t require any change, but let’s just add the custom function we used earlier to output all terms of a custom post as a list of strings. Copy/paste the following code inside your functions.php file:

function bl_get_terms_from_post() {
	
	$term_obj_list = get_the_terms( $post->ID, 'store_categories' );
	$terms_string = join( ', ', wp_list_pluck( $term_obj_list, 'name' ) );

	return $terms_string;
}

Note that store_categories is the name of our taxonomy. Make sure to match your own taxonomy’s name.

The JavaScript

First I’ll paste the full code that you need to replace, then I’ll comment on the important changes. Here is the final code:

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

    const canvas = document.querySelector('#map');

    if (!document.body.classList.contains('bricks-is-frontend') || canvas === null) {
        return;
    }

    var mbAttr = 'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>';
    var mbUrl = 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw';

    var streets = L.tileLayer(mbUrl, {
        id: 'mapbox/streets-v11',
        tileSize: 512,
        zoomOffset: -1,
        attribution: mbAttr
    });

    var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    });

    var satellite = L.tileLayer(mbUrl, {
        id: 'mapbox/satellite-v9',
        tileSize: 512,
        zoomOffset: -1,
        attribution: mbAttr
    });

    var map = L.map('map', {
        center: [canvas.dataset.leafletLat, canvas.dataset.leafletLong],
        zoom: canvas.dataset.leafletZoom,
        maxZoom: canvas.dataset.leafletMaxZoom,
        layers: [osm, cities]
    });

    var baseLayers = {
        'OpenStreetMap': osm,
        'Streets': streets,
        'Satellite': satellite
    };

    var layerControl = L.control.layers(baseLayers).addTo(map);

    // Jump to map on click

    const storeHeadersLink = document.querySelectorAll('.store-header-link');

    if (storeHeadersLink.length > 0) {
        storeHeadersLink.forEach((link) => {
            link.addEventListener('click', (e) => {
                e.preventDefault();
                let lat = e.target.dataset.leafletLat;
                let long = e.target.dataset.leafletLong;
                map.flyTo([lat, long], 17);
            });
        });
    };

    // Filter search input with IsotopeJS

    // quick search regex
    var qsRegex;
    var filterSelector = "*";

    // init Isotope
    var iso = new Isotope('.isotope-wrapper', {
        itemSelector: '.store-wrapper',
        layoutMode: 'fitRows',
        filter: function(itemElem) {
            var search = qsRegex ? itemElem.textContent.match(qsRegex) : true;
            var filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;

            return search && filterRes;
        },
    });
    var mapRendering = () => {
        boundsList = [];
        var filteredItems = iso.filteredItems;
        if (filteredItems < 1) {
            return;
        }
        filteredItems.forEach(elm => {
            elmt = elm.element;
            let header = elmt.querySelector('.store-header')
            let storeId = header.dataset.store;
            let lat = header.dataset.leafletLat;
            let long = header.dataset.leafletLong;
            stores[storeId].addTo(cities);
            boundsList.push(new L.LatLng(lat, long));
        });
        map.fitBounds(boundsList, {
            padding: [50, 50]
        });
    }

    // use value of search field to filter
    var quicksearch = document.querySelector('.quicksearch');
    quicksearch.addEventListener('keyup', debounce(function() {
        qsRegex = new RegExp(quicksearch.value, 'gi');

        // delete the current markers
        cities.clearLayers();

        //filter the store list
        iso.arrange();

        //add the new filtered markers to the map
        mapRendering();
    }, 200));

    // debounce so filtering doesn't happen every millisecond
    function debounce(fn, threshold) {
        var timeout;
        threshold = threshold || 200;
        return function debounced() {
            clearTimeout(timeout);
            var args = arguments;
            var _this = this;

            function delayed() {
                fn.apply(_this, args);
            }
            timeout = setTimeout(delayed, threshold);
        };
    };

    // Filter terms

    // bind filter button click
    var filtersElem = document.querySelectorAll(".filterbtn-wrapper .filterbtn");
    if (filtersElem.length > 0) {
        filtersElem.forEach(elem => elem.addEventListener("click", function(event) {
            event.preventDefault();
            var filterValue = event.target.getAttribute("data-filter");
            filterSelector = filterValue;

            // delete the current markers
            cities.clearLayers();

            //filter the store list
            iso.arrange();

            //add the new filtered markers to the map
            mapRendering();
        }));
    };


    // change filterbtn--active 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");
        });
    }
});

The first change we’ve made is to add a second filter method to our Isotope initialization:

    // init Isotope
    var iso = new Isotope('.isotope-wrapper', {
        itemSelector: '.store-wrapper',
        layoutMode: 'fitRows',
        filter: function(itemElem) {
            var search = qsRegex ? itemElem.textContent.match(qsRegex) : true;
            var filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;

            return search && filterRes;
        },
    });

Note that we return the result of both the search filter AND (not OR) the buttons filters. That means you the user will be able to use both types of filters simultaneously.

The second important change is we decided to move all the code dedicated to the filter process of the markers into a separate function, so we can call that function mapRendering() whether the filter is the search input or buttons:

        // Populate the filtered markers
	var mapRendering = () => {
		boundsList = [];
		var filteredItems = iso.filteredItems;
		if (filteredItems < 1){
			return;
		}
		filteredItems.forEach(elm => {
			elmt = elm.element;
			let header = elmt.querySelector('.store-header')
			let storeId = header.dataset.store;
			let lat = header.dataset.leafletLat;
			let long = header.dataset.leafletLong;
			stores[storeId].addTo(cities);
			boundsList.push(new L.LatLng(lat, long) );
		} );
		map.fitBounds(boundsList, {padding: [50, 50]});
	}

We added a eventListener on the filter buttons so on each button’s click, our script will retrieve the term associated with it and apply the filters both on the list and the map:

// bind filter button click
	var filtersElem = document.querySelectorAll(".filterbtn-wrapper .filterbtn");
	if (filtersElem.length > 0) {
		filtersElem.forEach ( elem => elem.addEventListener("click", function(event) {
			event.preventDefault();
			var filterValue = event.target.getAttribute("data-filter");
			filterSelector = filterValue;

			// delete the current markers
			cities.clearLayers();

			//filter the store list
			iso.arrange();

			//add the new filtered markers to the map
			mapRendering();
		}));
	};

Finally, the last part of the code is dedicated to the active class .filterbtn--active added when a filter button is clicked (only for styling purposes):

    // change filterbtn--active 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");
        });
    }

And that’s it!

Conclusion

Hopefully, everything is in place and you should be able to test the new term filters on frontend:

That’s it for now! I hope you enjoyed this tutorial and let’s catch up on the next one!

Instant access to all 250+ Bricks code tutorials with BricksLabs Pro

2 comments

Leave your comment