Support Ukraine 🇺🇦 Help Provide Humanitarian Aid to Ukraine.
For Administrators

Fast Inline Editing Lists with LWC and Conga Grid

6 min read

We had a client request to let users inline edit multiple records from list views and related lists in Salesforce. A few requirements we had:

  • Display more than 15 columns in list views or 10 columns in related list views
  • Display links to related records under a column
  • Mass update a field for multiple records
  • Mass update multiple fields for multiple records

After exploring multiple tools, we narrowed down to Conga Grid, which met all the requirements out of the box except one. Conga Grid doesn’t allow mass updating of relationship fields that have lookup filters. But it has a nice feature, called Custom Actions, which allows you to add user-defined buttons or actions. In this article, we will learn about the solution we created to open Lightning Web Components from Conga Grid custom actions and mass update fields with lookup filters.

Here’s an example of what it looks like:

Quick demo of Conga Grid with a Custom Lookup

Add Conga Grid Custom Action via Static Resource

Conga Grid Custom Action follows the following structure. We use addCustomAction method from crmc global variable to add custom action. Couple of key things to note here are:
1. isAvailable function: It is called to decide when to display the custom action. We can add our custom logic here to control when the action shows up.

2. click function: It is called when you click the custom action. This is where all the magic happens. We use window.parent.postMessage function to send a message to parent window. We will handle this method in an Aura component to listen to message and pass it on to a Lightning Web Component.

crmc.require(['sfdc', 'KendoPopup'], function (sfdc, popup) { 
    crmc.addCustomAction({
        // Unique identifier for this action item
        "itemID": "Open_Lightning_Modal",
        "isAvailable": function (context) { 
            // Return true to display the item, false to hide it
            // check to ensure this is correct object and feature is enabled
            var isAccount = context.objectDescribe.name == "Account"; 
            var isEnabled = this.featureSecurity.getSetting(context.objectDescribe.name, this.itemID) !== false; 
            // check to ensure at least one record is selected
            var multipleSelected = context.selectedRows && context.selectedRows.length > 0; 
            return isAccount && isEnabled && multipleSelected; 
        },
        "isHeaderAvailable": function(context) {
            // This function determines if this item can be displayed from the column header menu
            return false;
        },
        "isToolbarAvailable": function(context) {
            // This function determines if this item can be displayed in the Toolbar as a button
            return false;
        },
        "getLabel": function (context) { 
            // This function returns the display label of the action item and is called before the item is shown 
            return "Open Lightning Modal"; 
        },
        "createSubmenuItems": function (context) { 
            // If this function returns additional action item objects, they will appear as submenu items 
            return []; 
        },
        "click": function (context, grid) { 
            if (context.selectedRows.length > 0) {
                let rowIds = [];
                Object.each(context.selectedRows, function (selectedRow) {
                    rowIds.push(selectedRow["Id"]);
                });
                window.parent.postMessage(
                    {
                        action: 'OPEN_MODAL',
                        recordIds: rowIds,
                        message: 'Hello World'
                    },
                    '*'
                );
            } else { 
                popup.popup('Please select records', 'Select at least one record to proceed.'); 
            }
        }
    });
});

Add a Modal using Lightning Web Component

We create a Lightning Web Component using the LightningModal element. This component has all the functionality which we want to perform on the selected records. We can custom code all the requirements in this component, but, for this example I’m just adding the boilerplate needed for end-to-end demo. Here are code snippets from modalExample.html, modalExample.js and modalExample.js-meta.xml files for modalExample Lightning Web Component.

<template>
    <lightning-modal-header label="Conga Grid Action"></lightning-modal-header>
    <lightning-modal-body>
        <p>{message}</p>
        <p>{recordIds}</p>
    </lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Close" onclick={handleClose}></lightning-button>
    </lightning-modal-footer>
</template>
import LightningModal from 'lightning/modal';
import { api } from 'lwc';

export default class ModalExample extends LightningModal {

    @api recordIds;
    @api message;
    
    handleClose() {
        this.close('closed');
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>61.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

Add Lightning Web Component to open Lightning Modal

We create a Lightning Web Component, let us with name congaModalListerner, which will expose an @api method to open the Lightning Modal component which we created in the previous step. Here is how that component looks:

<!-- this component's template has no markup -->
<template></template>
import { LightningElement, api } from "lwc";
import ModalExample from "c/modalExample";

export default class CongaModalListener extends LightningElement {

  @api
  async openModalFromVF(data) {
    const { message, recordIds, action } = data;

    if (!message) {
      console.error(
        "CongaModalListener: No message sent for modal."
      );
      return;
    }

    let result;
    if (action === "OPEN_MODAL") {
      result = await ModalExample.open({
        size: "large",
        description: "Hello World Modal",
        recordIds: recordIds,
        message: message
      });
    }

    if (result === "ok") {
      // TODO: we can display result using toast event
    } else {
      // TODO: we can display result using toast event
    }
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>61.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

Add an Aura Component with Window Message Listener

We create an aura component, named CongaGridModalBridge, and add a “message” event listener to the window object. When a message event is received on the window, this component will find a reference to the congaGridListener component and call openModalFromVF api method from it. Here is what constitutes this component:

<!-- markup for CongaGridModalBridge.cmp -->
<aura:component implements="flexipage:availableForAllPageTypes" access="global">

    <!-- add the Lightning web component created in the previous step -->
    <c:congaModalListener aura:id="modalHandler"/>

    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
</aura:component>
// JavaScript for CongaGridModalBridgeController.js
({
    doInit: function (component, event, helper) {
        window.addEventListener("message", function(event) {
            // Optional: restrict by origin
            // if (event.origin !== 'https://yourdomain.my.salesforce.com') return;
            if (event.data && event.data.action === 'OPEN_MODAL') {
                const modalHandler = component.find("modalHandler");
                if (modalHandler) {
                    modalHandler.openModalFromVF(event.data);
                }
            }
        }, false);
    }
})

Putting Everything Together

Now, we are ready to put all the pieces together. We create a Lightning Page, add the Visualforce page that has the CRMC_PP:Grid component to this Lightning Page and then add the CongaGridModalBridge to this page. Save the page and reload page, it should look something like below:

Lightning Page with Visualforce Page and Aura Component

Conclusion

By using Lightning Components with Conga Grid we can bridge the gap between complex data presentation and a seamless user experience. This approach empowers developers to add custom business logic maintaining separation of concerns while using the powerful features available in Conga Grid. Whether you’re modernizing existing Visualforce implementations or building new Lightning-first applications, this pattern provides a flexible and scalable way to deliver enterprise-grade data grids in Salesforce.


About CloudAnswers

Salesforce apps, powerful components, custom development, and consulting. Our experienced team helps you to create and modify workflow processes in salesforce.

Discover more from CloudAnswers

Subscribe now to keep reading and get access to the full archive.

Continue reading