
Hello, and welcome to Another Salesforce Blog! Here I will be posting solutions to problems that I couldn’t find an answer to in hopes of helping those who find themselves stuck when using the Salesforce platform.
User Story
We want to create a lightning__RecordPage
component that displays a table that looks different whether it is in a main region or a sidebar, which means that, by default, the component is responsive and mobile friendly.
For this example, we will be using the relationship between the Account
and Contact
objects in Salesforce to display a list of related Contacts
on the Account
object. When in a CLASSIC
view, the table will display columns showing a utility:email
icon, the Name
, Department
, and Phone
fields of the related Contact
records, and a lightning-button-menu
that displays options to edit and delete the contacts.
Note: We will not be building out the email functionality or the edit/delete functionality in this tutorial, these are placeholders for the purpose of demonstration.
The GitHub repository for this project is located here.

Background
There are several ways to do this, but the easiest way is to use CSS grid-template-areas. This will make our code faster, sleeker, and easier to maintain than a solution within the markup.
This tutorial requires knowledge of Lightning Web Components, Apex, and requires Visual Studio Code.
Solution
Step One – Build Out Relevant Apex Classes.
We know that we need to get the Account
and its related Contact
records, as well as the fields specified previously. Because this component will be on a lightning__RecordPage
, we are able to access the recordId
of the record page, and are thus able to query for the relevant account. We will start by building a data access with this query.
GetAccountInfo.cls:
public with sharing class GetAccountInfo {
public static Account getAccount(Id accountId){
Account accountToReturn = [
SELECT
Id,
Name,
(SELECT Id, Name, Department, Email, Phone FROM Contacts)
FROM Account
WHERE Id = :accountId
LIMIT 1
];
return accountToReturn;
}
}
Next, we will build a helper class that allows us to put the information from the queried Account
record and its related Contact
records into a wrapper class, allowing us to access them as a JavaScript object. We include a constructor for the wrapper class to allow us to initialize instances of the wrapper class, and we must make the constructor @TestVisible
so that we are able to access the private constructor of the wrapper class.
AccountInfoWrapper.cls:
public with sharing class AccountInfoWrapper {
public static AccountWrapper getAccountWrapper(Id accountId) {
Account queriedAccount = GetAccountInfo.getAccount(accountId);
AccountWrapper wrapperToReturn = new AccountWrapper(queriedAccount);
return wrapperToReturn;
}
public with sharing class AccountWrapper {
@AuraEnabled public String accountId;
@AuraEnabled public String accountName;
@AuraEnabled public List<ContactWrapper> contactList;
@TestVisible
private AccountWrapper(Account accountToWrap) {
this.accountId = accountToWrap.Id;
this.accountName = accountToWrap.Name;
this.contactList = new List<ContactWrapper>();
if(accountToWrap.Contacts.size() > 0) {
for(Contact contactToWrap : accountToWrap.Contacts) {
ContactWrapper wrappedContact = new ContactWrapper();
wrappedContact.contactId = contactToWrap.Id;
wrappedContact.contactName = contactToWrap.Name;
wrappedContact.contactDepartment = contactToWrap.Department;
wrappedContact.contactEmail = contactToWrap.Email;
wrappedContact.contactPhone = contactToWrap.Phone;
this.contactList.add(wrappedContact);
}
}
}
}
public with sharing class ContactWrapper {
@AuraEnabled public String contactId;
@AuraEnabled public String contactName;
@AuraEnabled public String contactDepartment;
@AuraEnabled public String contactEmail;
@AuraEnabled public String contactPhone;
}
}
Lastly, we will create an @AuraEnabled
class that interfaces with our helper class and our JavaScript in the Lightning Web Component. It is within this class that we would place any permissions gates that would prevent a user from accessing the relevant records if they did not have the required permissions.
InterfaceClass.cls:
public with sharing class InterfaceClass {
@AuraEnabled(cacheable=true)
public static AccountInfoWrapper.AccountWrapper getAccountInfoWrapper(Id accountId){
try {
return AccountInfoWrapper.getAccountWrapper(accountId);
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
}
The test classes and .cls-meta.xml
files for these classes are included in the GitHub repository for this post.
Step Two – Create Parent Component.
We are going to create a parent Lightning Web Component that calls the Interface class that we wrote above and passes the returned wrapper class as a JavaScript object to the child Lightning Web Component. This will allow us to easily make our code object agnostic and reusable. We are also going to use the parent component to get the width of the child component so that we can make our component reactive.
contactParentComponent.html:
<template>
<c-display-contact-table class="displayContactTable" display-contacts={contacts} screen-width={screenWidth}></c-display-contact-table>
</template>
Note that we have included the tag c-display-contact-table
, which will be the name of our child component. We are passing two variables, the object array contacts
, and the screenWidth
. We have also denoted the class for our child component as displayContactTable
, which will allow us to query for this tag in a second.
For the JavaScript file, let’s break it down line by line.
First, we import our decorators and our Apex method, getAccountInfoWrapper
:
import { LightningElement, api, track, wire } from 'lwc';
import getAccountInfoWrapper from '@salesforce/apex/InterfaceClass.getAccountInfoWrapper';
Next, we define our class and our variables. We make the class record context aware by using @api recordId
to create a public recordId
property. Additionally, we create tracked arrays for contacts
and wiredContacts
(more on this in a second!), an empty String for an error
message, and variables for screenWidth
and componentWidth
. We set the default screenWidth
to CLASSIC
, but we can call this whatever makes sense.
import { LightningElement, api, track, wire } from 'lwc';
import getAccountInfoWrapper from '@salesforce/apex/InterfaceClass.getAccountInfoWrapper';
export default class ContactParentComponent extends LightningElement {
@api recordId;
@track contacts = [];
@track wiredContacts = [];
error = '';
screenWidth = 'CLASSIC';
componentWidth;
After initializing our variables, we write our connectedCallback()
function, which sets a listener for when the window is resized. We call this.resizeFunction
within our event listener, which we will write in just a moment.
connectedCallback() {
window.addEventListener('resize', this.resizeFunction);
}
In order to call the Apex method that we have imported, we will then create a @wire
function, getContacts
. We will use a result
format so that if we want to use refreshApex()
, we are able to do so easily using refreshApex(this.wiredContacts)
. If the result
has data
, we will do a deep clone of result.data.contactList
into this.contacts
to pull the information from our wrapper class into an easily accessible JavaScript object that we will pass to our child component, and we call our resize function to make sure that we’re displaying our table properly based on the width of the component. If there’s an error, we display it in the console, and nothing will display in the table.
@wire(getAccountInfoWrapper, {accountId : '$recordId'})
getContacts(result) {
this.wiredContacts = result;
if(result.data) {
this.contacts = JSON.parse(JSON.stringify(result.data.contactList));
this.error = undefined;
this.resizeFunction();
}
else if (result.error) {
this.contacts = undefined;
this.error = result.error.body.message;
console.error(this.error);
}
}
Lastly, and this is the fun part, we are going to do some JavaScript magic to get the width of our table within its specified container on the record page. Not only will this adjust to the width of a region or sidebar, it will automatically display the compact table on a mobile screen, say, a screen of less than 600 pixels.
First, we’re going to define our component
as the result of a .querySelector()
, which will return the class .displayContactTable
that we defined in our markup.
Next, we’re going to set our this.componentWidth
by obtaining the .getBoundingClientRect().width
of our recently defined component
. This returns the width of the DOMRect
object, which is a rectangle as shown below:

DOMRect
object via developer.mozilla.orgThe width of this rectangle will include the width of any border and padding included, which allows us to get the total width of our child component. Neat!
If the component width is less than 600 pixels, we set our screenWidth
to MOBILE
, otherwise we set the screenWidth
to CLASSIC
.
resizeFunction = () => {
let component = this.template.querySelector('.displayContactTable');
this.componentWidth = component.getBoundingClientRect().width;
if(this.componentWidth < 600) {
this.screenWidth = 'MOBILE';
}
else {
this.screenWidth = 'CLASSIC';
}
}
}
In all, our contactParentComponent.js
file will look like this:
import { LightningElement, api, track, wire } from 'lwc';
import getAccountInfoWrapper from '@salesforce/apex/InterfaceClass.getAccountInfoWrapper';
export default class ContactParentComponent extends LightningElement {
@api recordId;
@track contacts = [];
@track wiredContacts = [];
error = '';
screenWidth = 'CLASSIC';
componentWidth;
connectedCallback() {
window.addEventListener('resize', this.resizeFunction);
}
@wire(getAccountInfoWrapper, {accountId : '$recordId'})
getContacts(result) {
this.wiredContacts = result;
if(result.data) {
this.contacts = JSON.parse(JSON.stringify(result.data.contactList));
this.error = undefined;
this.resizeFunction();
}
else if (result.error) {
this.contacts = undefined;
this.error = result.error.body.message;
console.error(this.error);
}
}
resizeFunction = () => {
let component = this.template.querySelector('.displayContactTable');
this.componentWidth = component.getBoundingClientRect().width;
if(this.componentWidth < 600) {
this.screenWidth = 'MOBILE';
}
else {
this.screenWidth = 'CLASSIC';
}
}
}
Once we’ve created our files, we need to make sure that they are accessible by the Lightning App Builder and accessible on a lightning__RecordPage
by updating our .js-meta.xml
file appropriately. I won’t show this here, but the file will be available in the appropriate GitHub repository.
Step Three – Create Child Component.
Now that we’ve got our contact
array pulling from Apex, we need somewhere to display it!
We’ll start with the markup. Note: some of this functionality is not defined, and is a placeholder. I have notated this within the markup.
displayContactTable.html:
<template>
<table class={tableClass}>
<colgroup>
<col span="1" class="small-column">
<col span="1" class="large-column">
<col span="1" class="large-column">
<col span="1" class="large-column">
<col span="1" class="small-column">
</colgroup>
<thead>
<tr class="slds-line-height_reset">
<th scope="col">
<!-- FUNCTIONALITY FOR THIS COLUMN NOT DEFINED -->
<div class="slds-truncate slds-text-title_caps" title="Email Contact"> </div>
</th>
<th scope="col">
<div class="slds-truncate slds-text-title_caps" title="Contact Name">
Contact Name
</div>
</th>
<th scope="col">
<div class="slds-truncate slds-text-title_caps" title="Contact Department">
Contact Department
</div>
</th>
<th scope="col">
<div class="slds-truncate slds-text-title_caps" title="Contact Phone">
Contact Phone
</div>
</th>
<th scope="col">
<!-- FUNCTIONALITY FOR THIS COLUMN NOT DEFINED -->
<div class="slds-truncate slds-text-title_caps" title="Dropdown"> </div>
</th>
</tr>
</thead>
<tbody>
<template for:each={displayContacts} for:item="contact">
<tr key={contact.contactId}>
<td>
<!-- FUNCTIONALITY FOR THIS COLUMN NOT DEFINED -->
<button class="slds-button slds-button_icon" title="Email Contact">
<lightning-icon icon-name="utility:email" alternative-text="Email Contact" title="Email"></lightning-icon>
</button>
</td>
<td>
{contact.contactName}
</td>
<td>
{contact.contactDepartment}
</td>
<td>
{contact.contactPhone}
</td>
<td>
<!-- FUNCTIONALITY FOR THIS COLUMN NOT DEFINED -->
<lightning-button-menu variant="bare" icon-name="utility:chevrondown" menu-alignment="auto">
<lightning-menu-item value="Edit" label="Edit"></lightning-menu-item>
<lightning-menu-item value="Delete" label="Delete"></lightning-menu-item>
</lightning-button-menu>
</td>
</tr>
</template>
</tbody>
</table>
</template>
Note that in line 2 (highlighted) that we are setting the class tableClass
dynamically. We will be using a getter
for this in the JavaScript, which will update the class reactively when the width of the container is changed. We also use a getter
and setter
for the value of the displayContacts
object array.
displayContactTable.js:
import { LightningElement, api, track, wire } from 'lwc';
export default class DisplayAccountInfo extends LightningElement {
@api screenWidth = 'CLASSIC';
@track _contacts = [];
@api displayContacts = [];
@api
get contacts() {
return this._contacts
}
set contacts(value) {
this._contacts = value;
this.displayContacts = value;
}
get tableClass() {
if (this.screenWidth === 'MOBILE') {
return 'mobileContainerDiv grid slds-table slds-table_cell-buffer slds-table_bordered full-table'
}
return 'classicContainerDiv slds-table slds-table_cell-buffer slds-table_bordered full-table'
}
}
Note that if the screenWidth
is MOBILE
, we return the classes mobileContainerDiv
and grid
. If the screenWidth
is anything other than MOBILE
, we return the class classicContainerDiv
.
Now comes the part we’ve all been waiting for, the CSS that will help us collapse our table neatly. We’ll set some colors, use our .mobileContainerDiv
class to hide some column headers when in MOBILE
view, and, most importantly, set our cells to display in consecutive rows rather than in columns. We can do this using grid-template-areas
.
In order to get this to collapse neatly, we’ll use the following CSS for our tr
elements:
/*USE GRID DISPLAY AND GRID TEMPLATE AREAS TO FORMAT COLLAPSED TABLE*/
table.mobileContainerDiv tr {
display: grid;
grid-template-rows: min-content auto;
grid-template-columns: .5fr 2fr .5fr;
grid-template-areas:
"a b b b e"
"a c c c e"
"a d d d e";
}
This specifies the grid-template-areas
a
through e
(highlighted), and allows us to specify which td
elements will occupy which area
as seen below:
/*icon column*/
table.mobileContainerDiv td:first-of-type {
display: grid;
grid-area: a;
justify-content: center;
align-content: center;
border: bottom;
}
/*contact name column*/
table.mobileContainerDiv td:nth-of-type(2) {
display: grid;
grid-area: b;
border: top;
}
/*contact department column*/
table.mobileContainerDiv td:nth-of-type(3) {
display: grid;
grid-area: c;
color: gray;
border: none;
}
/*contact phone column*/
table.mobileContainerDiv td:nth-of-type(4) {
display: grid;
grid-area: d;
border: none;
}
/*dropdown column*/
table.mobileContainerDiv td:nth-of-type(5) {
display: grid;
grid-area: e;
justify-content: center;
align-content: center;
border: bottom;
}
We align
and justify
our utility:email
icon, as well as our lightning-button-menu
icon so that they look pretty and are centered within their respective areas
, and we play around with the borders so that they display as appropriate.
Once the CSS is rendered, our MOBILE
sized table will look like this:

In all, our CSS file looks like this:
/*header css*/
table th, tr {
background-color: #F4F6F9;
}
table td {
background-color: white;
}
/*HIDE ALL COLUMN HEADERS EXCEPT CONTACT NAME*/
/*email icon column*/
table.mobileContainerDiv th:first-of-type {
display: none;
}
/*contact department column*/
table.mobileContainerDiv th:nth-of-type(3) {
display: none;
}
/*contact phone column*/
table.mobileContainerDiv th:nth-of-type(4) {
display: none;
}
/*dropdown column*/
table.mobileContainerDiv th:nth-of-type(5) {
display: none;
}
/*USE GRID DISPLAY AND GRID TEMPLATE AREAS TO FORMAT COLLAPSED TABLE*/
table.mobileContainerDiv tr {
display: grid;
grid-template-rows: min-content auto;
grid-template-columns: .5fr 2fr .5fr;
grid-template-areas:
"a b b b e"
"a c c c e"
"a d d d e";
}
/*icon column*/
table.mobileContainerDiv td:first-of-type {
display: grid;
grid-area: a;
justify-content: center;
align-content: center;
border: vertical
}
/*contact name column*/
table.mobileContainerDiv td:nth-of-type(2) {
display: grid;
grid-area: b;
border: top;
}
/*contact department column*/
table.mobileContainerDiv td:nth-of-type(3) {
display: grid;
grid-area: c;
color: gray;
border: none;
}
/*contact phone column*/
table.mobileContainerDiv td:nth-of-type(4) {
display: grid;
grid-area: d;
border: none;
}
/*dropdown column*/
table.mobileContainerDiv td:nth-of-type(5) {
display: grid;
grid-area: e;
justify-content: center;
align-content: center;
border: bottom;
}
Step Four – Add To Record Page and Test!
Now that we’ve got our code written, we need to add it to our Account record page and test it.

Thanks for reading, let me know if you have any comments or questions!
-Evelyn, Another Salesforce Blog

Make a one-time donation
Make a monthly donation
Make a yearly donation
Choose an amount
Or enter a custom amount
Help keep Another Salesforce Blog on the internet by donating today!
Your contribution is appreciated.
Your contribution is appreciated.
DonateDonate monthlyDonate yearly