Deep Clone using Headless Actions in LWC

A headless quick action executes custom code in a Lightning web component. Unlike a screen action, a headless action doesn’t open a modal window. So in short, if you want to do some custom logic via apex to run by click of a quick action button we can use headless actions in LWC.

Below are a few examples/use cases where we can implement headless actions.
1. A custom clone button (With Deep Clone)
2. A custom approval button.
3. Submit data to an external system.
4. Enrich the record with details from an external system etc.

Configure a Component for Quick Actions

To use a Lightning web component as a quick action, define the component’s metadata. Specifically to use an LWC as headless action use the below XML for the meta XML file. Note that the <actiontype is Action for a headless quick action and if you would like to enable the component as a screen popup (regular) include actionType as ScreenAction.

<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
   <apiVersion>52.0</apiVersion>
   <isExposed>true</isExposed>
   <targets>
       <target>lightning__RecordAction</target>
   </targets>
    <targetConfigs>
   <targetConfig targets="lightning__RecordAction">
     <actionType>Action</actionType>
   </targetConfig>
 </targetConfigs>
</LightningComponentBundle>

Implementation

Now that we have the setup ready, the custom logic that you need to implement must be provided with the js file of the LWC. The HTML file could be empty with just the template tags.
In your Lightning web component, expose invoke() as a public method. The invoke() method executes every time the quick action is triggered.

import { LightningElement, api } from "lwc";

export default class HeadlessSimple extends LightningElement {
  @api invoke() {
    console.log("This is a headless action.");
  }
}

Example Scenario

So now if you want to use a deep clone logic from the quick action button the code looks like this.

import { LightningElement, api } from "lwc";
import { ShowToastEvent } from 'lightning/platformShowToastEvent'
import { NavigationMixin } from 'lightning/navigation';
import startDeepClone from '@salesforce/apex/ROR_CloneController.startDeepClone';

export default class Ror_deepcloneaction extends NavigationMixin(LightningElement) {

    @api invoke() {
        this.startToast('Deep Clone!','Starting cloning process...');
        //Call the cloning imperative apex js method
        this.startCloning();
    }
    
    startCloning(){
        startDeepClone({recordId: this.recordId})
        .then(result => {
            this.startToast('Deep Clone!','Cloning Process Completed');
            this.navigateToRecord(result);
         })
         .catch(error => {
            this.startToast('Deep Clone!','An Error occured during cloning'+error);
         });
    }

    startToast(title,msg){
        let event = new ShowToastEvent({
            title: title,
            message: msg,
        });
        this.dispatchEvent(event);
    }

    navigateToRecord(clonedRecId){
        this[NavigationMixin.Navigate]({
            type: 'standard__recordPage',
            attributes: {
                recordId: clonedRecId,
                actionName: 'view',
            },
        });
    }

}

You can see that on click of the quick action button, the invoke method gets executed, that in turn calls the imperative apex call; does the clone; returns the new clone record’s id and on a successful return from the apex, use an event to alert the user and use NavigationMixin to redirect the user to the cloned record.

Please access the code from the Github repo here.

Aura’s Helper Equivalent in LWC

Back in time when we were creating Lightning components on aura framework, developers were hooked on with the helper methods. We all were told that all the reusable code should go into helper methods. However when we moved to LWC development, with no helper javscript file, where do we put all these reusable code? Let is take a look.

Reusable helpers in LWC

In LWC, we just have one javascript file, so it is necessary to have all the reusable code written within this file. Lets take a quick example of how we can call a method that fires toast message. This method we’ll make generic and try to refer from multiple places. Also I’ve added how to use a pattern matching method as well.

import { LightningElement} from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class MyClass extends LightningElement {
    searchTerm = null;
    //Function for maanging toast messages
    showToast(titleMsg,description,variant) {
        const event = new ShowToastEvent({
            title: titleMsg,
            message: description,
            variant : variant
        });
        this.dispatchEvent(event);
    }
    checkNumeric(inputStr){
        let pattern = /^([^0-9]*)$/;
        if(!inputStr.match(pattern)){
            return true;
        }
        else {
            return false;
        }
    }
    validateParam(searchTerm) {
        let searchTermLen = searchTerm.length;
        if (searchTermLen === 0 || searchTermLen > 3) {
            this.showToast('Error','Search Term from must have minimum 1 and maximum 3 characters', 'error');
        }
        else if (searchTermLen === 1) {
            if(this.checkNumeric(searchTerm)){
                //Some Logic
            }
            else {
                this.showToast('Error','First Character of Search starts with Number', 'error');
            }
        }
    }
    //The handler that will executed from the click of search button
    handleSearchClick(event) {
        this.validateParam(this.searchTerm);
            // Do all your logic below
        }
}

From the above its evident that on handleSearchClick(), we call the validateParam() method. From the validateParam() method we are calling the showToast() method by passing parameters. The showToast() method based on its input parameter will render the toast message on the UI.

‘This’ – The magic word!

The key to call reusable code is the use of ‘this‘ keyword. Functions, in JavaScript, are essentially objects. Like objects they can be assigned to variables, passed to other functions and returned from functions. And much like objects, they have their own properties. One of these properties is ‘this‘.

The value that ‘this‘ stores is the current execution context of the JavaScript program. Thus, when used inside a function this‘s value will change depending on how that function is defined, how it is invoked and the default execution context.

Dynamic Chart in LWC With Multiple Datasets Using ChartJS

Data visualization is the graphical representation of information and data. By using visual elements like charts, graphs, and maps, data visualization tools provide an accessible way to see and understand trends, outliers, and patterns in data. Executives and Managers in any firm are interested to visualize the data in a way that would provide insights to them. One such visualization library that is very popular and open source is ChartJS. It is a simple yet flexible JavaScript charting for designers & developers. In this blog, let us take a look on how we could use ChartJs to draw charts on a lightning web component.

In the blog, I will demonstrate how you can get data from salesforce object using apex and feed it to the bar chart. Also, will use an aggregate query to get the data summed up and will show data in two datasets. To build the UI, I’ve used two LWCs. One being a parent and another its child.

Most of the data work will be handled by the parent component and will pass as an attribute (chartConfiguration) to the child. In this blog, we are trying to show an Opportunity Bar Chart with Expected Revenue & Amount for various stages.

  1. Download the ChartJS file from here and load it as a static resource with the name ‘ChartJS’.
  2. Create a Lightning Web Component with name ‘gen_barchart’ copy the below code.

gen_barchart.js

import { LightningElement, api } from 'lwc';
import chartjs from '@salesforce/resourceUrl/ChartJs';
import { loadScript } from 'lightning/platformResourceLoader';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class Gen_barchart extends LightningElement {
    @api chartConfig;

    isChartJsInitialized;
    renderedCallback() {
        if (this.isChartJsInitialized) {
            return;
        }
        // load chartjs from the static resource
        Promise.all([loadScript(this, chartjs)])
            .then(() => {
                this.isChartJsInitialized = true;
                const ctx = this.template.querySelector('canvas.barChart').getContext('2d');
                this.chart = new window.Chart(ctx, JSON.parse(JSON.stringify(this.chartConfig)));
            })
            .catch(error => {
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Error loading Chart',
                        message: error.message,
                        variant: 'error',
                    })
                );
            });
    }
}

I have used platformResourceLoader to load the script from the static resource on a renderedCallback() lifecycle hook.

gen_barchart.html

<template>
    <div class="slds-p-around_small slds-grid slds-grid--vertical-align-center slds-grid--align-center">
        <canvas class="barChart" lwc:dom="manual"></canvas>
        <div if:false={isChartJsInitialized} class="slds-col--padded slds-size--1-of-1">
            <lightning-spinner alternative-text="Loading" size="medium" variant="base"></lightning-spinner>
        </div>
    </div>
</template>

In the HTML I have added a canvas tag, as per the ChartJS documentation the Chartjs library uses that canvas to draw the chart.

3. Create an apex class to pull the data from salesforce. In this example I’ve used a SOQL to pull data from Opportunity and aggregated the the Amount and ExpectedRevenue field.

GEN_ChartController.cls

public class GEN_ChartController {
    @AuraEnabled(cacheable=true)
    public static List<AggregateResult> getOpportunities(){
        return [SELECT SUM(ExpectedRevenue) expectRevenue, SUM(Amount) amount, StageName stage 
               FROM Opportunity WHERE StageName NOT IN ('Closed Won') GROUP BY StageName LIMIT 20];
    }
}

4. Create another LWC as the parent that does the data work for us.

gen_opportunitychart.js

import { LightningElement, wire } from 'lwc';
import getOpportunities from '@salesforce/apex/GEN_ChartController.getOpportunities';

export default class Gen_opportunitychart extends LightningElement {
    chartConfiguration;

    @wire(getOpportunities)
    getOpportunities({ error, data }) {
        if (error) {
            this.error = error;
            this.chartConfiguration = undefined;
        } else if (data) {
            let chartAmtData = [];
            let chartRevData = [];
            let chartLabel = [];
            data.forEach(opp => {
                chartAmtData.push(opp.amount);
                chartRevData.push(opp.expectRevenue);
                chartLabel.push(opp.stage);
            });

            this.chartConfiguration = {
                type: 'bar',
                data: {
                    datasets: [{
                            label: 'Amount',
                            backgroundColor: "green",
                            data: chartAmtData,
                        },
                        {
                            label: 'Expected Revenue',
                            backgroundColor: "orange",
                            data: chartRevData,
                        },
                    ],
                    labels: chartLabel,
                },
                options: {},
            };
            console.log('data => ', data);
            this.error = undefined;
        }
    }
}

From the above block of code, you can see the multiple elements in the dataset. This is how you keep adding dataset to show on the chart. The chartconfiguration as provided here defines the type of chart, data, labels and any options.

gen_opportunitychart.html

<template>
    <lightning-card title="Open Opportunities" icon-name="utility:chart">
        <template if:true={chartConfiguration}>
            <c-gen_barchart chart-config={chartConfiguration}></c-gen_barchart>
        </template>
</lightning-card>
</template>

gen_opportunitychart.js-meta.xml

<?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__Tab</target>asasas
        <target>lightning__AppPage</target>
    </targets>
</LightningComponentBundle>

5. Create a Lightning App page to preview the chart.

The chart will be rendered as shown below:

You can download the files from the github repo here.

#Giveback #StaySafe

Identify Source of a Lightning Web Component (LWC)

Scenario

Many times we may get a scenario where a lwc will be placed in community for external users and also in the app builder so that its used by internal users. Let us see how we can identify from where the component is being accessed now so that we can conditionally render the design/css.

We’ll make use of the lifecycle hook connectedCallback() to pull this information from an apex to the LWC’s javascript.

Sample Code

identifysource.html

<template>
    {isincommunity}
</template>

identifysource.js

import { LightningElement,track } from 'lwc';
import checkPortalType from '@salesforce/apex/IdentifySource.checkPortalType';
export default class Identifysource extends LightningElement {
	@track isincommunity;
	connectedCallback(){
        checkPortalType()
            .then(result => {
                var isInPortal = result === 'Theme3' ? true : false;
                //setting tracked property value
                this.isincommunity = isInPortal;
            })
            .catch(error => {
                this.error = error;
        });
    }
}

IdentifySource.cls

public with sharing class IdentifySource {
    @AuraEnabled
    public static String checkPortalType() {
        return UserInfo.getUiThemeDisplayed();
    }
}
Background

If you are from the Aura background, you could relate the connectedCallback() with the doInit(). If you want to perform any logic before the element is rendered, you can add that to the connectedCallback() method. The connectedCallback() lifecycle hook fires when a component is inserted into the DOM. connectedCallback() in Lightning Web Component flows from parent to child. So we call the apex from the connectedCallback() method.

Now on the apex side, we have the UserInfo class that retrieves the UI Theme of the logged in user. This way we can identify on which theme the user has logged in. Below data shows the output of the getUiThemeDisplayed() method.

  • Theme1—Obsolete Salesforce theme
  • Theme2—Salesforce Classic 2005 user interface theme
  • Theme3—Salesforce Classic 2010 user interface theme
  • Theme4d—Modern “Lightning Experience” Salesforce theme
  • Theme4t—Salesforce mobile app theme
  • Theme4u—Lightning Console theme
  • PortalDefault—Salesforce Customer Portal theme
  • Webstore—Salesforce AppExchange theme

Blog at WordPress.com.

Up ↑