3rd Aug '22
/
4 comments

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 630 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
ACF Flexible Content Field – Layout Labels and Sub Field Labels

ACF Flexible Content Field – Layout Labels and Sub Field Labels

How to output the text of layout labels and/or sub field labels using the Flexible Content field of ACF Pro.
Categories:
Pro
“Truncate text to these many characters” Bricks Control

“Truncate text to these many characters” Bricks Control

Bricks provides a :<number> dynamic data tag modifier that can be used to limit the amount of text by the specified number of words. Ex.:…
Pro
Related Posts using ACF Post Object in Bricks

Related Posts using ACF Post Object in Bricks

Modifying the query loop to limit the posts to only related posts via an ACF Post Object custom field.
Categories:
Tags:
Pro
Filtering Query Loop Posts by a Non-empty Custom Field in Bricks

Filtering Query Loop Posts by a Non-empty Custom Field in Bricks

This Pro tutorial shows how we can prefilter the results of a query output by a Bricks query loop to only those for which the…
Categories:
Tags:
Pro
ACF Options Fields in Bricks

ACF Options Fields in Bricks

This Pro tutorial walks you through outputting the values of custom fields attached to an ACF Pro's Options page in Bricks builder. We shall register…
Categories:
Pro
Custom Nested Queries in Bricks

Custom Nested Queries in Bricks

In most of the previous tutorials covering nested query loops in Bricks on this site, we accessed the parent query's looping object in the inner…
Categories: