Showing posts with label Google Map. Show all posts
Showing posts with label Google Map. Show all posts

Monday, September 21, 2020

Display Google Map within a Flow using Lightning Web Components

Motivation behind this


Earlier I have written about the post on displaying Google maps either on Salesforce Community and through drag-and-drop functionality. From a long time, I want to experiment that on Flow and tried to achieve the same thing.

This way, we can extend the power of Lightning Web Components combining the configurable features of Flow.

Use Case


Business has a requirement to see the location of an Account in a Google Map, but only on a demand basis. Business doesn't want to see this map on Record Detail page every time. Rather, they want to view as and when required upon clicking on a button on Record Detail page and it will show as a pop up screen.

Developer wants to build this Google map component using Lightning Web Components which will be displayed as a pop up using Flow.

Expected Outcome




Clicking on Show Location quick action, the flow will be displayed as pop up screen to display the location.

Solution Approach


Salesforce provides lightning-map component which displays one or more locations. It inherits the styling from map of Lighting Design System. 

And, it is easier to display the component inside Flow, rather than using a separate pop-up component.

So, let's first prepare the component in LWC.

displayAccountMapInFlow.html

Simple page which holds lighting-map component.


<template>
    <lightning-map
        map-markers={mapMarkers}
        zoom-level={zoomLevel}>        
    </lightning-map>    
</template>

displayAccountMapInFlow.js

Few important points on the approach:
  • My earlier post talks about fetching the data using Apex classes. Here I have used ui*api Wire adapters. The benefits of using this is, we don't have a create separate Apex Class to fetch the record. Secondly, it recognizes access rights of the users meaning if the user doesn't have access to the field then error will return. If you are not sure about field access then you can use those fields as Optional.
  • We know that, we can get recordId using @api, but when the component is embed into flow then this recordId will not work. We need to pass recordId explicitly from Flow. This is tricky.
  • As this component can be reused to display in App page or record detail page so the recordId check has been done in connectedCallback method. 
Following way, you can check falsy values, no need to separately verify null or undefined.

this.recordId = (!!this.recordId) ? this.recordId: this.sfdcRecordId; 

Here is the way, through metadata config file.

<targetConfigs>
	<targetConfig targets="lightning__FlowScreen">
		<property name="sfdcRecordId" 
			type="string" 
			label="Pass record Id"
			description="Pass record Id"/>			   
	</targetConfig>
</targetConfigs> 

Entire .js as below


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* Author: Santanu Boral
*/
import { LightningElement, api,track, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { getRecord } from 'lightning/uiRecordApi';

//define the field values to be retrieved
const FIELDS = ['Account.Name', 'Account.BillingStreet',
                'Account.BillingCity', 'Account.BillingState',
                'Account.BillingPostalCode', 'Account.BillingCountry'
                ];

export default class DisplayAccountMapInFlow extends LightningElement {
    @api recordId;  //if this component is used other than flow then it will be used
    @api sfdcRecordId; //this is passed from flow
    @api zoomLevel; //this is passed from flow
    
    account; //internal variable to store the account data
    mapMarkers = [];  //this is used on HTML for attribute value 
    
    //This method check the values passed into the component
    connectedCallback(){
        this.recordId = (!!this.recordId) ? this.recordId: this.sfdcRecordId;  
        this.zoomLevel = (!!this.zoomLevel) ? this.zoomLevel: 6;        
    }

    //fetch record details based on recordId
    @wire(getRecord, { recordId: '$recordId', fields: FIELDS })
    wiredRecord({ error,data }) {
        if (data) {
            this.account = data;
            //prepare marker to display on map
            this.mapMarkers = [
                {
                    location: {
                        Street: this.account.fields.BillingStreet.value,
                        City: this.account.fields.BillingCity.value, 
                        State: this.account.fields.BillingState.value,
                        PostalCode: this.account.fields.BillingPostalCode.value,                         
                        Country: this.account.fields.BillingCountry.value
                    },    
                    icon: 'custom:custom26',
                    title: this.account.fields.Name.value,
                }                                    
            ];
            
        }
        else if (error){
            let message = 'Unknown error';
            if (Array.isArray(error.body)) {
                message = error.body.map(e => e.message).join(', ');
            } else if (typeof error.body.message === 'string') {
                message = error.body.message;
            }
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error loading Account',
                    message,
                    variant: 'error',
                }),
            );
        }
    }
}

displayAccountMapInFlow.js-meta.xml

You can see the zoomLevel has been passed along with recordId from Flow.


<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>49.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="zoomLevel" 
                type="string" 
                label="Enter zoom level of map"
                description="Enter zoom level of map"/>   
            <property name="sfdcRecordId" 
                type="string" 
                label="Pass record Id"
                description="Pass record Id"/>                   
        </targetConfig>
    </targetConfigs>        
</LightningComponentBundle>

Development on Flow Side


Overall flow will look like as below:


First, create recordId variable to capture the recordId of the current record.


Next, create a variable with text datatype with a name varRecordId.

Now, assignment to be done as follows:


Finally, the form screen as below


You can see that zoom level is passed as 16 and record Id has been passed with {!varRecordId}

Now, let's call this flow through quick action and expose that in the page layout.

That's it!.

For testing purpose, go to a record detail page and clicking on Show Location quick action, the flow will be opened to display the component.

Hope it helps and thanks for reading.

References



Further Reading




Sunday, July 28, 2019

Display Google Map using Salesforce Lightning Web Components leveraging modern JavaScript

Motivation behind this


Currently I am exploring Lightning Web Components (LWC) and trying to display Google map using LWC leveraging modern JavaScript (ES6 Standard). Since modern JavaScript has new functions like Spread function, Arrow function which are well blended into LWC JavaScript controller and hence, drives me to come out with this proof-of-concept.

Use Case


Business has a requirement to display their various office locations in the Google Map which will get retrieved from Salesforce Account Object.

Developer wants to build this requirement using Lightning Web Components.


Expected Outcome





In the above picture I am displaying our company office locations (Bangaluru and Noida) in Google Map. I have taken only 2 locations for simplicity.

Solution Approach


Salesforce provides lightning-map component which displays one or more locations. It inherits the styling from map of Lighting Design System. 

So, it is better to use it.

Syntax for using lightning-map component


<template>
    <lightning-map
 map-markers={mapMarkers}
 markers-title="Our Company Location">
  </lightning-map>
</template>

A marker contains
  • Location Information: A coordinate pair of latitude and longitude, or an address composed of address elements.
  • Descriptive Information: Optional title, description, and an icon. These are relevant to the marker but not specifically related to location.
For more information, refer lightning-map documentation

Little bit homework for preparing data

Since data to be displayed dynamically from database, so created 2 Account records with a name starts with Tavant providing complete BillingAddress information. I have not given Latitude and Longitude as Location which can also be used alternatively.

Let's first construct the Apex class.

DisplayMapController.cls

getOfficeLocations method accepts accountNameInitial  as input parameter and returns List of Accounts based on SOQL query. We are not doing any data transformation which can be recognized in map display, rather let it be handled by LWC js controller will be mentioned below.

Note: 
@AuraEnabled (cacheable=true) used as it will be called from js controller and @wire is calling the method
Secondly, use of bind (:str) variables to get rid of SOQL injection.



public with sharing class DisplayMapController {
    
    @AuraEnabled (cacheable=true)
    public static List<Account> getOfficeLocations(String accountNameInitial){
        String str = accountNameInitial + '%';
        return [SELECT Id, Name, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry 
                FROM Account 
                WHERE Name LIKE :str];       
    }
}


Let's develop Lightning Web Components

Create a VSCode project with a menu selection SFDX: Create Lightning Web Component and provide a name displayLWCMap, which creates 3 files html (UI) , js (Controller) and xml (Configuration) at your chosen directory.

displayLWCMap.html

Inside the template lightning-map component has been used and also zoom-level has been used as additional attribute apart from map-markers and markers-title.

<template>
    <div class="slds-m-around_medium">
        <h1 class="slds-text-heading_small">Tavant Technologies</h1>
        <p class="slds-text-body_regular">Office locations</p>
    </div>
    <lightning-map 
        map-markers={mapMarkers}
        markers-title={markersTitle}
        zoom-level={zoomLevel}>
    </lightning-map>   
</template>

displayLWCMap.js

Good thing of LWC is handling all attributes as js controller level which is opposed to Aura and this class is most important for implementing this functionality.

Follow these steps:
  • import LightningElement, api, wire, track which will be used in this class.
  • import getOfficeLocations as we will be using getOfficeLocations method of DisplayMapController apex class.
  • At the class level, 
    • accountNameParam which is public reactive property decorated with @api
    • error, mapMarkers, markersTitle and zoomLevel are private reactive properties
  • @wire to function i.e getOfficeLocations and passing parameter '$accountNameParam' for accountName (here, parameter has been hard coded, though it can passed from html)
    • This is function, retrieve the data and prepare mapMarkers array with location, icon and title which should be recognized and used in the lightning-map.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { LightningElement , wire, track, api} from 'lwc';

import getOfficeLocations from '@salesforce/apex/DisplayMapController.getOfficeLocations';

export default class DisplayLWCMap extends LightningElement {

    @api accountNameParam; 
    accountNameParam = 'Tavant';
    @track error;   //this holds errors

    @track mapMarkers = [];
    @track markersTitle = 'Tavant Technologies';
    @track zoomLevel = 4;
    /* Load address information based on accountNameParam from Controller */
    @wire(getOfficeLocations, { accountNameInitial: '$accountNameParam'})
    wiredOfficeLocations({ error, data }) {
        if (data) {            
            data.forEach(dataItem => {
                this.mapMarkers = [...this.mapMarkers ,
                    {
                        location: {
                            City: dataItem.BillingCity,
                            Country: dataItem.BillingCountry,
                        },
        
                        icon: 'custom:custom26',
                        title: dataItem.Name,
                    }                                    
                ];
              });            
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.contacts = undefined;
        }
    }

}

displayLWCMap.js-meta.xml


This is configuration file which states where this component will be available.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="displayLWCMap">
    <apiVersion>45.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
    <target>lightning__AppPage</target>
    <target>lightning__RecordPage</target>
    <target>lightning__HomePage</target>
  </targets>
</LightningComponentBundle>


Leveraging Modern JavaScript


Arrow Function

You can observe that following statement has been used which doesn't have function() and return statement.

data.forEach(dataItem => {
  //method body without return statement
}

For example, earlier we use write this way to using function and return statement.

let result = function (a,b) {
  return a+b; 
}
console.log(result(1,2));

Now, with Arrow function, it can be written as:

let result = (a,b) => return a+b; 

console.log(result(1,2))

Also, Arrow function resolves some confusion when dealing with this keyword, when nested functions are involved. Functions have a special variable called this, often referred to as the “dynamic this,” which refers to the object used to invoke the function.

For more information, Refer Understand JavaScript Functions

Spread Operator

Spread operator helps to expand the array which we have used for creating mapMarkers array. Here is an example:


let arr = ['a','b']; 
let arr2 = [...arr,'c','d']; 
  
console.log(arr2); // [ 'a', 'b', 'c', 'd' ] 

We can see, arr values has been appended first and then c and d have been added to the same array.

Similarly, we can append array values at end the array too.


let arr = ['a','b']; 
let arr2 = ['c','d',...arr]; 
  
console.log(arr2); // [ 'c', 'd', 'a', 'b']