ACF PRO Maps - best practise for the "Bedrock" "Sage" way


#1

I’ve been over the forum but can’t see a complete answer.

I’m including ACF Pro in my project simply by installing the plugin in app/mu-plugins/

Step 1:
In setup.php I’m including the key so the map works in the admin:
// Google maps api key for ACF
function acf_google_map_api($api) {
$api[‘key’] = ‘API_KEY_HERE’;
return $api;
}
add_filter(‘acf/fields/google_map/api’, NAMESPACE . ‘\acf_google_map_api’);

Step 2:
I’ve added the SCSS into styles/components/map/_map.scss

Step 3
I’m registering the script with the API key for the frontend… I will wrap this in conditionals like if is_page etc… unless it’s better doing this via routes? if so an example would be appreciated.

wp_register_script('googlemap', "https://maps.googleapis.com/maps/api/js?key=KEY_HERE", false, false, false);
wp_enqueue_script('googlemap');

Step 4
I’m using a partial to setup the map:
public function property_map()
{
return get_field(‘property_map’);
}

So that I can access it in the view like so:

@if ($property_map)

@endif

Step 5
I can’t for the life of me work out how to include the ACF JS into the common.js scripts… I’ve got an example that copies the ACF code with a comment at the top:

/* eslint-disable no-unused-vars */

However the code I found tries to load google like so:
import google from ‘google’;

I don’t feel I need to do that but I’m guessing I need to tell webpack that google isn’t coming from an NPM module?

Can someone help shed some light please or let me know if I’m making a hash of it!

I can get it working with all the steps mentioned above and the ACF code inline on my single property page to help test the first 4 steps are good. It’s just the packaging of the ACF JS code I’m unsure of now.


#2

Sods law… got it working by creating a map.js route, adding a “map” class to the body as I feel thats a sensible way to bring the code in. And then used /*eslint-disable */ at the top of my map.js file.

I think the eslint comment I was copying and pasting was to force an error if undefined vars.

If anyone can comment on if this is messy or if I can improve my enqueue script process to watch for a body class too that’d be great or I’ll just use the default wp if is_page etc…


#3

I use this which I find useful, you won’t need to enqueue the script then.

https://www.npmjs.com/package/google-maps

yarn add google-maps

Then in google-maps.js module for example

/* global google */
import GoogleMapsLoader from 'google-maps';

// https://developers.google.com/maps/documentation/javascript/get-api-key
GoogleMapsLoader.KEY = 'KEY';

function addMap(el)
{
    // do things here
}

function init() {
  GoogleMapsLoader.load(google => {
      let els = document.querySelectorAll('.js-google-map');

      if (!els) {
          return;
      }

      Array.from(els).map(el => google.map = addMap(el));
}

#4

Thanks for the quick reply - sorry a couple of stupid questions incoming…

1, the code you’ve supplied would I simple add that within my routes/maps.js file as that will get triggered when a page has a “map” body class

2, where it says // do things here - is that where I’d place my ACF map code?

Thank you


#5

…one more!

  1. How would I then enqueue this in the admin as that doesnt look for a map class in the body.

#6

1

I wouldn’t add the body class.

Rather, create a JS component/module.

I would place the above JS in a file called scripts/comps/google-maps.js

Then export an init function, like above (this would be at the bottom of google-maps.js)

export default {
  init,
}

Then, in your routes file, in my case I just use scripts/routes/common.js

import googlemaps from '../comps/google-maps';

Then just call the function in common.js

googlemaps.init();

I like to develop my JS according to classes that exist in the DOM, which is what this does in google-maps.js init function

if (!els) {
      return;
}

If there are no DOM elements with js-google-map I just return and stop that component from running. Now you don’t need to worry about route body classes, etc.

2

That’s where you run Google Maps JS functions. ACF just returns the coordinates, you would need to pass those on to the JS via html data attributes. Then you can add logic/markers, etc in the JS.


#7

The backend is dealt with by ACF, just add the Google Key to ACF

/**
 * Google Map API
 * @link https://developers.google.com/maps/documentation/javascript/get-api-key
 */
add_action('acf/init', function () {
    acf_update_setting('google_api_key', '');
});

#8

I just realised that when I went to get a drink… doh! thanks for the detailed reply - super helpful


#9

Here’s my whole JS file if this helps. This is all in ‘vanilla’ JS.

comps/google-maps.js

/* global google */
import GoogleMapsLoader from 'google-maps';

// https://developers.google.com/maps/documentation/javascript/get-api-key
GoogleMapsLoader.KEY = 'KEY';

// map
let map;
let zoom = 15;

// https://snazzymaps.com
let styles = [{'featureType':'water','elementType':'geometry','stylers':[{'color':'#e9e9e9'},{'lightness':17}]},{'featureType':'landscape','elementType':'geometry','stylers':[{'color':'#f5f5f5'},{'lightness':20}]},{'featureType':'road.highway','elementType':'geometry.fill','stylers':[{'color':'#ffffff'},{'lightness':17}]},{'featureType':'road.highway','elementType':'geometry.stroke','stylers':[{'color':'#ffffff'},{'lightness':29},{'weight':0.2}]},{'featureType':'road.arterial','elementType':'geometry','stylers':[{'color':'#ffffff'},{'lightness':18}]},{'featureType':'road.local','elementType':'geometry','stylers':[{'color':'#ffffff'},{'lightness':16}]},{'featureType':'poi','elementType':'geometry','stylers':[{'color':'#f5f5f5'},{'lightness':21}]},{'featureType':'poi.park','elementType':'geometry','stylers':[{'color':'#dedede'},{'lightness':21}]},{'elementType':'labels.text.stroke','stylers':[{'visibility':'on'},{'color':'#ffffff'},{'lightness':16}]},{'elementType':'labels.text.fill','stylers':[{'saturation':36},{'color':'#333333'},{'lightness':40}]},{'elementType':'labels.icon','stylers':[{'visibility':'off'}]},{'featureType':'transit','elementType':'geometry','stylers':[{'color':'#f2f2f2'},{'lightness':19}]},{'featureType':'administrative','elementType':'geometry.fill','stylers':[{'color':'#fefefe'},{'lightness':20}]},{'featureType':'administrative','elementType':'geometry.stroke','stylers':[{'color':'#fefefe'},{'lightness':17},{'weight':1.2}]}];

// google maps callback
function addMap(el) {
  // config
  let config = {
    zoom,
    center: new google.maps.LatLng(0, 0),
    mapTypeId: google.maps.MapTypeId.ROADMAP,
    scrollwheel: false,
    styles,
  };

  // markers
  let markers = el.querySelectorAll('.js-google-map-marker');

  // init
  map = new google.maps.Map(el, config);

  // funcs
  addMapMarkers(markers);
  centerMap();

  // return map
  return map;
}

function addMapMarkers(markers) {
  // assign markers to map
  map.markers = [];

  // add marker
  markers.forEach((marker) => {
    // data from data- attributes
    let data = {
      lat: marker.getAttribute('data-lat'),
      lng: marker.getAttribute('data-lng'),
      icon: marker.getAttribute('data-icon'),
    };

    // position
    let position = new google.maps.LatLng(data.lat, data.lng);

    let icon = {
      url: data.icon,
      size: new google.maps.Size(71, 71),
      origin: new google.maps.Point(0, 0),
      anchor: new google.maps.Point(17, 34),
      scaledSize: new google.maps.Size(25, 25),
    };

    // create instance
    let instance = new google.maps.Marker({
      position,
      map,
      icon,
    });

    // add to map.markers array
    map.markers.push(instance);
  });
}

function centerMap() {
    // bounds
    let bounds = new google.maps.LatLngBounds();

    // get bounds for each marker
    map.markers.forEach((marker) => {
      let latlng = new google.maps.LatLng(marker.position.lat(), marker.position.lng());
      bounds.extend(latlng);
    });

    // determine if single or multiple
    if (map.markers.length === 1) {
      // set center and zoom
      map.setCenter(bounds.getCenter());
      map.setZoom(zoom);
    } else {
      // fit markers into map
      map.fitBounds(bounds);
    }
}

function init() {
  GoogleMapsLoader.load(google => {
      let els = document.querySelectorAll('.js-google-map');

      if (!els) {
        return;
      }

      Array.from(els).map(el => google.map = addMap(el));
  });
}

export default {
  init,
}

Then the Blade view, you would get data-lat, data-lng from the acf field of course.

<div class="google-map js-google-map">
  <div 
    class="google-map-marker js-google-map-marker" 
    data-lat="-33.9190454" 
    data-lng="18.3904372"
    data-icon="http://maps.google.com/mapfiles/ms/icons/blue.png">
  </div>
</div>

#10

Perfect thank you - theres lots of helpful information on this forum but seeing a full solution is always 100% easier to digest - I’m sure it’ll help more than just me!


#11

Also, if you are using Bedrock, you can store your Google Maps API key in your .env:

GOOGLE_MAPS_API='xxxxxx'

In your PHP, you can retreive it using: env('GOOGLE_MAPS_API') (Bedrock recommends using the env library). Adding that to Darren’s code above would look like:

/**
 * Google Map API
 * @link https://developers.google.com/maps/documentation/javascript/get-api-key
 */
add_action('acf/init', function () { 
  acf_update_setting('google_api_key', env('GOOGLE_MAPS_API')); 
});

#12

Thank you - I’ve just done that now based on your recommendation I had spotted it but hadn’t clicked that it was to come from the .env file - all updated!


#13

Very good recommendation, thanks knowler


#14

I used Google Maps in a project Bedrock/Sage9 project today. This is how I implemented it step by step. I came across this thread when I was searching for other Google Maps related stuff :slight_smile: So I thought I’d share my implementation and be able to reference it later. It might be a bit hacky with the wp_head part. Love you hear your improvements!

  1. Add Google Maps API key to config. Open config/environments/development.php and add Config::define('GOOGLE_MAPS_API_KEY', 'XXX');. Don’t forget to add this to your other environments as you go.

  2. Load the API-key to ACF (this is used to make Google maps field work in backend). In web/app/themes/sage/app create a new file advanced-custom-fields.php and add

<?php

namespace App;

add_action('acf/init', function () {
  acf_update_setting('google_api_key', GOOGLE_MAPS_API_KEY);
});
  1. Open web/app/themes/sage/app/setup.php and add the API key to front-end (this is used to load Google Maps API later). This is done by using wp_localize_script(). Add this to the wp_enqueue_scripts action:
wp_localize_script('sage/main.js', 'sage', array(
  'env' => WP_ENV,
  'ajaxurl' => admin_url('admin-ajax.php'),
  'google_api_key' => GOOGLE_MAPS_API_KEY,
));
  1. Add the markup to the theme where you want the map to render. My markup looks like this:
<div class="map-container">
  <div class="map-map">
    <div id="post-map-positions"></div>
  </div>
</div>

Important part here is the post-map-positions ID which will load the map.

  1. Load locations for markers. If you don’t use markers you can skip this. In this example I created a Google maps field called location in ACF.

To load the data to the front-end. I use this function in the appropriate web/app/themes/sage/app/Controllers/<controller>.php It returns the map points along with some post data for all saved posts and adds it to a javascript variable in the html. You can modify it to your needs.

  public function map_points() {
    add_action('wp_head', function() {
      global $wpdb;

      $sql = "
        SELECT
          p.id,
          p.post_title,
          pm.meta_value
        FROM
          wp_postmeta pm
        INNER JOIN
          wp_posts p ON p.ID = pm.post_id
        WHERE
          pm.meta_key = %s AND
          pm.meta_value != '' AND
          p.post_status = 'publish' AND
          p.post_type = 'post'
        ;
      ";

      if ($post_positions = $wpdb->get_results($wpdb->prepare($sql, 'location'), ARRAY_A)) {
        $locations = array_map(function ($c) {
          return array_merge(
            array(
              'id' => $c['id'],
              'permalink' => get_permalink($c['id']),
              'title' => $c['post_title'],
            ),
            unserialize($c['meta_value'])
          );
        }, $post_positions);

        printf('
          <script type="text/javascript">
            var mapPositions = %s;
          </script>
        ', json_encode($locations));
      }
    });

    return true;
  }
  1. Use load-google-maps-api (https://www.npmjs.com/package/load-google-maps-api) to load Google maps on the front end. yarn add load-google-maps-api

  2. Add the following javascript to a route js file to fire on the page you want to show the map. ie. where the markup in step 4 and data in step 5 are available.

/* eslint-disable no-undef */

import loadGoogleMapsApi from 'load-google-maps-api';

export default {
  init() {
    loadGoogleMapsApi({
      key: window.sage.google_api_key || '',
    }).then(() => {
      const mapOptions = {
        disableDefaultUI: true,
        center: {
          lat: 50,
          lng: 50,
        },
        zoom: 5,
      };

      const post_positions = window.mapPositions || false;

      if (post_positions && document.getElementById('post-map-positions')) {
        const map = new google.maps.Map(document.getElementById('post-map-positions'), mapOptions);
        const bounds = new google.maps.LatLngBounds();
        const positions = post_positions;

        positions.forEach(position => {
          const latlng = new google.maps.LatLng(position.lat, position.lng);
          const marker = new google.maps.Marker({
            position: latlng,
            map,
          });

          const contentString = `
            <div>
              <h4>${position.title}</h4>
              <p>
                <a href="${position.permalink}">Read more</a>
              </p>
            </div>
          `;

          const infowindow = new google.maps.InfoWindow({
            content: contentString,
          });

          marker.addListener('click', () => {
            infowindow.open(map, marker);
          });

          bounds.extend(latlng);
        });

        map.setCenter(bounds.getCenter());
        map.setZoom(5);
      }
    }).catch(error => {
      console.error(error); // eslint-disable-line no-console
    });
  }
};