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

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!

Get access to all 526 Bricks code tutorials with BricksLabs Pro

2 comments

Leave your comment

 

Related Tutorials..

Pro
Category Grid with ACF Images in Bricks

Category Grid with ACF Images in Bricks

Creating a grid of post categories, each category card showing an image from an ACF field, category name & description.
Categories:
Pro
Customizing ACF Repeater/Relationship or Meta Box Relationship Query Parameters in Bricks

Customizing ACF Repeater/Relationship or Meta Box Relationship Query Parameters in Bricks

For query types other than post, term and user in Bricks i.e, ACF Repeater/ACF Relationship/Meta Box Relationship there are no built-in controls for modifying the…
Categories:
Pro
Post Data for the Current User in Bricks with Meta Box

Post Data for the Current User in Bricks with Meta Box

In the Bricks Facebook group a user asked: I need help with Metabox and Bricks! I have created a CPT "SalesRep" with multiple custom fields…
Categories:
Pro
ACF Flexible Content in Bricks with Each Row in its Own Section

ACF Flexible Content in Bricks with Each Row in its Own Section

In the previous ACF Flexible Content in Bricks tutorial we showed how Bricks' Flexible Content type of query can be used for showing the ACF…
Accessing ACF Repeater Sub Fields Programmatically in Bricks Query Loop

Accessing ACF Repeater Sub Fields Programmatically in Bricks Query Loop

It is possible to output sub field's values when a Bricks query loop's type has been set to a ACF Repeater without writing code. This…
Categories:
Pro
Stacking Post Cards in Bricks

Stacking Post Cards in Bricks

How we can stack posts in a query loop so they stick on top of the previous one when scrolling.
Categories:
Tags: