Blog

Get Our Latest Thoughts & Opinions

How To Create A Generic Table For Lightning Experience

  • admin

We were recently given a task to prepare a component which would display a table with the columns we were given. At one point, we had created several such tables and others were waiting for implementation. As a result, we came up with the idea to create a generic component that would be able to meet our expectations while reducing the amount of code we needed – that’s how our DynamicTable component was born.

This post contains tips on how we dealt with some of the challenges that arose. We wanted to ensure that the component worked when we put it in the managed package, as well as the standalone component. At the same time, we wanted to create a highly customizable component - display records in a certain way, add the ability to restrict sorting to selected columns and so on.

How DynamicComponent looks

Using the component is very easy:

<code style="color: #000000; word-wrap: normal;"> <c:dynamictable fieldlabels="['Account__r.Name:Customer Name']" fieldnames="['Account__r.Name','Real_Probability_to_Leave__c','Account__r.Customer_Still_Active__c','Customer_Annual_Revenue__c','Potential_Revenue_At_Risk__c']" matchcriteria="{#'Churn_Report__c = \'' + v.recordId + '\'}" referencefields="['Account__r.Name']" sobjectname="Churn_Customer_Analyse__c" sortbyfield="Potential_Revenue_At_Risk__c" sortorder="DESC" title="Probability Active Customers Will Leave">  
</c:dynamictable></code>

That’s all. We do not need to attach it to an extra controller et cetera – and the generated component looks like the image below:


One of the advantages of this component is that we don’t need to specify the display types of fields – our component automatically retrieves the field type based on how it’s defined in Salesforce. Furthermore, we can even change this behaviour. We can simply show an integer field as a checkbox (e.g. for 0 it will show unchecked checkbox, for any other value - checked checkbox). Our feature also allows users to select columns which should show a link to a related record, select columns which should be sortable and change default labels of columns. Last but not least, our component respects the Field Level Security, so when a user doesn't have access to a field – the user won’t see it at all.

What's under the hood?

We use an Apex controller with three @AuraEnabled methods: the first method to retrieve and prepare columns metadata, second - to count how many records are in total (we limit to show not more than 2000 records) and third – to retrieve records to show them in the table. Let’s run through how these methods work.

Getting column metadata is the first method that is called when our component is initialized. This method will pass all the data on the columns to our component: 'SObject' name, list of fields to show, data about the sortable/reference fields, which fields should have overridden labels and the fields display types.

<code style="color: #000000; word-wrap: normal;"> @AuraEnabled  
 public static String getColumnMetadata(String sobject_name, List<string> field_names, List<string> field_labels, List<string> sortable_field_names, List<string> reference_fields, List<string> override_field_type){  
      return JSON.serialize(new ColumnMetadataWrapper(sobject_name, field_names, field_labels, sortable_field_names, reference_fields, override_field_type), true);  
 }</string></string></string></string></string></code>

'ColumnMetadataWrapper' i an inner class, which contains two fields: map of 'ColumnMetadata' to original field name and String for error message if it exists. But let’s look at the constructor of this class.

<code style="color: #000000; word-wrap: normal;"> public ColumnMetadataWrapper(String sobject_name, List<string> field_names, List<string> field_labels, List<string> sortable_field_names, List<string> reference_fields, List<string> override_field_type){  
      String namespace = retrieveNamespace();  
      try {  
           String sobject_internal_name = parseSObjectName(namespace, sobject_name);  
           if(!Schema.getGlobalDescribe().get(sobject_internal_name).getDescribe().isAccessible()){  
                throw new DynamicTableController.DynamicTableException('User doesn\'t have access to object: ' + Schema.getGlobalDescribe().get(sobject_internal_name).getDescribe().getName() + '.');  
           }  
           parseFieldsString(namespace, sobject_internal_name, field_names);  
           setColumnsLabels(field_labels);  
           setSortableColumns(sortable_field_names);  
           setReferenceColumns(reference_fields);  
           setOverridedFieldTypeColumns(override_field_type);  
      } catch (DynamicTableException e){  
           error_message = e.getMessage();  
           column_metadata = new Map<string, columnmetadata="">();  
      }  
 }  
</string,></string></string></string></string></string></code>

At first, we retrieve the namespace of the package in which the controller is located – if it exists of course. In that way, we can simply declare fields without the namespace in our component. So, these two declarations are allowed in Lightning Components and are equivalent, considering that our namespace is “ns”:

<code style="color: #000000; word-wrap: normal;"> <c:dynamictable ...="" fieldnames="['Account__r.Name','Real_Probability_to_Leave__c']" sobjectname="Churn_Customer_Analyse__c">  
</c:dynamictable></code>

 

<code style="color: #000000; word-wrap: normal;"> <c:dynamictable ...="" fieldnames="['ns__Account__r.Name',' ns__Real_Probability_to_Leave__c']" sobjectname="ns__Churn_Customer_Analyse__c">  
</c:dynamictable></code>

However, when we look for a field from another package – we need to append namespace for every field from that package. It’s logical - we are not able to predict from which package the developer wants to show an object to the user.

Then, if we’re looking for an object – if it exists - we check if the user has permission to view it. If not, we throw our exception with the message that will be shown to the user. After that, we start to parse fields names in a similar way.

<code style="color: #000000; word-wrap: normal;"> private void parseFieldsString(String namespace, String sobject_internal_name, List<string> field_names){  
      column_metadata = new Map<string, columnmetadata="">();  
      for(String field_name : field_names){  
           column_metadata.put(field_name, new ColumnMetadata(namespace, sobject_internal_name, field_name));  
      }</string,></string></code>

We store the original field name as a map key, declared on the Lightning component level, to connect them with field names from the other attributes like 'FieldLabels' and 'SortableFieldNames'. Also, we use another inner class, ColumnMetadata, which contains information about a single field: it stores field API name, label, display type. Also, we’ve stored overridden display type and flags for showing field values as a reference or to set the column as sortable. The 'Constructor' of this class looks like this:

<code style="color: #000000; word-wrap: normal;"> public ColumnMetadata(String namespace, String sobject_internal_name, String field_name){  
      Map<string, schema.sobjectfield=""> sobject_fields = Schema.getGlobalDescribe().get(sobject_internal_name).getDescribe().fields.getMap();  
      List<string> relationship = field_name.split('\\.');  
      if(relationship.size() > 1){  
           populateRelationshipFieldDescribe(namespace, relationship, sobject_fields);  
      } else {  
           populateValueFieldDescribe(namespace, relationship[0], sobject_fields);  
      }  
      field_override_type = null;  
      field_is_reference = false;  
      field_is_sortable = false;  
 }  
</string></string,></code>

We’re splitting the field by dot to retrieve a list of all relationships if it exists. If yes, then we check if the relationship is valid and if the user has permission to see the relationship field with 'populateRelationshipFieldDescribe' method. We do this for all elements that we've got after splitting, except the last one. The last one element will be processed with 'populateValueFieldDescribe' method which also retrieves field label and display type and assigns it to the suitable class fields.

Returning to 'ColumnMetadataWrapper' constructor, after all fields are successfully parsed, we customize our dynamic table if additional parameters were set on it. Additional parameters are: FieldLabels, OverrideFieldType, ReferenceFields, SortableFieldNames. For the first two parameters, we’re providing the data as a list of String in the format: 'FieldName:Value'. For example, if we want to change the standard label for 'Account__r.Name' field, we put this in attribute:

<code style="color: #000000; word-wrap: normal;"> <c:dynamictable ...="" fieldlabels="['Account__r.Name:Customer Name']">  
</c:dynamictable></code>


For 'ReferenceFields' and 'SortableFieldNames', we’re providing only the field names, as we don’t need to pass additional values.
 

<code style="color: #000000; word-wrap: normal;"> <c:dynamictable ...="" referencefields="['Account__r.Name']">  
</c:dynamictable></code>

After all of the columns, metadata is collected, the object is constructed and serialized into JSON. This format will be used in the component controller to prepare columns and data.

The other two methods in Apex controller are simpler. The first method simply returns an Integer which is a number of all records that match the criteria set by the developer. The second method returns a serialized list of records that match these criteria limited to 2000 records. Records are also sorted by field, default set by the developer on 'SortByField' parameter, later by the user when he clicks on the column header.

Lightning Component

Lightning Component has several attributes. Three of them are required: 'SObjectName', 'FieldNames' and 'MatchCriteria'. Others are optional: some of these, mentioned above, can change the behaviour of the table, some of them concern the appearance of the table header, as well as the list of the available page sizes options and checkboxes to select records. Others are used internally to store data about metadata columns, selected and retrieved records and to show the number of visible records and so on – these have parameters access set to private.

Let’s see how the component works. Everything starts with the initialization of the component. It is done by the component controller method called 'initializeComponent'. It sets the initial values for variables and sets possible values for page size selection. After that, there is a call to our first Apex method to initialize columns. When we receive a response, we store the metadata in one of our attributes, but before it is done, we collect data needed to generate the header of the table, then it is saved to another attribute – TableColumns. The header is generated as follows:

<code style="color: #000000; word-wrap: normal;"> <aura:iteration items="{!v.TableColumns}" var="column">  
      <aura:if istrue="{!column.is_selection_column}"><div class="slds-checkbox_add-button">  
                     <input checked="{!v.AllRecordsSelected}" class="slds-assistive-text" id="{!globalId + 'select-all'}" onchange="{!c.selectAllRecords}" tabindex="-1" type="checkbox" />  
                     <label class="slds-checkbox_faux" for="{!globalId + 'select-all'}">  
                          <span class="slds-assistive-text">Select item</span>  
                     </label></div>  
             
           <aura:set attribute="else">  
                <aura:if istrue="{!column.field_is_sortable}">  
                       
                          <a class="slds-th__action slds-text-link--reset" data-id="{!column.field_name}" onclick="{!c.changeSort}">  
                               <span class="slds-assistive-text" data-id="{!column.field_name}">Sort </span>  
                               <span class="slds-truncate" data-id="{!column.field_name}" title="{!column.field_label}">{!column.field_label}</span>  
                               </a>
<div class="slds-icon_container" data-id="{!column.field_name}"><a class="slds-th__action slds-text-link--reset" data-id="{!column.field_name}" onclick="{!c.changeSort}">  
                                    <c:svgicon category="utility" class="slds-icon slds-icon--x-small slds-icon-text-default slds-is-sortable__icon" name="arrowdown" svgpath="/resource/lightning/https://www.pexlify.com/assets/icons/utility-sprite/svg/symbols.svg#arrowdown">  
                               </c:svgicon></a></div><a class="slds-th__action slds-text-link--reset" data-id="{!column.field_name}" onclick="{!c.changeSort}">  
                          </a>  
                       
                     <aura:set attribute="else">  
                            
                               <span class="slds-truncate" data-id="{!column.field_name}" title="{!column.field_label}">{!column.field_label}</span>  
                            
                     </aura:set>  
                </aura:if>  
           </aura:set>  
      </aura:if>  
 </aura:iteration>  
</code>

After the previous action is done, we are calling for total records and retrieve records. This is done by 'retrieveTotalRecords' and 'retrieveRecords' helper methods respectively. After the second is completed, 'updateTableRows' helper method runs. This method prepares single record columns to be showed in the right way - if it is a reference column, then it should have an ID to a valid record. If display type is a percentage – it should multiply the value by 100. Also, there is provided logic to override display type. We can override fields to several types: Boolean, Currency, Date, Double, Integer, Percent and String – but some types are not possible to override, for example from Email to Currency – we’re not able to convert an email into currency.

However, all types can be easily converted into String – in this way Email, Phone and Website columns are not formatted into links. We can easily override most types to Boolean – if a value exists (and for number types that are not 0) then the checkbox will be checked – otherwise not. In this way, we can show the user that a value is set but we don’t show what the value behind it is.

Besides the main functionality of the Dynamic Table, there are some additional methods which run dynamic tables smoothly. 'updatePagination' helper method ensures that the availability of buttons is correct. It also calculates which records should be shown for the user based on page number.

Let’s have a look at preparing the table rows.

<code style="color: #000000; word-wrap: normal;"> <aura:iteration indexvar="index" items="{!v.TableRows}" var="row">  
        
           <aura:iteration items="{!row}" var="column">  
                <aura:if istrue="{!column.is_selection_column}"><div class="slds-checkbox_add-button">  
                               <input checked="{!column.is_checked}" class="slds-assistive-text" data-id="{!index}" id="{!globalId + '-select-' + index}" onchange="{!c.selectRecord}" tabindex="-1" type="checkbox" />  
                               <label class="slds-checkbox_faux" for="{!globalId + '-select-' + index}">  
                                    <span class="slds-assistive-text">Select item</span>  
                               </label></div>  
                       
                     <aura:set attribute="else"><div class="slds-truncate">  
                                    <aura:if istrue="{!column.value != null}">  
                                         <aura:if istrue="{!column.field_type == 'BOOLEAN'}">  
                                              <aura:if istrue="{!column.reference != null}">  
                                                   <a id="{!column.reference}" onclick="{!c.navigateToSObject}"><ui:outputcheckbox value="{!column.value}"></ui:outputcheckbox></a>  
                                                   <aura:set attribute="else">  
                                                        <ui:outputcheckbox value="{!column.value}">  
                                                   </ui:outputcheckbox></aura:set>  
                                              </aura:if>  
                                         </aura:if>  
                                         <!--—other display types ---->  
                                    </aura:if></div>  
                            
                     </aura:set>  
                </aura:if>  
           </aura:iteration>  
        
 </aura:iteration>  
</code>

At first, we are checking if the column is a selection column – if yes, checkbox field is rendered with attached 'onchange' action. This will set the record as selected and update the attribute. Otherwise, we render an output field which is dependent on the display type. Depending on the type, we render an 'outputCheckbox', 'outputCurrency' and other UI components. We also wrap them inside of the 'a' tag to create a reference to a record, if the column was set as reference column. We navigate to a record through the 'navigateToSObject' controller method.

Selected records are stored originally in the map – the key is a row ID to add them or remove faster.

<code style="color: #000000; word-wrap: normal;"> switchRow : function(component, index, is_checked){  
      let all_records = component.get('v.AllRecords');  
      let first_record_on_page = component.get('v.FirstRecordOnPage');  
      let selected_records_map = component.get('v.SelectedRecordsMap');  
      let index_on_page = (first_record_on_page + index - 1);  
      if(index_on_page <= all_records.length){  
           all_records[index_on_page].is_checked = is_checked;  
           if(is_checked){  
                selected_records_map.set(all_records[index_on_page].Id, all_records[index_on_page]);  
           } else {  
                selected_records_map.delete(all_records[index_on_page].Id);  
                component.set('v.AllRecordsSelected', false);  
           }  
      }  
      this.updateSelectedRecords(component);  
 }  
</code>

After that, 'updateSelectedRecords' is executed to return the selected components to outside of the component by the attribute:

<code style="color: #000000; word-wrap: normal;"> updateSelectedRecords : function(component){  
      let selected_records_map = component.get('v.SelectedRecordsMap');  
      component.set('v.SelectedRecords', Array.from(selected_records_map.values()));  
 }  
</code>
<code style="color: #000000; word-wrap: normal;"> <c:dynamictable ...="" selectedrecords="{!v.selectedRecords}">  
</c:dynamictable></code>

Then, every time the 'selectedRecords' is changed, we get changes to the selection inside of the component.

The whole code for this component is provided and available to download in the package below.

DynamicTable Component Package

Also, you can check out our GitHub page to access and download the code - https://github.com/Pexlify/Generic-Table-For-Lightning-Experience

We hope you found this post interesting and hopefully, this table component will come in useful!

If you are interested in migrating to lightning or other Salesforce services, get in touch today

Get In Touch

Discover how Pexlify can create digital experiences that transforms and optimises your business with Salesforce.

Get Started