4th Aug '22
/
2 comments

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 610 Bricks code tutorials with BricksLabs Pro

2 comments

Leave your comment

 

Related Tutorials..

Custom Animations on Scroll without plugins in Bricks

Custom Animations on Scroll without plugins in Bricks

This tutorial provides the Javascript and CSS codes that can be pasted in order to create custom CSS animations on scroll without any third-party plugins…
Categories:
Bricks Query Loop – Posts Authored by Logged in User

Bricks Query Loop – Posts Authored by Logged in User

How to display only the posts authored by the currently logged-in user in a query loop.
Categories:
GSAP in Bricks: a concrete example

GSAP in Bricks: a concrete example

In this tutorial, we're going to create a custom timeline with different popular GSAP animations and integrate it into the Bricks Builder. Introduction We released…
Categories:
Pro
Sticky Scrolling in Bricks Builder

Sticky Scrolling in Bricks Builder

Updated on 22 Apr 2024 This Pro tutorial provides the steps to fix elements in the viewport i.e., make them sticky while adjacent content in the section…
Categories:
Tags:
Pro
Conditionally Outputting Query Loop Item Based on Post Meta in Bricks

Conditionally Outputting Query Loop Item Based on Post Meta in Bricks

Rendering query-loop enabled posts depending on the value of each post's custom field value is tricky because by default, the custom field plugins' functions or…
Categories:
Pro
ACF Group Sub Field Values from Matching Post Term

ACF Group Sub Field Values from Matching Post Term

In Bricks Reddit, a user asks: Let's break this down and set up the scenario practically. First, there's a Language taxonomy for posts (could be…
Categories:
Pro
Custom WordPress Dashboard Page in Bricks

Custom WordPress Dashboard Page in Bricks

Updated on 10 Jul 2024 This Pro tutorial provides the steps to replace all the meta boxes of the WordPress admin dashboard page with the…
Categories:
Tags: