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 633 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
ACPT Gallery Field Query Loop in Bricks

ACPT Gallery Field Query Loop in Bricks

How we can output images from ACPT‘s Gallery field for posts as a grid using a query loop.
Categories:
Pro
CPT Submenu Items in ACF Pro Options Admin Menu

CPT Submenu Items in ACF Pro Options Admin Menu

This Pro tutorial provides the steps to add links to admin pages under an Options page created with ACF Pro. All code mentioned in this…
Posts Related by Current Post’s Terms in Bricks

Posts Related by Current Post’s Terms in Bricks

Showing other posts assigned to the same categories/tags/custom taxonomy terms as the current post.
Categories:
How to create filters with IsotopeJS in Bricks (Part 1)

How to create filters with IsotopeJS in Bricks (Part 1)

This tutorial series will explore the IsotopeJS library's features inside the Bricks ecosystem.
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: