Lightning Map Component Revisited

In this article, I revisit the Map component I shared a few months ago, incorporating some lessons learned around component lifecycle, timing, and race conditions.

Check out this video to see the new Map component in action in the Lightning Experience. The component shows all the locations for a specific account.

Basic Component Setup

<aura:component>

    <ltng:require styles="/resource/leaflet/leaflet.css" 
                  scripts="/resource/leaflet/leaflet.js"
             	  afterScriptsLoaded="{!c.jsLoaded}" />

    <aura:attribute name="accounts" type="Account[]"/>

    <!-- The Leaflet map object -->
    <aura:attribute name="map" type="Object"/>
    <!-- The Leaflet markers -->
    <aura:attribute name="markers" type="Object"/>

    <aura:handler name="change" value="{!v.accounts}"
                  action="{!c.accountsChangeHandler}"/>

    <div aura:id="map"></div>

</aura:component>

Code Highlights:

  • The component uses ltng:require to load the Leaflet Javascript library and CSS style sheet. The jsLoaded event handler is called after the files are loaded.
  • The accounts attribute holds a list of accounts to display on the map. The accounts are retrieved in the parent component and passed to the map component as an attribute.
  • The accountsChangeHandler event handler is called when the value of the accounts attribute changes.

Avoiding Race Conditions

From wikipedia: “A race condition is the behavior of a software system where the output is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when events do not happen in the order the programmer intended”.

To display the map with a marker for each account in the list, the component relies on different operations happening in parallel:

  1. The rendering of the component
  2. The asynchronous loading of the Leaflet Javascript library and CSS files
  3. The asynchronous request for the list of accounts

The order in which these operations complete is unpredictable. Rudimentary testing can lead you to wrong assumptions. The fact is: you need to assume that these operations can complete in any order. For example:

  1. In jsLoaded, you can’t just draw the map inside the component’s “map” div because you can’t assume that the component has already been rendered and therefore that the div already exists in the DOM.
  2. In accountsChangeHandler, you can’t just add markers to the map because you can’t assume that the map has already been drawn (the Leaflet library may still be loading).

Solution

  1. If you need to directly manipulate DOM elements in your component, you should do it in the component’s renderer. This is where the code to draw the map inside the “map” div should go. Let’s override the default rererender() function of the renderer as follows:
    ({
        rerender: function (component, helper) {
    
            var nodes = this.superRerender();
    
            // If Leaflet library is not yet loaded, we can't draw the map: return
            if (!window.L) return nodes;
    
            var map = component.get("v.map");
            
            // Draw the map if it hasn't been drawn yet
            if (!map) {
                var mapElement = component.find("map").getElement();
                map = window.L.map(mapElement, {zoomControl: true});
                component.set("v.map", map);
            }
            
            return nodes;
    
        }
    })
    
  2. In jsLoaded, we simply force the component to rerender():
    jsLoaded: function(component) {
        component.rerender();
    }
    
  3. To load the markers, we create a helper function (because it will be called from different places). Before adding the markers, we check that the map exists. If it doesn’t, we don’t do anything.
    ({
        addMarkers: function(component) {
            var map = component.get('v.map');
            var markers = component.get('v.markers');
            var accounts = component.get('v.accounts');
            
            // Remove existing markers
            if (markers) {
            	markers.clearLayers();
            }
            
            // Add Markers
            if (map && accounts && accounts.length> 0) {
                for (var i=0; i<accounts.length; i++) {
                    var account = accounts[i];
                    if (account.Location__Latitude__s && account.Location__Longitude__s) {
    	                var latLng = [account.Location__Latitude__s, account.Location__Longitude__s];
        	            var marker = window.L.marker(latLng, {account: account});
                       markers.addLayer(marker);
                    }
                }
                map.addLayer(markers);
            }
        }
    })
    
  4. In accountsChangeHandler(), we call addMarkers().
    accountsChangeHandler: function(component, event, helper) {
        helper.addMarkers(component);
    }
    

    If the map is already available, the markers will be added to it. If it’s not, nothing will happen (see previous step) and we’ll have to make sure the markers are added after the map is created (see next step).

  5. At the end of the rerender() function, we add a call to addMarker() to add the markers in case we received the accounts before the map was rendered (accountsChangeHandler was triggered before jsLoaded)

Summary and Source Code

Components that rely on different operations happening in parallel need to be designed carefully to avoid unintended behaviors. Click here to install the application in your developer org using an unmanaged package, and I’m looking forward to your feedback. You will need to add location information (latitude and longitude) to a few accounts for the component to be useful.

Leave your comments...

Lightning Map Component Revisited