Showing posts with label Lightning Web Components. Show all posts
Showing posts with label Lightning Web Components. Show all posts

Saturday, June 29, 2024

Salesforce Lightning Web Components Cheat Sheet - 2nd Edition

 

Motivation behind this


Salesforce has expanded features of Lightning Web Components and almost after 5 years I am trying to incorporate all the features into my cheat sheet.

One of the trailblazers motivated me last month on the same and thought of updating my own way.

I have prepared 12 pager cheat sheet which covers almost all most important features and functionalities. (To download entire document scroll through the bottom).





Topics Included


The cheat covers following areas:
  • Features & Advantages
  • Getting Started
  • Component Bundles & Rules
  • Reactivity
  • Composition
  • Dynamic Components
  • Working with DOM
  • Communicate with Events
  • Working with Salesforce Data
  • Aura-Component Co-existence
  • Lightning Web Components Best Practices (Final 2 pages)

Snapshots


Here are few snaps of the content.

Page 1

Page 6

Page 7

Page 10

There are 2 more pages on best practices.

Click on the below link to download.


Download entire cheat sheet

If you find this useful then post your feedback as comments and share it.

Note: Since I love all those astro, codey etc characters that's why I have used them in the document.

Further Reading


Friday, February 4, 2022

Display data from CMS using Lightning Web Component

 

Motivation behind this


We know that from B2B Commerce Lightning we can use readymade Lightning Web Components available through B2B Lex and use it for many purposes like Product Detail Card, Product Detail List, Cart Component, Order Summary etc. at Experience Cloud.
Refer B2B Commerce on Lightning Experience Components and it also fetches data from CMS.

What if I have to build similar LWC component which can be placed in Lightning RecordDetail page for internal users and data to be retrieved from CMS. This drives me to do a proof-of-concept and sharing you the steps to develop.





Use Case


Business wants to see the list of featured products for internal users and those product images are placed at CMS.

Developer wants to build LWC component to fetch records from CMS.

For sake of simplicity, those images will be displayed using lightning-carousel component.

Solution Approach


To start with, lets configure CMS and store images on CMS side.

Create CMS Workspace

From CMS Home Tab, Create CMS Workspace. Here given a name as Capricorn Store. Screen as follows:


Create a Channel

From Capricorn Store workspace, create a channel naming Capricorn Channel.




Add Content

Add 3 product related pictures as follows. For demo purpose I have used Coffee Machines pictures.


Sample product image and details will look like this:


Now let's create LWC component.

displayMediaFilesFromCMS.html

HTML will like below here lightning-carousel has been used.

<template>
    <lightning-card  title="Featured Products">
<div class="slds-medium-size_1-of-4 slds-align_absolute-center"> <lightning-carousel>
<template for:each={results} for:item="item"> <lightning-carousel-image key={item.title} src= {item.url} header= {item.title} href= {item.url}> </lightning-carousel-image>
</template> </lightning-carousel>
</div> </lightning-card>
</template>

displayMediaFilesFromCMS.js

Few notable things as follows:
  • Channel Name to be passed to Apex class's  retrieveMediaFromCMS() method using wire. For sake of simplicity channel name is hardcoded, it can be captured from design attributes too and using @api variable.
  • Loop through the array returned from Apex method and create new array with title and url as elements which will be used by carousel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { LightningElement, track, wire } from 'lwc';
import retrieveMediaFromCMS from '@salesforce/apex/CMSConnectHelper.retrieveMediaFromCMS';

export default class DisplayMediaFilesFromCMS extends LightningElement {
    channelName = 'Capricorn Channel';
    @track results=[];
    @wire(retrieveMediaFromCMS,{channelName: '$channelName'})
    wiredData({ error, data }) {
        if (data) {
            let objStr = JSON.parse(data);
            objStr.map(element=>{
                this.results = [...this.results,{title:element.title,
                                                url:element.url}]
            });  
            this.error = undefined;            
        } else if (error) {
            this.error = error;
            this.results = undefined;
        }
    }
}


CMSConnectHelper.cls


Salesforce provides ConnectApi.ManagedContent class and its method to retrieve data from CMS. No need to fetch data using REST APIs. Refer documentation

 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
/**
 * @description       : This class is used to retrieve data from CMS
 * @author            : Santanu Boral
 * 
**/
public with sharing class CMSConnectHelper {
    
    @AuraEnabled (cacheable=true)
    public static String retrieveMediaFromCMS(String channelName){
        String channelId = getChannelId(channelName);

        //get the image content
        ConnectApi.ManagedContentVersionCollection obj = 
            ConnectApi.ManagedContent.getAllContent(channelId, 0, 5, 'en_US', 
                                                    'cms_image',false,
                                                    '2011-02-25T18:24:31.000Z','2021-09-25T18:24:31.000Z',true);
        
        List<ReturnWrapper> wrapperList = new List<ReturnWrapper>();
        System.debug('json value=' + JSON.serialize(obj));

        //loop through each item and prepare a wrapper list
        for(ConnectApi.ManagedContentVersion versionObj: obj.items){
            ReturnWrapper wrapper = new ReturnWrapper();
            wrapper.title = versionObj.title;
            
            //get the url
            Map<String,ConnectApi.ManagedContentNodeValue> contentNodesMap = versionObj.contentNodes;
            for(String str:contentNodesMap.keySet()){                
                if(str=='source'){
                    wrapper.url= ((ConnectApi.ManagedContentMediaSourceNodeValue)contentNodesMap.get(str)).url;
                }		
            }
            wrapperList.add(wrapper);	
        }
        return JSON.serialize(wrapperList);
    }

    @AuraEnabled (cacheable=true)
    public static String getChannelId(String channelName){
        ConnectApi.ManagedContentChannelCollection channelRepObj = 
                ConnectApi.ManagedContent.getAllDeliveryChannels(0,2);        

        //loop through the channels and return the channel Id
        for(ConnectApi.ManagedContentChannel channelObj: channelRepObj.channels){
            if(channelObj.channelName == channelName){
                return channelObj.channelId;
            }
        }
        return null;
    }

    public class ReturnWrapper{
        String title {get;set;}
        String url {get;set;}
    }
}

getAllContent() returns a JSON as follows which needs to be parsed to form title and url combination for each item elements.

 
  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
{
  "currentPageUrl": "/services/data/v51.0/connect/cms/delivery/channels/0ap2v000000Pu1w/contents/query?endDate=2021-09-25T18%3A24%3A31.000Z&language=en_US&managedContentType=cms_image&page=0&pageSize=5&startDate=2011-02-25T18%3A24%3A31.000Z",
  "items": [
    {
      "contentKey": "MCR6FJVMX4BRHSVJD4LPWUPPN3OI",
      "contentNodes": {
        "thumbUrl": {
          "nodeType": "Url",
          "value": "https://live.staticflickr.com/65535/49815666003_4973d84842.jpg"
        },
        "title": {
          "nodeType": "NameField",
          "value": "TR-COFMAC-001"
        },
        "source": {
          "fileName": null,
          "isExternal": true,
          "mediaType": "Image",
          "mimeType": null,
          "nodeType": "MediaSource",
          "referenceId": null,
          "resourceUrl": null,
          "unauthenticatedUrl": null,
          "url": "https://live.staticflickr.com/65535/49815666003_4973d84842.jpg"
        }
      },
      "contentUrlName": "tr-cofmac-001",
      "language": "en_US",
      "managedContentId": "20Y2v000000HbISEA0",
      "publishedDate": "2021-05-09T16:32:44.000Z",
      "title": "TR-COFMAC-001",
      "type": "cms_image",
      "typeLabel": "Image",
      "unauthenticatedUrl": "https://myinstance.my.salesforce.com/cms/delivery/v51.0/0ap2v000000Pu1wAAC/contents/20Y2v000000HbISEA0?oid=00D2v000002A3VoEAK"
    },
    {
      "contentKey": "MCSC6EEJVMZZB5HDHRKMTEQH6TIQ",
      "contentNodes": {
        "thumbUrl": {
          "nodeType": "Url",
          "value": "https://live.staticflickr.com/65535/49815637818_d892f8127d.jpg"
        },
        "title": {
          "nodeType": "NameField",
          "value": "E-ESP-001-1"
        },
        "source": {
          "fileName": null,
          "isExternal": true,
          "mediaType": "Image",
          "mimeType": null,
          "nodeType": "MediaSource",
          "referenceId": null,
          "resourceUrl": null,
          "unauthenticatedUrl": null,
          "url": "https://live.staticflickr.com/65535/49815637818_d892f8127d.jpg"
        }
      },
      "contentUrlName": "e-esp-001-1",
      "language": "en_US",
      "managedContentId": "20Y2v000000HbIIEA0",
      "publishedDate": "2021-05-09T16:32:06.000Z",
      "title": "E-ESP-001-1",
      "type": "cms_image",
      "typeLabel": "Image",
      "unauthenticatedUrl": "https://myinstance.my.salesforce.com/cms/delivery/v51.0/0ap2v000000Pu1wAAC/contents/20Y2v000000HbIIEA0?oid=00D2v000002A3VoEAK"
    },
    {
      "contentKey": "MCKPXLBGW36RDYXN75NDWFVTB5FA",
      "contentNodes": {
        "thumbUrl": {
          "nodeType": "Url",
          "value": "https://live.staticflickr.com/65535/49816090811_622af115a8.jpg"
        },
        "title": {
          "nodeType": "NameField",
          "value": "B-C-COFMAC-001"
        },
        "source": {
          "fileName": null,
          "isExternal": true,
          "mediaType": "Image",
          "mimeType": null,
          "nodeType": "MediaSource",
          "referenceId": null,
          "resourceUrl": null,
          "unauthenticatedUrl": null,
          "url": "https://live.staticflickr.com/65535/49816090811_622af115a8.jpg"
        }
      },
      "contentUrlName": "b-c-cofmac-001",
      "language": "en_US",
      "managedContentId": "20Y2v000000HbIDEA0",
      "publishedDate": "2021-05-09T16:28:55.000Z",
      "title": "B-C-COFMAC-001",
      "type": "cms_image",
      "typeLabel": "Image",
      "unauthenticatedUrl": "https://myinstance.my.salesforce.com/cms/delivery/v51.0/0ap2v000000Pu1wAAC/contents/20Y2v000000HbIDEA0?oid=00D2v000002A3VoEAK"
    }
  ],
  "total": 3
}

Finally, Create a Lightning App Builder page and place the component, it will look like this:


We are done and thanks for reading.

You can also extend this code to expose this component to Experience Cloud with some minor changes.


Further Reading


Thursday, July 15, 2021

Communicating Change Data Capture (CDC) with Lightning Web Component

 

Motivation behind this



I have been looking for a real-life use case on Change Data Capture which drives me to build a quick poc and writing this post.

So, lets get started!




Use Case


Business has a requirement to approve or reject a record when anyone of the approvers approves/rejects the request. When a record is submitted for approval there can be multiple approvers in the queue and anyone can take necessary action. There is a possibility that, multiple users can open the record for approval at the same time and the approver needs to spend time reading each items minutely, putting comments and so on. It might take some time to approve.

In the meantime, other approver can approve the same request which current user may not know that the record is stale. Normally, during approving/rejecting user can be alerted, but this is not a good user experience as he has to spend time on completing the form.

Business wants a pro-active alert on the screen when the record status gets changed without refreshing the screen.

You can also think about similar use case for seat reservation etc.

Possible End Results


After building the use case, it will perform the functionality as following screen. When record gets changed, the alert will be shown pro-actively as below without refreshing the screen. Also, clicking on refresh icon, record will be refreshed instantly. Don't miss the video at the end.


Solution Approach


It's a great way to develop this solution using CDC with Lightning Web Component.

Flow Diagram


Create a flow diagram like this way to understand the functionalities, flow of control to meet the requirement.



To learn about Change Data Capture visit trailhead module on Change Data Capture Basics.

All custom objects and few Standard Objects like  Account, Contact, Lead, User, Order, OrderItem, Product2, and others supports CDC.

Configuring CDC


Let's start with creating an object and configure CDC. Here for the sake of poc, I have created a custom object called Financial with few attributes as shown in the first picture.

Then though setup, Change Data Capture menu, assign Financial object for CDC as follows:





Event payload structure


Event message generates following structure, notice changeType, entityName and channelName. In this poc, only status field is changed.



{
    "data": {
        "schema": "WnzuUutBoNEU0Qbh60KQRg",
        "payload": {
            "LastModifiedDate": "2021-07-04T18:34:04Z",
            "ChangeEventHeader": {
                "commitNumber": 11105108170443,
                "commitUser": "0052v00000dRntzAAC",
                "sequenceNumber": 1,
                "entityName": "Financial__c",
                "changeType": "UPDATE",
                "changedFields": [
                    "LastModifiedDate",
                    "Status__c"
                ],
                "changeOrigin": "com/salesforce/api/soap/52.0;client=SfdcInternalAPI/",
                "transactionKey": "000282f7-ee3a-ad08-3fd0-29b2549ab4a8",
                "commitTimestamp": 1625423644000,
                "recordIds": [
                    "a072v00001UkmEKAAZ"
                ]
            },
            "Status__c": "Approved"
        },
        "event": {
            "replayId": 7805409
        }
    },
    "channel": "/data/Financial__ChangeEvent"
}


Lightning Web Component as Subscriber


changeDataNotificationComponent.html:

This UI displays notification on the screen.


<!--
  @description : This component shows the message when current record changes.
  @author      : Santanu Boral
-->
<template>
    <lightning-card title="Change Data Capture with EmpApi" icon-name="custom:custom14">
        <template if:true = {isDisplayMsg}>
        <div class="slds-notify slds-notify_alert slds-alert_warning" role="alert">
            <span class="slds-assistive-text">warning</span>
            <lightning-icon icon-name="utility:warning" alternative-text="Warning!" title="Warning">                
            </lightning-icon>
<h2>{responseMessage} &nbsp; <lightning-button-icon icon-name="utility:refresh" variant="bare" alternative-text="refresh" title="refresh" onclick={handleRefresh}> </lightning-button-icon>
</h2> </div> </template> </lightning-card>
</template>

changeDataNotificationComponent.js:

Few notable points in this js file:
  • Importing lightning/empApi for subscribing and unsubscribing.
  • Use of arrow function so that class level method (handleNotification) can be called using this operator.

const messageCallback = (response) => {
            console.log('New message received: ', JSON.stringify(response));
            // Response contains the payload of the new message received
            this.handleNotification(response);
        };
  • handleNotification checks if current record is updated and shows the message.
  • refreshing the record using getRecordNotifyChange of lightning/uiRecordApi, which is efficient way of refreshing the record. Isn't it?
Entire js file 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { LightningElement,api } from 'lwc';
import { subscribe, unsubscribe, onError, setDebugFlag, isEmpEnabled } from 'lightning/empApi';
import { getRecordNotifyChange } from 'lightning/uiRecordApi';

export default class ChangeDataNotificationComponent extends LightningElement {
    @api recordId;
    @api channelName; //'/data/Financial__ChangeEvent'

    subscription = {}; //subscription information
    responseMessage; //message to be shown at UI
    isDisplayMsg; //indicator for message to be displayed

    // Initializes the component
    connectedCallback() {       
        this.handleSubscribe();
        // Register error listener       
        this.registerErrorListener();   
        this.isDisplayMsg = false;   
    }

    // Handles subscribing
    handleSubscribe() {
        // Callback invoked whenever a new event message is received
        const messageCallback = (response) => {
            console.log('New message received: ', JSON.stringify(response));
            // Response contains the payload of the new message received
            this.handleNotification(response);
        };

        // Invoke subscribe method of empApi. Pass reference to messageCallback
        subscribe(this.channelName, -1, messageCallback).then(response => {
            // Response contains the subscription information on subscribe call
            console.log('Subscription request sent to: ', JSON.stringify(response.channel));
            this.subscription = response;
            this.handleNotification(response);
        });
    }

    // Handles unsubscribing
    handleUnsubscribe() {
        // Invoke unsubscribe method of empApi
        unsubscribe(this.subscription, response => {
            console.log('unsubscribe() response: ', JSON.stringify(response));
            // Response is true for successful unsubscribe
        });
    }


    registerErrorListener() {
        // Invoke onError empApi method
        onError(error => {
            console.log('Received error from server: ', JSON.stringify(error));
            // Error contains the server-side error
        });
    }

    //this method checks if current record got updated and shows message on UI
    handleNotification(response){
        if(response.hasOwnProperty('data')){
            let jsonObj = response.data;
            
            if(jsonObj.hasOwnProperty('payload')){
                let payload = response.data.payload;
                let recordIds = payload.ChangeEventHeader.recordIds;

                //find the current recordId in the array and if found then display message
                const recId = recordIds.find(element=> element == this.recordId);
                if(recId !=undefined){
                    this.isDisplayMsg = true;
                    this.responseMessage = 'Current record has changed, please refresh the page';
                }
            }
        }
    }

    //this method refreshes current record page
    handleRefresh(){
        getRecordNotifyChange([{recordId: this.recordId}]);
        this.isDisplayMsg = false;
    }
}

In the meta.xml create channelName as property which can be used to set design attribute and assign '/data/Financial__ChangeEvent' when placing this component on the record detail page.

Let's Test this!


Refer following video to simulate the functionality. Here the record has been opened in a record detail page and in a separate tab same record is edited from List View. The alert message will be displayed on record detail page.


Finally we are done and thanks for reading.

References

Further Reading