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