https://cdnjs.cloudflare.com/ajax/libs/SyntaxHighlighter/3.0.83/scripts/shAutoloader.js

Saturday, 16 January 2016

Board Anything with SLDS and Lightning Components

Board Anything with SLDS and Lightning Components

Ba meme

Introduction

One of the features I really like in the Lightning Experience is the Opportunity Board (or Opportunity Kanban as it will be known in Spring 16). Not so much the ability to drag and drop to change the stage of an individual opportunity, although that’s very useful for the BrightGen Sales team, but more the visualisation of which state a number of records are in.

This seemed to me something that would useful for a wide range of records - anything with a status-like field really, which lead me to create Board Anything - a Lightning component that retrieves records for a particular SObject type, groups them by the value in a configurable status field and displays them as a table of Salesforce Lighting Design System board tiles - Leads for example:

Screen Shot 2016 01 16 at 17 10 36

Whatever, Where’s The Code

The Board Lightning Component

The board contents are displayed via a Lightning Component. In order for this component to be able to handle any type of sobject, there are a few attributes to configure its behaviour:

<aura:attribute name="SObjectType" type="String" default="Case" />
<aura:attribute name="StageValueField" type="String" default="Status" />
<aura:attribute name="StageConfigField" type="String" />
<aura:attribute name="FieldNames" type="String" default="CaseNumber,Subject" />
<aura:attribute name="ExcludeValues" type="String" default="Closed" />

These attributes influence the component as follows:

  • SObjectType - the type of SObject to display on the board
  • StageValueField - the field containing the stage (or status) values
  • StageConfigField - a picklist field containing the values to display on the board - use this if your StageValueField is a free text field to limit the stages that are displayed on the board
  • FieldNames - comma separated list of fields to display in each tile - the first is used as the title field for the tile
  • ExcludeValues - stage values that should be excluded from the board

Note that the defaults will result in a board of open cases being displayed as follows:

Screen Shot 2016 01 16 at 17 24 05

The component also defines design attributes for each of these attributes, so that you can configure a board via the Lightning Builder.

The component does most of its work on a list of wrapper classes. First it iterates the list  to output the stage headings, which will be in the order returned by the Schema Describe methods for the stage value field or stage config field, depending on which is defined:

 
<aura:iteration items="{!v.Stages}" var="stage">
  <th style="{!'width:' + v.ColumnWidth + '%'}">
     <h3 class="slds-section-title--divider slds-text-align--center">{!stage.stageName}</h3>
  </th>
</aura:iteration>

and then iterates the list again, displaying the tiles for each record in each stage:

<aura:iteration items="{!v.Stages}" var="stage">
  <td style="{!'width:' + v.ColumnWidth + '%; vertical-align:top'}">
    <aura:iteration items="{!stage.sobjects}" var="sobject">
      <ul class="slds-list--vertical slds-has-cards--space has-selections">
        <li class="slds-list__item slds-m-around--x-small">
          <div class="slds-tile slds-tile--board">
            <p class="slds-tile__title slds-truncate slds-text-heading--medium">{!sobject.titleField.fieldValue}</p>
            <div class="slds-tile__detail">
              <aura:iteration items="{!sobject.fields}" var="field">
                <p class="slds-truncate">{!field.fieldName} : {!field.fieldValue}</p>
              </aura:iteration>
            </div>
          </div>
        </li>
      </ul>
    </aura:iteration>
  </td>
</aura:iteration>

The Apex Controller

Most of the heavy lifting is done by the Apex controller, which gets the available stage values for the stage value field or stage config field:

if ( (null==stageConfigField))
{
  stageConfigField=stageValueField;
}
            
Map<String, String> excludeValuesMap=new Map<String, String>();
if (null!=excludeValues)
{
  for (String excludeValue : excludeValues.split(','))
  {
    excludeValuesMap.put(excludeValue, excludeValue);
  }
}
Map<String, BB_LTG_BoardStage> stagesByName=new Map<String, BB_LTG_BoardStage>();

Map<String, Schema.SObjectField> fieldMap =
             Schema.getGlobalDescribe().get(sobjectType).
		getDescribe().fields.getMap();
Schema.DescribeFieldResult fieldRes=
             fieldMap.get(stageConfigField).getDescribe();
            
List<Schema.PicklistEntry> ples = fieldRes.getPicklistValues();
for (Schema.PicklistEntry ple : ples)
{
  String stageName=ple.GetLabel();
  if (null==excludeValuesMap.get(stageName))
  {
    BB_LTG_BoardStage stg=new BB_LTG_BoardStage();
    stg.stageName=ple.GetLabel();
    stagesByName.put(stg.stageName, stg);
    stages.add(stg);
  }
}

Note that BB_LTG_BoardStage wrapper classes are created here - later these will be fleshed out with the records and their details.

It then generates a dynamic SOQL query to retrieve the named fields:

String queryStr='select Id,' + String.escapeSingleQuotes(stageValueField) + ', ' +
String.escapeSingleQuotes(fieldNames) +
                ' from ' + String.escapeSingleQuotes(sobjectType);
List<SObject> sobjects=Database.query(queryStr);

 and finally iterates the results of the query, grouping the records by stage and using dynamic DML to retrieve the field values: 

List<String> fieldNamesList=fieldNames.split(',');
for (SObject sobj : sobjects)
{
  String value=String.valueOf(sobj.get(stageValueField));
  BB_LTG_BoardStage stg=stagesByName.get(value);
  if (null!=stg)
  {
    BB_LTG_BoardStage.StageSObject sso=new
    BB_LTG_BoardStage.StageSObject();
    Integer idx=0;
    for (String fieldName : fieldNamesList)
    {
      fieldName=fieldName.trim();
      fieldRes= fieldMap.get(fieldName).getDescribe();
      BB_LTG_BoardStage.StageSObjectField ssoField=
                      new BB_LTG_BoardStage.StageSObjectField(fieldRes.getLabel(),
             sobj.get(fieldName));
      if (0==idx)
      {
        sso.titleField=ssoField;
      }
      else
      {
        sso.fields.add(ssoField);
      }
      idx++;
    }
    stg.sobjects.add(sso);
  }
}

What About the Example Leads Board?

The example Leads board is built using Lighting Out for Visualforce. As you may remember from my previous blog post on this, first I need to create an app that references the component, in this case BBSObjectBoardOutApp:

<aura:application access="GLOBAL" extends="ltng:outApp">
    <aura:dependency resource="c:BBSObjectBoard" />
</aura:application>

And then I have a Visualforce page (BBLeadBoard) that uses the app to instantiate the component and sets up the attributes to configure the board for leads:

<apex:page sidebar="false" showHeader="false" standardStylesheets="false">
    <apex:includeScript value="/lightning/lightning.out.js" />
    <div id="lightning"/>
 
    <script>
        $Lightning.use("c:BBSObjectBoardOutApp", function() {
            $Lightning.createComponent("c:BBSObjectBoard",
                                       {
					'SObjectType': 'Lead',
					'StageValueField' : 'Status',
					'FieldNames' : 'Company, FirstName, LastName, LeadSource',
					'ExcludeValues': 'Converted',
					},
                  "lightning",
                  function(cmp) {
                    // no further setup required - yet!
              });
        });
    </script>
</apex:page>

That’s All There Is?

No, there’s the wrapper class, a JavaScript controller and helper, but there’s nothing particularly exciting about those so they are available from my BBLDS samples repository at:

https://github.com/keirbowden/BBLDS

This also contains an unmanaged package so you can just install this into your dev org and try out the Visualforce page - check out the README for more information.

Anything Else I Need to Know

Yes.

  • As this is an unmanaged package there are test classes - you’re welcome.
  • The styling of the headings could be improved
  • Errors are written to the debug log and swallowed server side - I did this as otherwise the Lightning Builder displays exceptions all over the place when you try to update the attributes. Its not ideal, so feel free to change it!
  • No drag and drop support to change the stage/status value - while this makes sense for opportunities, it doesn’t for things like cases, so I used that as justification not to do it.

Related Posts

 

7 comments:

  1. Hey Keir,

    I saw a new tag added, apex:includeLightning.
    Can it be used in place of the apex:includescript lightning.out.js?

    ReplyDelete
  2. Yeah - it looks like they've removed the need for us to know what the JavaScript is.

    ReplyDelete
  3. Hey Bob,

    Do you know if replicating the opportunity kanban board Drag and Drop functionality can be done with vanilla Lightning/Aura? or would you have to use another JS library like Dragula?

    Thanks,
    Nathan

    ReplyDelete
  4. Hai Bob, how to retrieve the picklist values in lightning components.

    Thanks......

    ReplyDelete
  5. Hi, Bob
    I stucked on that how to retrieve the picklist values in lightning component ???

    Thanks

    ReplyDelete
  6. Hi Keir,

    As always an excellent post with awesome supporting code! Thanks for all you do for the dev community.

    Mike

    ReplyDelete
  7. Great post, Keir! Hopefully the Kanban view becomes more standard across Lightning, across more objects, and with the ability to customise the field to summarise. (For example, the sum of Lead age in the kanban column header would be great. Imagine it even displaying a trending chart of sum of time open for each stage.... I think I've just talked myself into committing something to your git project! =D )

    ReplyDelete