Purpose:
I have been assigned a task for building a custom configurable Related list component using Field Set and Lightning Web Component.
Now, I want to make it robust and flexible. This code will help many developers to quickly develop and leverage this concept.
Here is a use case why it is needed to build.
Use Case:
Business requirement was to view this component on the parent record detail page in the form of a Related list. User has edit and delete permissions to the related list's objects. But business wants that user will not going to see any options to edit or delete records from the related list.
For example, Contact related list is available under Account record. User has a permission to edit or delete on Contact object but those records only be available for view purpose. User will not able to see Edit/Delete option. As a future enhancement I'll add the edit and delete buttons as well. Stay tuned!
Currently I decided to built this component as read-only viewing purpose.
This requirement is applicable for similar use cases like this, so it will be better to create a configurable component using Field Set.
Fields from the Field Set will be displayed as columns and records based on those fields will be displayed as rows.
Final Result:
Let's dive into the solution:
We will be creating a Contact object's related list and this will be completed in 3 stages.
Stage 1:
Create a Field Set on Contact object with name relatedListFS and add necessary fields on the layout, as shown below:
Stage 2:
Build the dynamicRelatedList LWC component using the below code.
- Create the component and add the lightning card and lightning datatable in the component's HTML file.
- In the connectedCallback method of the component we'll going to call an apex method imperatively to fetch the column details and records to display in the lightning datatable.
dynamicRelatedList.html
<template>
<div class="container">
<div class="slds-card">
<div class="slds-media__body">
<h2 class="slds-card__header-title">
<span>{objApiName} Records ({recordCount})</span>
</h2>
</div>
<div class="slds-card__body">
<lightning-datatable
key-field="Id"
data={tableData}
columns={columns}
min-column-width=200>
</lightning-datatable>
</div>
</div>
</div>
</template>
dynamicRelatedList.js
Few important points we have here to keep in mind:
- Never define an attribute with name objectApiName as it is reserved, that's why I am using objApiName, otherwise it'll always going to take the current object api name of the record page on which you have added this component.
- When we pull entries from a Map which has been stored in Apex class, the index is inversed. For example, I have first stored FIELD_LIST and then RECORD_LIST in the Map. Now, the index of keys will be 1 for FIELD_LIST and 0 from RECORD_LIST.
- For making the first column as link, extra code is there.
import { LightningElement, api, track } from 'lwc';
import getData from '@salesforce/apex/dynamicRelatedListController.getData';
export default class DynamicRelatedList extends LightningElement {
@api recordId; // parent record id from record detail page
@api objApiName; //kind of related list object API Name
@api fieldSetName; // fieldSet containing all the required fields to display in related list
@api criteriaFieldAPIName; // This field will be used in WHERE condition of our SOQL query
@api showFirstColumnAsLink; //if the first column can be displayed as hyperlink
@track columns; //columns for List of fields in datatable
@track tableData; //data to show in datatable
recordCount; //this shows record count inside the ()
connectedCallback(){
let firstTimeEntry = false;
let firstFieldAPI;
console.log('#####objApiName--> '+this.objApiName);
//make an implicit call to fetch records from database
getData({ strObjectApiName: this.objApiName,
strfieldSetName: this.fieldSetName,
criteriaField: this.criteriaFieldAPIName,
criteriaFieldValue: this.recordId})
.then(data=>{
//get the map of fields and data
let objStr = JSON.parse(data);
//retrieving listOfFields from the map,
//here order is reverse of the way it has been inserted in the map
let listOfFields= JSON.parse(Object.values(objStr)[1]);
//retrieving listOfRecords from the map
let listOfRecords = JSON.parse(Object.values(objStr)[0]);
console.log('#####listOfRecords--> '+JSON.stringify(listOfRecords));
let items = []; //array to prepare columns
//if user wants to display first column as link and on click of the link it will
//naviagte to record detail page. Below code prepare the first column with type = url
listOfFields.map(element=>{
//it will enter this if-block just once
if(this.showFirstColumnAsLink !=null && this.showFirstColumnAsLink=='Yes'
&& firstTimeEntry==false){
firstFieldAPI = element.fieldPath;
//perpare first column as hyperlink
items = [...items ,
{
label: element.label,
fieldName: 'URLField',
fixedWidth: 150,
type: 'url',
typeAttributes: {
label: {
fieldName: element.fieldPath
},
target: '_blank'
},
sortable: true
}
];
firstTimeEntry = true;
} else {
items = [...items ,{label: element.label,
fieldName: element.fieldPath}];
}
});
//finally assigns item array to columns
this.columns = items;
this.tableData = listOfRecords;
console.log('listOfRecords',listOfRecords);
//if user wants to display first column has link and on clicking of the link it will
//naviagte to record detail page. Below code prepare the field value of first column
if(this.showFirstColumnAsLink !=null && this.showFirstColumnAsLink=='Yes'){
let URLField;
//retrieve Id, create URL with Id and push it into the array
this.tableData = listOfRecords.map(item=>{
URLField = '/lightning/r/' + this.objApiName + '/' + item.Id + '/view';
return {...item,URLField};
});
//now create final array excluding firstFieldAPI
this.tableData = this.tableData.filter(item => item.fieldPath != firstFieldAPI);
}
console.log('#####objApiName--> '+this.objApiName);
this.recordCount = this.tableData.length;
this.error = undefined;
})
.catch(error =>{
this.error = error;
console.log('error',error);
console.log('#####error--> '+JSON.stringify(error));
console.error('error',error);
this.tableData = undefined;
})
}
}
dynamicRelatedList.js-meta.xml
Configurable attributes in the meta.xml file of the component:
- Related List Object API Name - Here it is Contact.
- Field Set Name - The name of field set defined on the Object whose fields will be displayed as table columns. Here it is relatedListFS.
- Reference FieldAPIName - It is the field based on which query to be fired. Here, it is AccountId, as Contact related list will be placed on Account Record Page.
- Show First Column As Link - It will take Yes/No. For example, if we define Contact.LastName as first column and if we choose Yes option then Last Name will be shown as hyperlink and clicking on that link, it will be navigated to Contact Record Detail page. If we choose No then, it will be normal text column.
<?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__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage,lightning__AppPage,lightning__HomePage">
<property name="objApiName" label="Related List Object API Name" type="String" default=""/>
<property name="fieldSetName" label="Field Set Name" type="String" default=""/>
<property name="criteriaFieldAPIName" label="Reference FieldAPIName" type="String" default=""
description="The field which we will be using in WHERE condition of SOQL query."/>
<property name="showFirstColumnAsLink" label="Show First Column As Link"
type="String" datasource="Yes,No" default="Yes"/>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
dynamicRelatedList.cls
Few notable points regarding our apex method:
- Getting the instance of sObject based on strObjectApiName, the reflection has been used which is way more faster that globalDescribe.
- Using Schema.FieldSetMember to get the fields from fieldSet. Refer Documentation.
- getFieldPath() of FieldSetMember gives the fieldAPI which has been used to build SOQL query.
public with sharing class dynamicRelatedListController {
@AuraEnabled
public static String getData(String strObjectApiName, String strfieldSetName,
String criteriaField, String criteriaFieldValue){
System.debug('#####strObjectApiName--> '+strObjectApiName);
System.debug('#####strfieldSetName--> '+strfieldSetName);
System.debug('#####criteriaField--> '+criteriaField);
System.debug('#####criteriaFieldValue--> '+criteriaFieldValue);
Map<String, String> returnMap = new Map<String,String>();
if(!String.isEmpty(strObjectApiName) && !String.isEmpty(strfieldSetName)){
//get field details from FieldSet
SObject sObj = (SObject)(Type.forName('Schema.'+ strObjectApiName).newInstance());
List<Schema.FieldSetMember> lstFSMember =
sObj.getSObjectType().getDescribe().fieldSets.getMap().get(strfieldSetName).getFields();
//prepare SOQL query based on fieldAPI names
String query = 'SELECT ';
for(Schema.FieldSetMember f : lstFSMember) {
query += f.getFieldPath() + ', ';
}
query += 'Id FROM ' + strObjectApiName ;
//Prepare WHERE condition if criteria field is present
if(!(String.isEmpty(criteriaField) && String.isEmpty(criteriaFieldValue))){
query += ' WHERE ' + criteriaField + '=\'' + criteriaFieldValue + '\'';
}
//executing query
List<SObject> lstRecords = Database.query(query);
//prepare a map which will hold fieldList and recordList
returnMap.put('FIELD_LIST', JSON.serialize(lstFSMember));
returnMap.put('RECORD_LIST', JSON.serialize(lstRecords));
return JSON.serialize(returnMap);
}
return null;
}
}
Final Step:
Place the component on the Account Record Page using the Lightning App Builder and define the configurable attributes there
Very helpful Rohit.
ReplyDelete