Unleash the Power of Lightning-Datatable in Salesforce LWC Development
Nicolas H.
Certified Salesforce Developer | Cybersecurity Specialist | Integration Specialist
(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
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:
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
Benefits of Using lightning-datatable
领英推荐
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!
Cybersecurity Strategist | Marine Corps Cyber Advisor | Indiana Governor's Council on Cybersecurity | Veteran Mentor
1 个月Love this! Thanks for sharing man!