Sunday, March 21, 2021

Trailblazer Community Event: Preparing JavaScript for LWC Development

 

Motivation behind this


We have successfully organized the following event. Sharing this video and deck to community members for further reference.

NamePreparing JavaScript for LWC Development

On: Saturday, March 20, 2021

RSVPs Received: 500. Registration full.

Attendees: 163





Agenda


Following topics we have covered:
  • Why JavaScript Certification?
  • Exam Structure
  • Listen to experts who have passed JavaScript Certification
  • How this preparation benefits LWC development (solving real-life use cases using ECMAScript, when to do what).
By the end of the session, you will overcome business challenges applying knowledge of JavaScript.

Recordings



The browser doesn't open the video then click on this Link


Reference Deck

Download as (PDF)Preparing JavaScript for LWC Development


Further Reading


Thursday, February 25, 2021

Lightning Message Service: Communication between Lightning Web Components

 

Motivation behind this


Earlier I have written a post on Drag and Drop functionality using Salesforce Lightning Web Components leveraging pub-sub event propagation which works on publish-subscribe model. 

With the introduction of Lightning Message Service (LMS) the communication between components can be possible either in single page or multiple pages over a message channel. It also works to communicate among Visualforce pages, Aura components, Lightning Web Components.

This flexible event communication mechanism drives me to do a proof-of-concept.

Lets get started.


Use Case


Business has a requirement to see a Company's location on google map when company is chosen.

Developer wants to build using Lightning Web Components where one component will list down the companies and other component will show the map. Both are unrelated but listen to the events based on subscription.

Possible End Results


After building the use case, it will perform the functionality as following video:


The screen will look like this:

LMS Demo



Solution Approach


To start with, first create a message channel and push it to the org. the xml will be placed under force-app/main/default/messageChannels/ directory.

Here message channel name has given as Selected_EntityId and the file name will be Selected_EntityId.messageChannel-meta.xml

For more information, refer Create a Message Channel

<?xml version="1.0" encoding="UTF-8" ?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <masterLabel>SelectedEntityId</masterLabel>
    <isExposed>true</isExposed>
    <description>Message Channel to pass a entity Id</description>
    <lightningMessageFields>
        <fieldName>entityInfo</fieldName>
        <description>This is the record information that selected</description>
    </lightningMessageFields>
</LightningMessageChannel>


Lets create publisher component, here it is showAccountList which will display accounts and clicking on the Account it will publish events.

showAccountList component:

HTML will like below, here onclick event has been used with div.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<template> 
    <lightning-card title="Lightning Message Service: communication between LWC Components" 
                icon-name="custom:custom30">
    <div class="c-container">  
        <lightning-layout>
            <lightning-layout-item padding="around-small">   
                <template if:true={accounts.data}>                
                    <template for:each={accounts.data} for:item="account">
                        <div class="column" key={account.Id}
                            data-item={account.Id} onclick={handleClick}>
                            <div class="slds-text-heading_small">{account.Name}</div>
                        </div>
                    </template>
                </template>
            </lightning-layout-item>
        </lightning-layout>
    </div>
    </lightning-card>
</template> 

showAccountList.js

Few notable points in this js file:
  • Importing message channel to communicate via LMS.

// Import message service features required for publishing and the message channel
import { publish, MessageContext } from 'lightning/messageService';
import selectedEntity from '@salesforce/messageChannel/Selected_EntityId__c';
  • Retrieving Account information from Controller importing getAccountLocations
  • Publishing a message where entire account information is collected as an array and sending as payload in handleClick() event.
Entire js file as follows:

 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
/* eslint-disable no-console */
import { LightningElement, wire} from 'lwc';
import getAccountLocations from '@salesforce/apex/AccountHelper.getAccountLocations';

// Import message service features required for publishing and the message channel
import { publish, MessageContext } from 'lightning/messageService';
import selectedEntity from '@salesforce/messageChannel/Selected_EntityId__c';

export default class ShowAccountList extends LightningElement {
    
    @wire(MessageContext)
    messageContext;
    
    //retrieve account records from database via Apex class
    @wire(getAccountLocations) accounts;

    //clicking the div fires this event
    handleClick(event) {
        //retrieve AccountId from div
        let accountId = event.target.dataset.item;
        let arr = this.accounts.data.find(element => element.Id == accountId);
        let payload = { record: arr };
        publish(this.messageContext, selectedEntity, payload);       
    }       
}

AccountHelper.cls

getAccountLocations method it fetches Account data based on SOQL query. I want to show specific records for demo, that's why filter condition has been added.

public with sharing class AccountHelper {
    @AuraEnabled (cacheable=true)
    public static List<Account> getAccountLocations(){
        return[SELECT Id, Name, BillingStreet, BillingCity, 
    BillingState, BillingPostalCode, BillingCountry 
                FROM Account
                WHERE isDisplay__c = true];
    }
}

Screen will look like this:

showAccountList

Let's move to subscriber component which subscribes for the event and display account location in the map.

displayLocationSubscriber component

It will be like this:

displayAccountSubscriber

Code as follows where lightning-map is used to display location.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
    <lightning-card title="Lightning Map component will show selected Company Location" 
        icon-name="custom:custom30">
        <template if:true={isDisplayLocation}>
            <div class="slds-text-heading_small">{selectedAccountName}</div>                         
            <lightning-map
                map-markers={mapMarkers}
                markers-title={selectedAccountName}
                zoom-level = {zoomLevel}
                center = {center}>
            </lightning-map>
        </template>        
    </lightning-card>
</template>

displayLocationSubscriber.js

Few important points:
  • Defining the scope where subscriber component receives the message in our application. Here APPLICATION_SCOPE has been used.
  • Scope feature is only available using @wire(MessageContext)
  • There are methods to subscribe, unsubscribe and import message channel for using it.
  • In the subscribe method, you can see arrow function has been used so it can call any methods of the class using this operator. Scope can be passed as parameter.
  • handleMessage() method retrieves the account array and prepare attribute values to be used in lightning-map.
Entire js file as follows:


 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { LightningElement, wire, track} from 'lwc';
// Import message service features required for subscribing and the message channel
import {
    subscribe,
    unsubscribe,
    APPLICATION_SCOPE,
    MessageContext
} from 'lightning/messageService';
import selectedEntity from '@salesforce/messageChannel/Selected_EntityId__c';

let acct;
export default class DisplayLocationSubscriber extends LightningElement {
    subscription = null;
    record;

    isDisplayLocation=false;
    @track mapMarkers = []; //holds account location related attributes
    markersTitle = ''; //title of markers used in lightning map.
    zoomLevel = 4;   //initialize zoom level
    center; //location will be displayed in the center of the map

    selectedAccountName = '';

    @wire(MessageContext)
    messageContext;

    // Standard lifecycle hooks used to subscribe and unsubsubscribe to the message channel
    connectedCallback() {
        this.subscribeToMessageChannel();
        this.isDisplayLocation = false;
    }

    disconnectedCallback() {
        this.unsubscribeToMessageChannel();
    }

    // Encapsulate logic for Lightning message service subscribe and unsubsubscribe
    subscribeToMessageChannel() {
        if (!this.subscription) {
            this.subscription = subscribe(
                this.messageContext,
                selectedEntity,
                (message) => this.handleMessage(message),
                { scope: APPLICATION_SCOPE }
            );
        }
    }

    unsubscribeToMessageChannel() {
        unsubscribe(this.subscription);
        this.subscription = null;
    }

    // Handler for message received by component
    handleMessage(message) {
        //assigns account records
        acct = message.record;

        this.selectedAccountName = acct.Name;
        
        //prepares information for the lightning map attribute values.
        this.markersTitle = acct.Name;
        
        this.mapMarkers = [
            {
                location: {
                    Street: acct.BillingStreet,
                    City: acct.BillingCity,
                    State: acct.BillingState,
                    Country: acct.BillingCountry,
                },
                icon: 'custom:custom26',
                title: acct.Name,
            }                                    
        ];
        this.center = {
            location: {
                City: acct.BillingCity,
            },
        };
        this.zoomLevel = 6;
        this.isDisplayLocation = true;
    }    
}

Create a Lightning App Builder page with two regions and place those components and display the page, selecting an account on left side component will display location on right side.

Finally we are done and thanks for reading.


Further Reading


Monday, November 9, 2020

Build Configurable Dynamic Table from Field Set using Lightning Web Component

 

Motivation behind this


I have been looking for building configurable Related list component using Field Set and Lightning Web Component. Earlier I have written blog on  Describe Objects and Retrieve Records using Salesforce Lightning Web Components flavored with Dynamic Datatable.

Now, I want to move it one step further using field set. This approach will help many developers to quickly develop and leverage this concepts.

Here is a justification why it is needed to build.

Use Case


Business has requirement to view the Record detail page as well as its Related list. User has edit and delete permissions to the related list's objects. But business wants that user will be not see any options to edit or delete records from the related list.

For example, Case related list is available under Account or Contact record. User has a permission to edit or delete on Case object but those records only be available for view purpose. User will not able to see Edit/Delete option.

Only option is to built this component as read-only viewing purpose.

This requirement is applicable for similar use cases like this, so it will be better to create a configurable component using Field Set.

Fields from the Field Set will be displayed as columns and records based on those fields will be displayed as rows.

Possible End Result


After building the use case, it will look like this:



Solution Approach


Refer this video for three step solutions:


For an example purpose, I have tried to build Case object as Related List.


First Step

Create a Field Set on Case Object and add necessary fields on the layout. For example name is CaseRelatedListFS


Second Step

Build a LWC Component which will take following configurable attributes:
  • Related Object API Name - Here it is Case
  • Field Set Name - The name of field set defined on the Object whose fields will be displayed as table columns. Here it is CaseRelatedListFS
  • Reference API Name - It is the field based on which query to be fired. Here, it is ContactId, as Case related list will be placed on Contact Record Page.
  • First Column As RecordHyperlink - It will take Yes/No. For example, if we define CaseNumber as first column and if we choose Yes option then Case Number will be shown as hyperlink and clicking on that link, it will be navigated to Case Detail page. If we choose No then, it will be same for other columns.
Let's see this lwcFieldSetComponent.js-meta.xml where those parameters can be configured as targetConfig property.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>50.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage,lightning__AppPage,lightning__HomePage">
            <property name="SFDCobjectApiName" label="Related Object API Name" type="String" default=""/>
            <property name="fieldSetName" label="Field Set Name" type="String" default=""/>
            <property name="criteriaFieldAPIName" label="Reference FieldAPIName" type="String" default=""
                            description="The field on which query to be performed. 
                            e.g. it can be AccountId from which Case records will be fetched."/>
            <property name="firstColumnAsRecordHyperLink" label="First Column As RecordHyperLink" 
                            type="String" datasource="Yes,No" default="Yes"/>                        
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Approach has been taken following way:
  • Create a LWC component adding a lightning-datatable which will be populated on load.
  • In the connectedCallback method of js it will fetch fields and records from the database
  • Finally display those in the datatable.
lwcFieldSetComponent.html


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<template>    
    <div class="c-container">
        <div class="slds-card">
            <div class="slds-media__body">
                <h2 class="slds-card__header-title">
                    <span>{lblobjectName} Records ({recordCount})</span>
                </h2>
            </div>
            <div class="slds-card__body">
                <lightning-datatable 
                    key-field="Id"
                    data={tableData}
                    columns={columns}
                    min-column-width=200>
                </lightning-datatable> 
            </div>                           
        </div> 
    </div>    
</template>

lwcFieldSetComponent.js

Few important points to refer here:


  • Never define an attribute as objectApiName as it is reserved, that's why I have used SFDCobjectApiName, otherwise, it will always take current object.

  • When we pull entries from a Map which has been stored in Apex class, the index is inversed. For example, I have first stored FIELD_LIST and then RECORD_LIST in the Map. Now, the index of keys will be 1 for FIELD_LIST and 0 from RECORD_LIST.

  • Extra coding to handle hyperlink record navigation in the first column.


Rest of the comments in the code is self-explanatory.


  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
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
/*
 *   Author: Santanu Boral
*/
import { LightningElement, api, track } from 'lwc';
import getFieldsAndRecords from '@salesforce/apex/FieldSetHelper.getFieldsAndRecords';

export default class LwcFieldSetComponent extends LightningElement {
    
    @api recordId;  // record id from record detail page e.g. ''0012v00002WCUdxAAH'
    @api SFDCobjectApiName; //kind of related list object API Name e.g. 'Case'
    @api fieldSetName; // FieldSet which is defined on that above object e.g. 'CaseRelatedListFS'
    @api criteriaFieldAPIName; // This field will be used in WHERE condition e.g.'AccountId'
    @api firstColumnAsRecordHyperLink; //if the first column can be displayed as hyperlink

    @track columns;   //columns for List of fields datatable
    @track tableData;   //data for list of fields datatable
    
    recordCount; //this displays record count inside the ()
    lblobjectName; //this displays the Object Name whose records are getting displayed

    connectedCallback(){
        let firstTimeEntry = false;
        let firstFieldAPI;

        //make an implicit call to fetch records from database
        getFieldsAndRecords({ strObjectApiName: this.SFDCobjectApiName,
                                strfieldSetName: this.fieldSetName,
                                criteriaField: this.criteriaFieldAPIName,
                                criteriaFieldValue: this.recordId})
        .then(data=>{        
            //get the entire map
            let objStr = JSON.parse(data);   
            
            /* retrieve listOfFields from the map,
             here order is reverse of the way it has been inserted in the map */
            let listOfFields= JSON.parse(Object.values(objStr)[1]);
            
            //retrieve listOfRecords from the map
            let listOfRecords = JSON.parse(Object.values(objStr)[0]);

            let items = []; //local array to prepare columns

            /*if user wants to display first column has hyperlink and clicking on the link it will
                naviagte to record detail page. Below code prepare the first column with type = url
            */
            listOfFields.map(element=>{
                //it will enter this if-block just once
                if(this.firstColumnAsRecordHyperLink !=null && this.firstColumnAsRecordHyperLink=='Yes'
                                                        && firstTimeEntry==false){
                    firstFieldAPI  = element.fieldPath; 
                    //perpare first column as hyperlink                                     
                    items = [...items ,
                                    {
                                        label: element.label, 
                                        fieldName: 'URLField',
                                        fixedWidth: 150,
                                        type: 'url', 
                                        typeAttributes: { 
                                            label: {
                                                fieldName: element.fieldPath
                                            },
                                            target: '_blank'
                                        },
                                        sortable: true 
                                    }
                    ];
                    firstTimeEntry = true;
                } else {
                    items = [...items ,{label: element.label, 
                        fieldName: element.fieldPath}];
                }   
            });
            //finally assigns item array to columns
            this.columns = items; 
            this.tableData = listOfRecords;

            console.log('listOfRecords',listOfRecords);
            /*if user wants to display first column has hyperlink and clicking on the link it will
                naviagte to record detail page. Below code prepare the field value of first column
            */
            if(this.firstColumnAsRecordHyperLink !=null && this.firstColumnAsRecordHyperLink=='Yes'){
                let URLField;
                //retrieve Id, create URL with Id and push it into the array
                this.tableData = listOfRecords.map(item=>{
                    URLField = '/lightning/r/' + this.SFDCobjectApiName + '/' + item.Id + '/view';
                    return {...item,URLField};                     
                });
                
                //now create final array excluding firstFieldAPI
                this.tableData = this.tableData.filter(item => item.fieldPath  != firstFieldAPI);
            }

            //assign values to display Object Name and Record Count on the screen
            this.lblobjectName = this.SFDCobjectApiName;
            this.recordCount = this.tableData.length;
            this.error = undefined;   
        })
        .catch(error =>{
            this.error = error;
            console.log('error',error);
            this.tableData = undefined;
            this.lblobjectName = this.SFDCobjectApiName;
        })        
    }
}

FieldSetHelper.cls

Few notable points on this getFieldsAndRecords method:

  • Getting the instance of SObject based on strObjectApiName, the reflection has been used which is way more faster that globalDescribe.
  • Use of Schema.FieldSetMember to get the fields from Field Set. Refer Documentation
  • getFieldPath() of FieldSetMember gives the fieldAPI which has been used to build SOQL query
Finally, both field List and record List are getting stored in the Map and returned to js.


 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
public with sharing class FieldSetHelper {
    @AuraEnabled (cacheable=true)
    public static String getFieldsAndRecords(String strObjectApiName, String strfieldSetName,
                                             String criteriaField, String criteriaFieldValue){
        Map<String, String> returnMap = new Map<String,String>();
        if(!String.isEmpty(strObjectApiName) && !String.isEmpty(strfieldSetName)){
            //get fields from FieldSet
            SObject sObj = (SObject)(Type.forName('Schema.'+ strObjectApiName).newInstance());
            List<Schema.FieldSetMember> lstFSMember = 
                sObj.getSObjectType().getDescribe().fieldSets.getMap().get(strfieldSetName).getFields();

	    //prepare SOQL query based on fieldAPIs	
	    String query = 'SELECT ';
	    for(Schema.FieldSetMember f : lstFSMember) {
	        query += f.getFieldPath() + ', ';
            }
            query += 'Id FROM ' + strObjectApiName ;

            //Just in case criteria field not specified then it will return all records
            if(!(String.isEmpty(criteriaField) && String.isEmpty(criteriaFieldValue))){
                query += ' WHERE ' + criteriaField + '=\'' + criteriaFieldValue + '\'';
            }
                        
	    //execute query
             List<SObject> lstRecords = Database.query(query);
            
             //prepare a map which will hold fieldList and recordList and return it
	     returnMap.put('FIELD_LIST', JSON.serialize(lstFSMember));
	     returnMap.put('RECORD_LIST', JSON.serialize(lstRecords));
	     return JSON.serialize(returnMap);
        }
        return null;
    }
}

Final Step

After building the component, place it on Record Page through App Builder and define attribute values.



You can see that component is displaying fields and records.

This concept can be leveraged easily at any project. The component can be improvised more like, handling events, based on Salesforce field type Datatable column types can be defined, it could be good to incorporate paginations etc, but it is a good start.

We are done and thanks for reading.



Further Reading