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

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

This tutorial provides the PHP & JS codes that can be pasted in order to filter your markers on a map each time you digit a letter in search field inside the Bricks Builder.

Table of Contents

Requirements

Bricks structure

Let’s try to apply a filter to both the store list and the map based on a quick search field. In order to achieve it, we’ll have to load the Isotope JS library (included as a core library of Bricks) and slightly modify our Bricks structure by adding a dedicated wrapper:

Add a class isotope-wrapper to it:

And finally add a 100% width to this container:

Let’s move on to our new Code element and add our search input by adding the following HTML:

<input type="text" class="quicksearch" placeholder="Search" />

Now let’s jump on the Store Header element and add a new data-attribute called data-store and let’s add post_id as the value:

Finally let’s open our map canvas and add a new data-attribute called data-leaflet-max-zoom with value 12 (feel free to change it to fits your needs):

And that should be it for the settings in Bricks. Let’s code some PHP and JavaScript!

The PHP

First of all, let’s enqueue the Isotope JS script and do some changes to our previous code, so we can dynamically add and remove our markers based on the input file. Replace the previous code with this one:

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

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

	   // Leaflet

       if ( get_field( 'activate_leaflet' ) ) {
    	    wp_enqueue_script( 'leaflet', get_stylesheet_directory_uri() . '/js/leaflet/leaflet.js', array(), filemtime( get_stylesheet_directory() . '/js/leaflet/leaflet.js' ) );
			wp_enqueue_style( 'leaflet', get_stylesheet_directory_uri() . '/css/leaflet/leaflet.css', filemtime( get_stylesheet_directory() . '/css/leaflet/leaflet.css' ) );
			wp_enqueue_script( 'leaflet_init', get_stylesheet_directory_uri() . '/js/leaflet_init.js', array('leaflet'), filemtime( get_stylesheet_directory() . '/js/leaflet_init.js' ) );
			$cities = "var stores = [];var boundsList = []; var cities = L.layerGroup();";
			// CUSTOM QUERY
			$args  = array(
				'post_type'      => 'stores',
				'posts_per_page' => '-1',
				'post_status'    => 'publish'
			);
			$query = new WP_Query( $args );

			// GET POST SETTINGS
			if ( $query->have_posts() ) :
				while ( $query->have_posts() ):
					$query->the_post();
					$id = get_the_ID();
					$title = get_field( 'store_name' );
					$address = get_field('store_address');
					$lat = get_field( 'store_lattitude' );
					$long = get_field( 'store_longitude' );

					$cities .= "stores[" . $id . "] = L.marker([" . esc_attr( $lat ) . "," . esc_attr( $long ) . "]).bindPopup(`<a href=" . get_permalink() . "><h4>" . esc_attr( $title ) . "</h4></a><br>" . $address . "`);boundsList.push( new L.LatLng( ". $lat .", ". $long ." ) );";
				endwhile;
				$cities .= "stores.forEach( store => store.addTo( cities ) )";
			endif;
			wp_add_inline_script( 'leaflet_init', $cities, 'before' );
	   }
	}
});

The JavaScript

Replace the previous script by this one:

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;

	// init Isotope
	var iso = new Isotope( '.isotope-wrapper', {
		itemSelector: '.store-wrapper',
		layoutMode: 'fitRows',
		filter: function( itemElem ) {
			return qsRegex ? itemElem.textContent.match( qsRegex ) : true;
		},
	});


	// 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
		boundsList = [];
		var filteredItems = iso.filteredItems;
		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]});
	}, 200 ) );

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

});

We’ve added a new maxZoom property to our map to avoid zooming too far into the map when we filter single elements:

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

We added the following script to initialize Isotope JS and match the search input as our filter:

// Filter search input with IsotopeJS

	// quick search regex
	var qsRegex;

	// init Isotope
	var iso = new Isotope( '.isotope-wrapper', {
		itemSelector: '.store-wrapper',
		layoutMode: 'fitRows',
		filter: function( itemElem ) {
			return qsRegex ? itemElem.textContent.match( qsRegex ) : true;
		},
	});


	// 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
		boundsList = [];
		var filteredItems = iso.filteredItems;
		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) );
		} );

                // Bound the filtered markers to the map
		map.fitBounds(boundsList, {padding: [50, 50]});
	}, 200 ) );

Note that we also added an eventListener on the quick search input so each time the user will digit a character inside the input, the script will populate the map with the filtered markers and calculate the correct bounds so all the markers are visible on the resulting map.

Finally, we added a setTimeout function to delay the results by 200ms between the moment the character is typed and the results are shown – that will avoid excessive CPU usage and give a better UX experience:

// 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 );
		};
	}

Conclusion

If everything is working correctly, you should be now able to filter your store list with a quick search input:

Pretty cool isn’t?

Get access to all 526 Bricks code tutorials with BricksLabs Pro

4 comments

  • If the markers is hidden after writing in the search box, add .store-header classe to Store header element.

  • Your code only work in Firefox when we are logout but if login it show the undefined error. I guess your code has some issue with either with bricks or isotop or the JS code has element that is not properly defined for for Firefox compatibility.

  • Hi Maxime Beguin,

    I got stuck on search box. It is not working. Would like look at the site please. In case I did something wrong. I can't figure out.

    dev.bloomdirect.co.uk/find-a-florist/

  • Hi Maxime Beguin,

    I got stuck on search box. It is not working. Would like look at the site please. In case I did something wrong. I can't figure out.

    https://dev.bloomdirect.co.uk/find-a-florist/

Leave your comment

 

Related Tutorials..

Pro
How to Combine Multiple Menus into Bricks’ Mobile Menu

How to Combine Multiple Menus into Bricks’ Mobile Menu

Bricks' native 'Nav Menu' element comes with a built-in mobile menu, which shrinks the menu down to a menu toggle button to open an offcanvas…
Categories:
Pro
Outputting ACF Repeater’s Sub Fields

Outputting ACF Repeater’s Sub Fields

This Pro tutorial provides the code for printing custom HTML after looping through rows of a ACF Repeater (available in ACF Pro) that has a…
Categories:
How to display a Query Loop in 3 columns in Bricks

How to display a Query Loop in 3 columns in Bricks

This tutorial provides the builder settings and CSS codes that can be pasted in order to create a 3-columns query loop container inside Bricks Builder.
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:
Load more for Query loops in Bricks

Load more for Query loops in Bricks

Did you know that Bricks has AJAX loading for query loop posts on-click out of the box?
Categories:
Pro
Search Results Grouped by Post Types in Bricks

Search Results Grouped by Post Types in Bricks

Updated on 08 Oct 2024 This Pro tutorial provides the steps to arrange the search results by specified post types in a Bricks site. We…