The Blog - Expert Thoughts & Opinions

Display Tables in a Lightning Component on a Narrow Screen

One of the best features of Salesforce Lightning is that the same application we build with clicks and code is available to our users anywhere. They can use it from their desktop and the Salesforce mobile app. 

We don’t need to create 2 versions of every configuration or development we do. We usually don’t need to worry about adjusting the changes we do for devices with narrow screens like tablets or mobiles either.

Most of the functionality within Salesforce adapts to different screen widths with little to no effort. However, when we develop a different Lightning component, we need to keep in mind that our component needs to still look appealing on all devices. This includes devices with a narrow screen such as tablets and phones. Our Lightning components will be available on the Salesforce mobile app, and we want to make sure they look nice. 

A common problem with mobile devices is displaying tables. Tables are a great way to visualize data, but naturally, tables take a lot of width. Tables and mobile devices do not usually work well together. If you’ve never tried, it’s usually not a great experience trying to scroll horizontally across a table while also scrolling vertically, all on the little screen of your mobile device. 

One solution to this problem is to visualize tables differently on devices with narrow screens. We prefer to use tiles. If you’ve never heard of tiles, you might be surprised to know that you are very familiar with them as Salesforce uses them all the time. Tiles in Salesforce look like this:

Let's take the example from our previous blog post where we created a Lightning component which can be embedded into a flow. That component will show a list of quotes under an opportunity. It looks like this:

This looks fine on wide screens, but on narrow screens, the same table won’t look as appealing. What we want to do is show a tiles-based view for narrow screens and the table for wide screens. How do we do that?

First, we need to add a CSS file to our component which will show and hide sections based on the screen width. In our example it looks like this:

@media (max-width: 800px) {
    .records-table {
        display: none;
    }
}

@media (min-width: 800px) {
    .records-tiles {
        display: none;
    }
}

Then, in our component, we will have both views, the table and the tiles view. Our CSS will make sure only the right view appears to the user. This is how our component will now look for a narrow screen:

You can check in our previous blog post where we explained the code of our component. These are the changes we’ve made to include both the tiles view and the table view:

FlowTableSelection.cmp:

<aura:component controller="FlowTableSelectionController" access="global" 
implements="lightning:availableForFlowScreens"> 

      <aura:attribute name="FieldLabels" type="List"/> 
      <aura:attribute name="TableRows" type="List"/> 
      <aura:attribute name="ObjectAPIName" type="String"/> 
      <aura:attribute name="WhereClause" type="String"/> 
      <aura:attribute name="FieldSetName" type="String"/> 
      <aura:attribute name="SelectedRecordId" type="String"/> 
      <aura:attribute name="AllowSelection" type="Boolean"/> 
      <aura:attribute name="NoRecords" type="Boolean" default="true"/> 
      <aura:attribute name="NoRecordsMessage" type="String"/> 

      <ltng:require styles="{!$Resource.FlowTableCSS}"/> 

      <aura:handler name="init" value="{!this}" action="{!c.doInit}"/> 

      <aura:renderIf isTrue="{!not(v.NoRecords)}"> 
            <table class="slds-table slds-table_bordered slds-table_striped 
slds-table_fixed-layout records-table" style="border: 1px solid rgb(217, 219, 221);"> 
               <thead> 
                  <tr class="slds-text-title_caps"> 
                     <aura:iteration items="{!v.FieldLabels}" var="field_label" > 
                        <th> 
                           <div class="slds-truncate">{!field_label}</div> 
                        </th> 
                     </aura:iteration> 
                  </tr> 
               </thead> 
               <tbody> 
                  <aura:iteration items="{!v.TableRows}" var="row"> 
                     <tr> 
                        <aura:iteration items="{!row.Fields}" var="field"> 
                           <td class="slds-truncate"> 
                              <lightning:layout > 
                                 <aura:renderIf isTrue="{!and(v.AllowSelection, field.FirstField)}"> 
       <lightning:layoutItem class="slds-m-right_x-small"> 
       <lightning:input type="radio" label="" name="SelectedRecord" value="{!row.RecordId}" variant="label-hidden" 
onchange="{!c.handleRadioClick}"/> 

       </lightning:layoutItem> 
                                 </aura:renderIf> 
                                 <lightning:layoutItem class="slds-truncate"> 
       <c:OutputFormattedField Field="{!field}"/> 
       </lightning:layoutItem> 
                                 </lightning:layout> 
                           </td> 
                        </aura:iteration> 
                     </tr> 
                  </aura:iteration> 
               </tbody> 
            </table> 
            <div class="records-tiles"> 
               <aura:iteration items="{!v.TableRows}" var="row"> 
                  <lightning:tile label="{!row.Fields[0].Value}"> 
                     <aura:set attribute="media"> 
                        <aura:renderIf isTrue="{!v.AllowSelection}"> 
                           <lightning:layoutItem class="slds-m-right_x-small"> 
                              <lightning:input type="radio" label="" name="SelectedRecord" value="{!row.RecordId}" 
variant="label-hidden" onchange="{!c.handleRadioClick}"/> 
                           </lightning:layoutItem> 
                        </aura:renderIf> 
                     </aura:set> 
                     <dl class="slds-dl_horizontal"> 
                        <aura:iteration items="{!row.Fields}" var="field"> 
                           <dt class="slds-dl_horizontal__label"> 
                              <p class="slds-truncate" title="{!field.FieldLabel}">{!field.FieldLabel}:</p> 
                           </dt> 
                           <dd class="slds-dl_horizontal__detail slds-tile__meta"> 
                              <p class="slds-truncate" title="{!field.Value}"><c:OutputFormattedField Field="{!field}"/></p> 
                           </dd> 
                        </aura:iteration> 
                     </dl> 
                  </lightning:tile> 
               </aura:iteration> 
            </div> 
            <aura:set attribute="else"> 
               <div class="slds-align_absolute-center"> 
                  {!v.NoRecordsMessage} 
               </div> 
            </aura:set> 
       </aura:renderIf> 
</aura:component>

Note how both sections appear on the component but the CSS classes we added hide and show them based on the screen width. Only one of them will appear on the screen at any time.

This is the remaining of our code to make this happen.

OutputFormattedField.cmp:

<aura:component> 
       <aura:attribute name="Field" type="Object"/> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'BOOLEAN')}"> 
              <lightning:input type="checkbox" disabled="true" label="" 
value="{!v.Field.Value}" variant="label-hidden"/> 
          </aura:renderIf> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'CURRENCY')}"> 
             <lightning:formattedNumber value="{!v.Field.Value}" style="currency" maximumFractionDigits="2"/> 
          </aura:renderIf> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'PERCENT')}"> 
             <lightning:formattedNumber value="{!v.Field.Value}" style="percent" maximumFractionDigits="2"/> 
          </aura:renderIf> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'DOUBLE' || v.Field.FieldType == 'LONG' || 
v.Field.FieldType == 'INTEGER')}"> 
             <lightning:formattedNumber value="{!v.Field.Value}" style="decimal" maximumFractionDigits="2"/> 
          </aura:renderIf> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'PHONE')}"> 
             <lightning:formattedPhone value="{!v.Field.Value}"/> 
          </aura:renderIf> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'DATE' || v.Field.FieldType == 'TIME' || 
v.Field.FieldType == 'DATETIME' || v.Field.FieldType == 'STRING' || v.Field.FieldType == 'PICKLIST' || 
v.Field.FieldType == 'MULTIPICKLIST' || v.Field.FieldType == 'ADDRESS' || v.Field.FieldType == 'ID' || 
v.Field.FieldType == 'REFERENCE' || v.Field.FieldType == 'EMAIL')}"> 
             {!v.Field.Value} 
          </aura:renderIf> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'TEXTAREA')}"> 
             <aura:unescapedHtml value="{!v.Field.Value}"/> 
          </aura:renderIf> 
          <aura:renderIf isTrue="{!(v.Field.FieldType == 'URL')}"> 
             <lightning:formattedUrl value="{!v.Field.Value}" /> 
          </aura:renderIf> 
</aura:component>

FlowTableSelectionController:

public class FlowTableSelectionController { 
      @AuraEnabled 
   public static ApexMethodResult getRecordsToDisplayInTable(String sobject_name, String field_set_name, String 
where_clause){ 
      ApexMethodResult apex_method_result = new ApexMethodResult(); 
      try { 
            Schema.FieldSet field_set = getFieldSetForObject(sobject_name, field_set_name); 
            apex_method_result.setFieldLabels(field_set); 
            String query = getQueryForOjbectFieldSetAndWhereClause(sobject_name, field_set, where_clause); 
            List<sObject> table_records = Database.query(query); 
            for(sObject table_record : table_records){ 
               TableRow table_row = new TableRow(table_record, field_set); 
               apex_method_result.TableRows.add(table_row); 
             } 
      } catch(Exception e){ 
         apex_method_result.handleException(e); 
      } 
      return apex_method_result; 
    } 


      private static Schema.FieldSet getFieldSetForObject(String sobject_name, String field_set_name){ 
         Map<String, Schema.SObjectType> global_describe = Schema.getGlobalDescribe(); 
         if(!global_describe.containsKey(sobject_name)){ 
            throw new FlowTableSelectionException('Bad object specified ' + sobject_name); 
         } 
         Schema.SObjectType sobject_type = global_describe.get(sobject_name); 
         Schema.DescribeSObjectResult sobject_type_describe = sobject_type.getDescribe(); 

        if(!sobject_type_describe.FieldSets.getMap().containsKey(field_set_name)){
              throw new FlowTableSelectionException('Can\'t find fieldset ' + field_set_name); 
           } 
           return sobject_type_describe.FieldSets.getMap().get(field_set_name); 
        } 

        private static String getQueryForOjbectFieldSetAndWhereClause(String sobject_name, Schema.FieldSet field_set, 
String where_clause){ 
           List<String> fields_api_name = new List<String>(); 
           for(Schema.FieldSetMember fieldset_member : field_set.getFields()){ 
              fields_api_name.add(fieldset_member.getFieldPath()); 
           } 
           String query = 'SELECT ' + String.join(fields_api_name, ', ') + ' FROM ' + sobject_name; 
           if(!String.isBlank(where_clause)){ 
              query += ' WHERE ' + where_clause; 
           } 
           return query; 
       } 

       @TestVisible 
     private class ApexMethodResult { 
       @AuraEnabled 
       public List<TableRow> TableRows; 
          @AuraEnabled 
       public List<String> FieldLabels; 
       @AuraEnabled 
       public Boolean Success; 
       @AuraEnabled 
       public String ErrorMessage; 

       public ApexMethodResult(){ 
             this.Success = true; 
          this.TableRows = new List<TableRow>(); 
          this.FieldLabels = new List<String>(); 
       } 
   
          public void handleException(Exception e){ 
             this.Success = false; 
             this.ErrorMessage = e.getMessage(); 
          } 

          public void setFieldLabels(Schema.FieldSet field_set){ 
             for(Schema.FieldSetMember fieldset_member : field_set.getFields()){ 
                this.FieldLabels.add(fieldset_member.getLabel()); 
             } 
          } 
     } 

        @TestVisible 
     private class TableRow { 
           @AuraEnabled 
       public List<Field> Fields; 
           @AuraEnabled 
       public String RecordId; 

            public TableRow(sObject record, Schema.FieldSet field_set){ 
               this.RecordId = record.Id; 
               this.Fields = new List<Field>(); 
               for(Schema.FieldSetMember fieldset_member : field_set.getFields()){ 
                  Field table_row_field = new Field(record, fieldset_member, this.Fields.isEmpty()); 
                  this.Fields.add(table_row_field); 
               } 
          
            } 
     } 

      @TestVisible 
   private class Field { 
         @AuraEnabled 
     public String Value; 
         @AuraEnabled 
     public String FieldType; 
         @AuraEnabled 
     public String FieldLabel; 
         @AuraEnabled public Boolean FirstField; 

         public Field(sObject record, Schema.FieldSetMember fieldset_member, Boolean first_field){ 
            this.FirstField = first_field; 
            String field_api_name = fieldset_member.getFieldPath(); 
            this.FieldType = String.valueOf(fieldset_member.getType()); 
            this.FieldLabel = fieldset_member.getLabel(); 
            if(record.get(field_api_name) != null){ 
               if(this.FieldType == 'DATE'){ 
                  this.Value = ((Date)record.get(field_api_name)).format(); 
               } else if (this.FieldType == 'DATETIME'){ 
                  this.Value = ((DateTime)record.get(field_api_name)).format(); 
               } else if (this.FieldType == 'PERCENT'){ 
                  this.Value = String.valueOf((Decimal)record.get(field_api_name) / 100.0); 
               } else { this.Value = String.valueOf(record.get(field_api_name)); 
               } 
             } 
         } 
      } 

      @TestVisible 
    private class FlowTableSelectionException extends Exception {} 
}