Unleash the Power of Lightning-Datatable in Salesforce LWC Development
Example of an LWC component to search account's related contacts by name or email

Unleash the Power of Lightning-Datatable in Salesforce LWC Development

(2/18/25 Security section updated to reflect secure dynamic SOQL for this use case)

When developing Salesforce Lightning Web Components (LWCs), a common requirement is to display data in a tabular format. Traditionally, crafting tables required significant effort in HTML for layout design and in JavaScript for data handling. However, the introduction of lightning-datatable has revolutionized this process, providing developers with a powerful, flexible, and declarative approach to creating tables.

In this blog post, I will demonstrate how lightning-datatable eliminates the need for manually laying out HTML tables and allows developers to manage the table configuration entirely through JavaScript. We'll also explore how I set up a dynamic and feature-rich datatable in a recent project.


Github Repo

https://github.com/ShinobiSec/lwc-lightning-datatable

What is lightning-datatable?

lightning-datatable is a built-in Lightning component that renders tabular data in a user-friendly and responsive format. It comes with features such as:

  • Column definitions with sortable and wrapped text options
  • Custom rendering for cells
  • Infinite scrolling for large datasets
  • Easy integration with Salesforce data

Example Use Case: Record History Component

In a recent project, I created a "Record History App" to display record and user history for any object in Salesforce. Here’s how I implemented one of the front-end components using lightning-datatable in an LWC.

Step 1: Create an Apex Class to Provide Data to the LWC Component

Our first task is to generate a controller class that will be used to provide data to our LWC component.

Don’t worry, I know this might look overwhelming if you're just beginning your apex programming journey. All you need to know is we have set up a way to dynamically perform a SOQL query using multiple inputs at the same time.

For this post I am focusing on the magic of using a lightning-datatable in an LWC.

public with sharing class LightningDatatableController {

    // @AuraEnabled allows the class to be access by your LWC component using import in the JS file.
    @AuraEnabled(cacheable=True)
    // We will be returning contacts in this example
    public static List<Contact> getContacts(Id accountId, String name, String email, Integer rows) 
    {
        // Create a list of contacts to be returned
        List<Contact> contacts = new List<Contact>();

    //Create map for query with binds
        Map<String, Object> params = new Map<String, Object>{'accountId' => accountId, 'name' => '%' + name + '%', 'email' => '%' + email + '%', 'rows' => rows};
        
        //Perform query based on existing params
        
        if(accountId != NULL && string.isNotBlank(name) && string.isNotBlank(email)){
            String query = 'SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId AND Name LIKE :name AND Email LIKE :email LIMIT :rows';
            contacts = Database.queryWithBinds(query, params, AccessLevel.USER_MODE);
        }
        
        else if(accountId != NULL && string.isNotBlank(name)){
            String query = 'SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId AND Name LIKE :name LIMIT :rows';
            contacts = Database.queryWithBinds(query, params, AccessLevel.USER_MODE);
        }
        
        else if(accountId != NULL && string.isNotBlank(email)){
            String query = 'SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId AND Email LIKE :email LIMIT :rows';
            contacts = Database.queryWithBinds(query, params, AccessLevel.USER_MODE);
        }
        
        else if(accountId != NULL){
            String query = 'SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId LIMIT :rows';
            contacts = Database.queryWithBinds(query, params, AccessLevel.USER_MODE);
        }
                   
        return contacts;
    }
//Ending bracket
}        

Step 2: Define the Component Structure in HTML

The HTML template uses lightning-datatable for rendering the table. Here’s a general example structure:

The layout items include two input fields, a button, and a lightning-datatable that will display the data returned. In this case, we are returning contacts related to the account record we are viewing.

<template>
  <lightning-card title="LWC lightning-datatable">
      <lightning-layout multiple-rows="true" vertical-align="end" class="slds-box">
          <lightning-layout-item size="4" padding="around-small">
            <lightning-input type="text" label="Search account contacts by name" value={key} onchange={updateKey}></lightning-input>
          </lightning-layout-item>

          <lightning-layout-item size="4" padding="around-small">
            <lightning-input type="text" label="Search account contacts by email" value={email} onchange={updateEmail}></lightning-input>
          </lightning-layout-item>

          <lightning-layout-item size="4" padding="around-small">
            <lightning-button label="Clear Filters / Show All" variant="brand" onclick={clearFilters}></lightning-button>
          </lightning-layout-item>

          <lightning-layout-item size="12" style="height: 125px">
            <lightning-datatable
              show-row-number-column hide-checkbox-column
              key-field="Id"
              data={contacts}
              columns={columns}
              enable-infinite-loading="true"
              onloadmore={handleViewMoreRecords}>
            </lightning-datatable>
          </lightning-layout-item>
      </lightning-layout>
  </lightning-card>
</template>        

Step 3: Define Columns and Data in JavaScript

The magic happens in the JavaScript file. Here, I define the columns, fetch data using an Apex method, and dynamically format the rows.

import { LightningElement, track, wire, api } from 'lwc';
import getContacts from '@salesforce/apex/LightningDatatableController.getContacts';

export default class lightningDatatable extends LightningElement {
    @api recordId;
    key = '';
    email = '';
    @track contacts;
    rowLimit = 2;

    columns = [
        { label: 'Name', fieldName: 'nameUrl', type: 'url', typeAttributes: { label: { fieldName: 'Name' }, target: '_blank' } },
        { label: 'Contacts Title', fieldName: 'Title', type: 'text' },
        { label: 'Email', fieldName: 'Email', type: 'email', typeAttributes: { label: { fieldName: 'Email' }, target: '_blank' } },
        { label: 'Created Date', fieldName: 'CreatedDate', type: 'date', typeAttributes: { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true } }
    ];

    @wire(getContacts, { accountId: '$recordId', name: '$key', email: '$email', rows: '$rowLimit' })
    wiredContacts({ data, error }) {
        if (data) {
            this.contacts = data.map(record => ({
                ...record,
                nameUrl: `/lightning/r/${record.Id}/view`
            }));
        } else if (error) {
            console.error(error);
        }
    }
     
    updateKey(event) {
        this.key = event.target.value;
        this.email = '';
    }

    updateEmail(event) {
        this.email = event.target.value;
        this.key = '';
    }
    
    clearFilters() {
        this.key = '';
        this.email = '';
    }

    handleViewMoreRecords() {
        this.rowLimit += 25;
    }
}        

Step 4: Update the LWC meta.xml to indicate where we want to allow the LWC to be applied

isExposed attribute must equal true to expose the component in the page editor.

Targets define where the component can be utilized.

See this documentation for a complete list of available targets

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="<https://soap.sforce.com/2006/04/metadata>">
    <apiVersion>62.0</apiVersion>
    <isExposed>True</isExposed>
    <targets>
    <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>        

Key Features in This Implementation

  1. Dynamic Columns: Defined in the columns array, eliminating the need for hardcoding a table in HTML.
  2. Data Mapping: Transformed backend data to include dynamic URLs for records, contacts, single-click to email, single-click to initiate a phone call if you have a contact center/softphone configured in your org.
  3. Filtering and Pagination: Controlled through simple event handlers, making the component dev and user-friendly.

Benefits of Using lightning-datatable

  • Time Efficiency: Reduces the need to manually define and style HTML tables.
  • Consistency: Ensures a uniform look and feel across Salesforce components.
  • Flexibility: Easily configurable through JavaScript, supporting features like dynamic columns, sorting, and pagination.

Below I’ll continue breaking down some of the things I find using lightning-datatables in LWC especially awesome! Some of which I found not very well explained or simply non-existent on the interwebs, as most devs still seem to be using HTML tables over LWC datatables. After banging my head against the wall for a while, am happy to be able to share this info in a single post with the developer community.


Brief Explanation of Wire and API

import wire

The @wire decorator is used to connect a property or a function to Salesforce data or metadata. It is primarily used to work with Salesforce Apex methods or Lightning Data Service (LDS).

import api

The api decorator exposes properties. In this case providing us with the account’s recordId.

But YOU SAID POWER in the title?

The power of lightning-datatables is in the relationship between the HTML and JS file. Let’s dig deeper into the columns portion. I struggled with this more than I care to admit publicly. I’ve intentionally given examples for most column properties and data types that can be used in a datatable. The columns (list of objects) determine how the data is passed and displayed in the HTML template/browser document.

columns = [
        { label: 'Name', fieldName: 'nameUrl', type: 'url', typeAttributes: { label: { fieldName: 'Name' }, target: '_blank' } },
        { label: 'Contacts Title', fieldName: 'Title', type: 'text' },
        { label: 'Email', fieldName: 'Email', type: 'email', typeAttributes: { label: { fieldName: 'Email' }, target: '_blank' } },
        { label: 'Created Date', fieldName: 'CreatedDate', type: 'date', typeAttributes: { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true } }
    ];
        

The one I struggled with the most was making the record's name, in this case the contacts name, a hyperlink that routes us directly to the contact. The tricky part is handling the data the lightningDatatable.js receives from the wired function.

Data is returned as an [object][object] and needs to be unpacked in order to get the recordId into our formatted URL. Let’s take a closer look at that.

        /* The map function along with formatted url data is required
        to make the URL type column's rows hyperlinked and to take 
        us to the specific record in that row. What is seen is simply 
        the name of the contact determined by the column configuration
        { label: 'Name', fieldName: 'nameUrl', type: 'url', typeAttributes: { label: {                 fieldName: 'Name' }, target: '_blank' } }
         */
_________________

@wire(getContacts, { accountId: '$recordId', name: '$key', email: '$email', rows: '$rowLimit' })
    wiredContacts({ data, error }) {
        if (data) {
            this.contacts = data.map(record => ({
                ...record,
                nameUrl: `/lightning/r/${record.Id}/view`
            }));
        } else if (error) {
            console.error(error);
        }
    }
        

Using Infinite Scrolling to Load More Rows

Using infinite scrolling is another fancy trick that can be used to increase performance and reduce the chance of ever hitting a SOQL Query Row limit exception for large datasets.

<lightning-layout-item size="12" style="height: 125px">
            <lightning-datatable
              show-row-number-column hide-checkbox-column
              key-field="Id"
              data={contacts}
              columns={columns}
              enable-infinite-loading="true"
              onloadmore={handleViewMoreRecords}>
              </lightning-datatable>
<lightning-layout-item>        

To enable infinite scrolling, specify?enable-infinite-loading = "true"

The onLoadMore event handler retrieves additional data when the user scrolls to the bottom of the table, continuing until no more records are available to query.

Set the lightning-layout-items height using style="height: 125px". We can adjust this depending on the height you’d like to make your table and how many records you’d like to display on screen when the page loads or a search is performed.

In lightningDatatable.js, notice that we set a rowLimit that is used to limit the amount of records returned by our controller, that is, until we scroll to the bottom of the table. At that point, the handleViewMoreRecords function is triggered by the HTML onloadmore event. Which will add the amount of records you wish to add to the initial rowLimit and so on.

rowLimit = 2; // We return only two records to start based on the height of the table
// in this example. And we add 25 more each time we reach the bottom of the table. 
    handleViewMoreRecords() {
        this.row limit += 25;
    }        

Security

When performing SOQL queries, it’s crucial to always consider security implications. I am not going to dig into the various methods to create secure SOQL queries which are found in Salesforce documentation.

I've intentionally shared the solution I've developed below to use a dynamic SOQL query in our LWC’s controller, particularly when using the LIKE operator in the WHERE clause. creating a map<string, object> and the queryWithBinds database query method is the secure and correct approach for this use case.

// re-iterating the concept found in LightningDatatableController shared earlier in the article

//Create map for query with binds
        Map<String, Object> params = new Map<String, Object>{'accountId' => accountId, 'name' => '%' + name + '%', 'email' => '%' + email + '%', 'rows' => rows};
        
        //Perform query based on existing params
        
        if(accountId != NULL && string.isNotBlank(name) && string.isNotBlank(email)){
            String query = 'SELECT Id, Name, Email FROM Contact WHERE AccountId = :accountId AND Name LIKE :name AND Email LIKE :email LIMIT :rows';
            contacts = Database.queryWithBinds(query, params, AccessLevel.USER_MODE);
        }        

Conclusion

lightning-datatable is an invaluable tool for Salesforce developers. By shifting much of the table configuration to JavaScript, it not only simplifies development but also enhances the maintainability and scalability of components. In my Record History application (available soon on the Salesforce AppExchange), this approach allowed me to create a highly dynamic and interactive table without touching HTML layouts.

I hope this guide helps you leverage the lightning-datatable component in your next project. Have questions or tips to share? Feel free to drop them in the comments!

Rob R.

Cybersecurity Strategist | Marine Corps Cyber Advisor | Indiana Governor's Council on Cybersecurity | Veteran Mentor

1 个月

Love this! Thanks for sharing man!

要查看或添加评论,请登录

社区洞察

其他会员也浏览了