Create Lightning Web Component (LWC) Datatable Columns using Apex

A “controversial” approach on generating both the LWC lightning-datatable data and column configuration fully from Apex

Justus van den Berg
8 min readMar 13, 2024

The “lightning-datatable” Lightning Web Component (LWC) requires two main parameters in to be generated:
1) The data, represented in a JSON structure with an object list containing one or more key/value pairs.
2) The columns, a JSON structure that contains the column configuration for the table with items like column type, date/number formatting, etc.

Commonly you populate the data parameter using an Apex controller method that runs some (query) logic and returns a data array that matches the column configuration. The columns parameter however is usually hard coded in the LWC’s Javascript file. This is not a problem until you have tables with dynamic columns.

I have found a surprising amount of scenarios where the column visibility and configuration is actually decided by logic that happens in an Apex Controller. This resulted in Apex methods that return both the data and column info in a wrapper. After calling the Apex method, the LWC Javascript creates or updates the columns and stitches together the columns/data table based on the response. It looks something like this:

// Set the data from the Apex controller
this.data = result.data;

// Create columns in JS based on data from the Apex controller (!! BAD !!)
for (let index = 0; index < result.columns.length; index++) {
this.columns.push({
label : result.columns[index],
fieldName : String(index),
initialWidth : result.columns[index].length * 12
});
}

The combination of Apex and Javascript working together to generate a single data structure for the data table is not a great solution to say the least: A single data structure should not be created through two different technologies nor in two different places (imho).

It’s difficult to read and maintain and it was driving me mental. So I decided to bite the bullet and create an Apex Class Data Structure that replicates the full “lightning-datatable” JSON parameter structure in a class called “utl.Ldt”. This allows for a full data table structure in a single response when we call the Apex Controller from an LWC. The full table is now created in a single place by a single method.

With the exception of the toJSON() method, all methods in the utl.Ldt class are returning themselves so everything can be chained together in a functional programming style. This makes it easy to setup the entire table from a single line.
When tables get large, the ability to not have to store the table in a variable can save a lot of heap space, something to keep in mind if you work with large volumes. You are sacrificing a little bit of readability though.

TL;DR

The repository can be found here:
https://github.com/jfwberg/lightweight-lwc-utl

The solution

I have set myself the following guidelines when choosing a solution:

  • Are data table columns static and will never change?
    Set the column configuration in the LWC Javascript file.
  • Are the columns created or updated dynamically through Apex together with the data in the same transaction?
    Generate a single ult.Ldt instance that represents the entire table with both data and columns using Apex.
  • Are the columns created or updated dynamically through Apex but can change independently from the data?
    Generate two utl.Ldt instances: One for the columns and one for the data, so both data and columns can be updated independently for improved performance. Contradicting myself already, but this seems like a bit of niche use case.
  • Are columns or data updated through Javascript after initialization?
    Create the table using a single utl.Ldt instance to start with and simply use Javascript for further updates.

Now the whole table configuration and data can be put in a single data structure that we can put that in a javascript variable i.e. “ldt”. The LWC’s HTML file looks something like the code below. The parameters (with the exception of front-end actions) are all referring to the same object and will never have to change between implementations.

<lightning-datatable 
key-field = {ldt.keyField}
data = {ldt.data}
columns = {ldt.columns}
hide-checkbox-column = {ldt.hideCheckboxColumn}
show-row-number-column = {ldt.showRowNumberColumn}
onrowaction = {handleRowAction}
></lightning-datatable>

The LWC’s Javascript file looks something like this:

// Lightning stuff
import {LightningElement} from "lwc";

// Custom Util for error handling
import {handleError} from 'utl/util';

// Apex Class and Method that gets the table data
import getTable from '@salesforce/apex/YourClass.getTable';

// Main JS class
export default class TableExample extends LightningElement {

// Variable to store the lightning-datatable data and configuration
ldt = {};

// Load the table on init
connectedCallback(){
this.handleGetTable();
}

// Method that calls an apex Method that returns a Lightning Data Table
// Object (utl.Ldt) as the controller response
handleGetTable(){

// Execute apex
getTable()
.then(apexResponse => {
// Set the table to the response
this.ldt = apexResponse;
})
.catch(error => {
handleError(error);
});
}
}

The Apex class looks something like this (I’ll go into more detail later on)

@AuraEnabled
public static utl.Ldt getTable(){
try {

// Create a key value pair table
utl.Ldt ldt = new utl.Ldt()
.setupKeyValue()
.addKeyValuePair('Key 01','Key 01')
.addKeyValuePair('Key 02','Key 02')
;

// Return the data table
return ldt;

} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}

So why do I think this is a bit controversial? By making the backend responsible for what the front-end looks like, you’re sort of going back in time with an “old fashioned” MVC paradigm. So it’s the age old dilemma of what part of the code is responsible for what and what part should be responsible.
The interesting part here is that the columns and data can be modified by both front- and backend controllers without them knowing of any changes that have occurred. So what can happen is that you initialize a table from Apex and make updates from the LWC and send updated data back and now update a table from the backend again. It just goes a bit back and forth. It might be the nature of LWC with data controllers but although it works, it doesn’t feel 100% right to me.

We can argue pros and cons but I think using a dedicated Apex Class is a good solution for columns that are dynamically generated in Apex. It keeps the code simple, maintainable and readable. It does create a little bit of Apex overhead, especially with many columns, but I am happy to deal with that. Feel free to leave your thoughts and or comments :-)

Examples

This section lines out a couple of examples of common scenarios and how to implement them. The full set of methods can be found in the readme file on the repository. The valid values and details on type attributes for these methods can all be found in the Lightning Datatable documentation.

Adding Data

You can add data row by row or as a full list at once. You can add additional rows one by one after setting the initial full data set.

// Add one row at a time
utl.Ldt ldt = new utl.Ldt()
.addRow(new Map<String,Object>{
'FirstName' => 'Action',
'LastName' => 'Hank'
})
.addRow(new Map<String,Object>{
'FirstName' => 'Fois',
'LastName' => 'Gras'
})
;

// Add a full data set
utl.Ldt ldt = new utl.Ldt()
.setData(
new List<Map<String,Object>>{
new Map<String,Object>{
'FirstName' => 'Action',
'LastName' => 'Hank'
},
new Map<String,Object>{
'FirstName' => 'Fois',
'LastName' => 'Gras'
}
}
)
// Add additional rows after setting the full set of data
.addRow(new Map<String,Object>{
'FirstName' => 'Flour',
'LastName' => 'Smuggler'
})
;

Adding basic columns

An example on how to set basic columns to get familiar with the class. These are not all the methods available, but it gives a good idea on how it is setup.

// Setup the output datatable
utl.Ldt ldt = new utl.Ldt()

// Data table setup
.setKeyField('ldtKey') // Defaults to "Id"
.setHideCheckboxColumn(true) // Defaults to true
.setShowRowNumberColumn(false) // Defaults to false

// Add a new column object, the default type is text
.addColumn(
new utl.Ldt.Col('Text field', 'textField')
)

// You can add the field type by adding it as a 3rd parameter
.addColumn(
new utl.Ldt.Col('Number field', 'numberField','number')
)

// You can start adding more configuration items at column level
.addColumn(
new utl.Ldt.Col('Number field', 'numberField')

// It's possible to set the field type later on as well
.setType('email')

// Set an initial width
.setInitialWidth(150)

// Set a fixed width (will ignore the initial width)
.setFixedWidth(150)

// Hide the label
.setHideLabel(true)

// Add an icon
.seticonName('utility:salesforce1')
)
;

Date/Time Column

Datetime columns require some extra formatting. It can be setup using the methods of the utl.Ldt.TypeAttributes class.

utl.Ldt ldt = new utl.Ldt()
.addColumn(
new utl.Ldt.Col('Date/Time Field', 'dtField')

// Set the type to date
.setType('date')

// Set the type attributes format the date
// to look like "30/3/2024, 12:14:25"
.setTypeAttributes(
new utl.Ldt.TypeAttributes()
.setDay('2-digit')
.setYear('numeric')
.setMonth('numeric')
.setHour('2-digit')
.setMinute('2-digit')
.setSecond('2-digit')
)
)
;

Actions Column

Example on how to implement an Actions Column with an initial width

utl.Ldt ldt = new utl.Ldt()
.addColumn(

// The action column does not require a name and label
new utl.Ldt.Col(null, null)

// Set column type to action
.setType('action')

// Example of setting the initial width
.setInitialWidth(80)

// Create the type attributes specific to row actions
.setTypeAttributes(
new utl.Ldt.TypeAttributes()
.setMenuAlignment('auto')
.addRowAction(new Ldt.RowAction('Set Password', 'set_password' ))
.addRowAction(new Ldt.RowAction('Set Color', 'set_color', 'utility:color_swatch'))
)
)
;

URL Column

Example on how to implement a URL column with a fixed width

utl.Ldt ldt = new utl.Ldt()
.addColumn(
new Ldt.Col('Field Label', 'Field_Api_Name')

// Set the column type to URL
.setType('url')
.setFixedWidth(120)

// URL specific attributes
.setTypeAttributes(
new Ldt.TypeAttributes()
.setLabel('Name')
.setTarget('_self')
.setTooltip('Field_Containing_Tooltip_Text_Api_Name')
)
)
;

Key/Value tables

One of the most common type of table I use, is a key value/pairs table to display some form of JSON data. I created a specific set of methods to simplify the creation of this type of table.

utl.Ldt ldt = new utl.Ldt()
// Set up the column configuration to key/value, keyField etc
.setupKeyValue()

// Alternatively, you can set your own labels (defaults to Key / Value)
.setupKeyValue(String keyLabel, String valueLabel)

// Data can be added by using the below method
// Note that values can occur more than once
.addKeyValuePair('Key 01','Value 01')
.addKeyValuePair('Key 02','Value 02')
;

Outputting data

You can return the entire table to Javascript as an utl.Ldt object, but alternatively you can also get the table as a JSON string by using the utl.Ldt.toJSON() method. Use this in combination with the Javascript JSON.parse() function. This is required if you have set custom class names. This method transforms the name of class_x variables back to class due to the reserved class keyword in Apex.

Final Note

At the time of writing I am a Salesforce employee, the above article describes my personal views and techniques only. They are in no way, shape or form official advice. It’s purely informative.
Anything from the article is not per definition the view of Salesforce as an Organization.

Cover Image by Microsoft Image Creator

--

--