LWC Patterns: Dynamic Record Access

It is not uncommon for requirements to change at some point in the future. We would like to think the code that we write will be used forever and we will never need to touch it again, but in today’s fast paced world of ever changing requirements, that is usually not the case. That is why with Salesforce, it is typically best practice to do as much as possible with admin configuration as opposed to code. When we do need to turn to code, we can implement that same philosophy so an admin can change the way the code behaves instead of a developer. We are going to walk through some patterns to do that with Lightning Web Components (LWC), the first of which is Dynamic Record access, also known as the wire adapter uiRecordApi.

Ever have the need to have the same component function on two different objects? What about different fields across those objects in different components? You could copy the entire component and change the objects and fields you need, but once you get beyond two or three you could end up with a bunch of singular components to maintain. We are going to look at a pattern for pulling dynamic objects and fields out of the uiRecordApi function by walking through building a component that displays a single admin defined field on any object.

We are going to create a Lightning Web Component Bundle called objectFields

Screen Shot 2020-04-30 at 3.02.57 PM

The first thing you start with is your metadata file. This file is like the guidemap for our component.

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>47.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Object Field</masterLabel>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
    <targetConfigs>
      <targetConfig targets="lightning__RecordPage">
        <property label="Field" name="field" type="String" default="" required="true" datasource="apex://LWCConfigurationObjectFieldsPicklist" />
      </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

The most important parts of this are the targets tag, which sets our component to be available on record pages, and the property tag within the targetConfig for record pages. The name property “field” we will use in our controller a little later. The datasource property on the property tag tells the app builder that we want to get the values from referenced Apex class.

global class LWCConfigurationObjectFieldsPicklist extends VisualEditor.DynamicPickList{
    VisualEditor.DesignTimePageContext context;

    global LWCConfigurationObjectFieldsPicklist(VisualEditor.DesignTimePageContext context) {
       this.context = context;
    }

    // No Default
    global override VisualEditor.DataRow getDefaultValue() {
        return null;
    }

    static VisualEditor.DynamicPickListRows picklistRows;
    global override VisualEditor.DynamicPickListRows getValues() {
        // Get Picklist rows of every object field
        if (picklistRows == null) {
            List<PicklistSortHelper> fields = new List<PicklistSortHelper>();
            if (context.pageType == 'RecordPage') {
                // Get Fields from Object
                Schema.DescribeSobjectResult[] results = Schema.describeSObjects(new String[] { context.entityName });
                for (Schema.DescribeSobjectResult res : results) {
                    for (Schema.sObjectField field : res.fields.getMap().values()) {
                        Schema.DescribeFieldResult f = field.getDescribe();
                        fields.add(new PicklistSortHelper(f.getLabel(), f.getName()));
                    }
                }
            }
            fields.sort();

            // Add Rows to Picklist
            VisualEditor.DynamicPickListRows rows = new VisualEditor.DynamicPickListRows();
            for (PicklistSortHelper f : fields) {
                rows.addRow(new VisualEditor.DataRow(f.label, f.name));
            }
            picklistRows = rows;
        }
        return pickListRows;
    }

    // Sorting Helper for Picklist Items
    private class PicklistSortHelper implements Comparable {

        String name;
        String label;

        PicklistSortHelper(String label, String name) {
            this.label = label;
            this.name = name;
        }

        public Integer compareTo(Object compareTo) {
            return this.label.compareTo(((PicklistSortHelper)compareTo).label);
        }

    }
}

By extending the VisualEditor.DynamicPickList class, we implement the pattern to return a list of picklist choices to the Lightning App Builder. In this case, we use the Schema describe calls to get a list of fields available on the object. The important part of this class is the context variable that is passed in from the constructor. This gives us the pageType (in this case, we are ensuring we are using this on a RecordPage, and the entityName, which is the API name the object this component is being used on.

Then when we go to use the component in the lightning app builder, the choices for our field property show a list of fields available on the SObject we are editing the Record Page for.

Object Fields Property Selection

Then we complete the rest of our component with the HTML template and Javascript controller.
For this example, we will use a very simplistic template that just shows the field label and value in a lightning card base component.

<template>
  <lightning-card>
    <div class="slds-card__body--inner">
      <div class="slds-form-element__label" >{fieldLabel}</div>
      <p><lightning-formatted-text value={fieldValue}></lightning-formatted-text></p>
    </div>
  </lightning-card>
</template>
import { LightningElement, track, api, wire } from 'lwc';
import { getRecord } from "lightning/uiRecordApi";
import { getObjectInfo } from 'lightning/uiObjectInfoApi';

export default class ObjectFields extends LightningElement {

  @api recordId;
  @api objectApiName;
  @api field;

  @track fieldValue;
  @track fieldLabel;
  @track fields;

  connectedCallback() {
    // Create Fields to pull in the data service
    let fs = [];
    fs.push(this.objectApiName + ".Id");
    fs.push(this.objectApiName + "." + this.field);
    this.fields = fs;
  }

  @wire(getRecord, { recordId: "$recordId", fields: "$fields" })
  wiredRecord({ error, data }) {
    if (data) {
      this.fieldValue = data.fields[this.field].value
    }
  }

  @wire(getObjectInfo, { objectApiName: "$objectApiName" })
  wiredRecordInfo({ error, data }) {
    if (data) {
      this.fieldLabel = data.fields[this.field].label
    }
  }

}

Lets step through the controller. First we import getRecord and getObjectInfo, which we will use for the wired methods for the data and labels respectively.

Then we add the two included $api properties, recordId and objectApiName. These will be auto-populated by the record page for the id of the record we are looking at and the API name, which we need to grab the values.

The last @api property is called field, which corresponds to the metadata property we set up in the first step. This is the value passed in by the Lightning App Builder page configuration.

The next three tracked properties include variables to hold the two outputs which we show on the template for fieldValue and fieldLabel, and a variable to hold the fields we will dynamically pass to the record api.

In the connectedCallback method, we build the array of fields we need to pass by concatenating the objectApiName and the field that was selected in the App Builder. This array ends up looking something like: [Contact.Id, Contact.FirstName].

Then we access the two imported methods with @wire decorators. The syntax for dollar sign variable name ($fields) indicates when that value changes, it will fire the wired method. Both of these methods will fire as soon as the page is accessed because the api properties and our property in the connected callback are set. Then we dynamically access the returned data by using array notation on the returned data to set the fieldValue and fieldLabel properties.

The results look something like this for the Contact First Name field:
Dynamic Field Result

With this pattern you are given admins a lot of control over what data to use in the component and you can build flexible components that work across multiple objects by allowing an admin to specify which field on the object contains the data your component needs.

Restricted

Salesforce Communities: Redirecting Standard URLs

Many times in your Visualforce + Tabs Community you want to override a standard page with a custom one, but since it is currently not possible to override a standard page based on profile, you would have to override the page for internal users as well. If you have many objects in your community, this could create many custom pages to maintain. If you are linking from another custom page, you can control the URL to only your custom page, but you cant control the URL in Chatter feeds or Chatter notifications. You also need to consider the enterprising users who know how a Salesforce URL is constructed and tries to go directly to your internal pages.

You don't want your users looking behind the curtain.

You don’t want your users looking behind the curtain.

One workaround is to use Javascript to keep people out of where you don’t want them.

   window.location.replace(window.parent.location.origin + '/community/yourCommunityHomePageHere')

Now, what if you need to add more logic to your redirection? You could keep modifying your Javascript with if statements depending on the request URL, or we can use a Visualforce page helper to tell the standard Javascript where we want the redirect to go. This keeps all of the logic in Apex where it is easier to maintain and test. You could do this purely in Javascript.

We are going to modify the header injection that we used in the post for adding Visualforce styles into the standard pages.

<script>
	var xmlhttp;
	if (window.XMLHttpRequest) { // code for IE7+, Firefox, Chrome, Opera, Safari
	  xmlhttp=new XMLHttpRequest();
	} else { // code for IE6, IE5
	  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
	}
	xmlhttp.onreadystatechange = function() {
	  if (xmlhttp.readyState==4 && xmlhttp.status==200) {
	    window.location.replace(window.parent.location.origin + xmlhttp.responseText);
	  }
	}
	window.stop();
	xmlhttp.open("GET","/customer/CCRedirect?original=" + 
               encodeURIComponent(window.parent.location.pathname + window.parent.location.search),false);
	xmlhttp.send();
</script>

This page does an XMLRequest callout to our Visualforce page with our logic and then does a JavaScript redirect of the browser. The window.stop() command prevents the browser from loading the regular page and creating a seizure inducing flash effect. Here is the page we call:

<apex:page controller="CCRedirectController" applyHtmlTag="false" showHeader="false" 
   contentType="application/json">
{!url}
</apex:page>
public with sharing class CCRedirectController {
	
	public String url {get;set;}
	
	public CCRedirectController() {

		String originalUrl = EncodingUtil.urlDecode(ApexPages.currentPage().getParameters().get('original'), 
                     'UTF-8');
		
		PageReference pageRef = Page.CCHome;
		url = pageRef.getUrl();
		url = '/customer' + url.subString(5, url.length());
	}
}

You will need to change /customer to the root of your community. To finish it off, our custom page that will be the target of the redirect

<apex:page showHeader="false" >
    <h1>This is the Community Home Page</h1>
</apex:page>

The result is a redirect to our custom home page whenever a standard page is accessed. The original URL parameter can be used to determine what the user was trying to access and redirect conditionally on what that request was. This can be useful for capturing links from Chatter feeds or emails. Here is an example of the CCRedirect class that redirects to a custom Case Visualforce page and passes the original request id to that page.

public with sharing class CCRedirectController {
	
	public String url {get;set;}
	
	public CCRedirectController() {
		
		PageReference pageRef;
		
		String originalUrl = EncodingUtil.urlDecode(ApexPages.currentPage().getParameters().get('original'), 
                      'UTF-8');
		String[] splitUrl = originalUrl.split('[\\/\\?]');
		
		pageRef = Page.CCHome;
		
		if (splitUrl.size() >= 3) {
			String id = splitUrl[2];
			if (id.startsWith(Case.sObjectType.getDescribe().getKeyPrefix())) {
				pageRef = Page.CCCaseView; 
				pageRef.getParameters().put('id', id);
			}
		}
		
		url = pageRef.getUrl();
		url = '/customer' + url.subString(5, url.length());
		
	}
}

If you need to conditionally redirect based on the request, you can return an empty string on the Redirect page, in your header javascript check for an empty response, and then do not perform the redirect. This example checks for the standard chatter urls and does not redirect:

<script>
		var xmlhttp;
		if (window.XMLHttpRequest) { // code for IE7+, Firefox, Chrome, Opera, Safari
		  xmlhttp=new XMLHttpRequest();
		} else { // code for IE6, IE5
		  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
		}
		xmlhttp.onreadystatechange = function() {
		  if (xmlhttp.readyState==4 && xmlhttp.status==200) {
		    if (xmlhttp.responseText.trim() != '') {
		      window.location.replace(window.parent.location.origin + xmlhttp.responseText);
	      }
		  }
		}
		xmlhttp.open("GET","/customer/CCRedirect?original=" + 
                     encodeURIComponent(window.parent.location.pathname + window.parent.location.search),false);
		xmlhttp.send();
</script>
public with sharing class CCRedirectController {
	
	public String url {get;set;}
	
	public CCRedirectController() {
		
		PageReference pageRef;
		
		String originalUrl = EncodingUtil.urlDecode(ApexPages.currentPage().getParameters().get('original'), 
                  'UTF-8');
		String[] splitUrl = originalUrl.split('[\\/\\?]');
		
		pageRef = Page.CCHome;
		
		if (splitUrl.size() >= 3) {
			String id = splitUrl[2];
			if (id.startsWith(Case.sObjectType.getDescribe().getKeyPrefix())) {
				pageRef = Page.CCCaseView; 
				pageRef.getParameters().put('id', id);
			}
			if (splitUrl[2] == '_ui' && splitUrl[3] == 'core') {
				pageRef = null;
			}
		}
		
		if (pageRef != null) {
			url = pageRef.getUrl();
			url = '/customer' + url.subString(5, url.length());
		}
	}
}
The result should look something like this

Salesforce Communities: Visualforce Header on Standard Pages without IFrames

The out-of-the-box Communities setup allows you to add a custom header and footer to your standard pages. However, the the header can only be an image file or HTML file and the footer needs to be HTML. What is additionally complicated is that these files must uploaded as a Document object, so it cannot be managed like regular code, and a new version cannot be uploaded while set in the Community Branding section. What do you do if you want to use standard pages, but need to dynamically show some information in the header based on the user, or if you want to use the same header style on both standard pages and visualforce templates and only want to maintain one version?

Enter the Visualforce injection pattern.

The basic concept is add some Javascript to the HTML header that will call a visualforce page and inject that into the standard page. We will put the header items in a Visualforce Component, that way we can reuse it on Visualforce templates if we need to in the future. This example is overly simplistic, but designed to just demonstrate the concept.

<apex:component controller="CCHeaderController">
	<apex:outputPanel id="ccHeader" layout="block" style="margin:40px;">
		<apex:outputText value="Hello {!userName}!" />
	</apex:outputPanel>
</apex:component>
public with sharing class CCHeaderController {
	
	public String userName {get; set;}
	
	public CCHeaderController() {
		userName = UserInfo.getName();	
	}
}
<apex:page showHeader="false" standardStyleSheets="false">
	<c:CCHeader />
</apex:page>
<script>
	var xmlhttp;
	if (window.XMLHttpRequest) { // code for IE7+, Firefox, Chrome, Opera, Safari
	    xmlhttp=new XMLHttpRequest();
	} else { // code for IE6, IE5
	    xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
	}
	xmlhttp.onreadystatechange = function() {
	    if (xmlhttp.readyState==4 && xmlhttp.status==200) {
	        document.write(xmlhttp.responseText);
	    }
	}
	xmlhttp.open("GET","/customer/CCStandardHeader",false);
	xmlhttp.send();
</script>
<div class="cc_body">

You will need to change the /customer path to whatever you have for your community. Also we have added the open div tag so we can wrap the rest of the standard document body. We can close this with the HTML file used in the Community footer file.

The result should look something like this

The result should look something like this

You can also add actions into your controller

<apex:component controller="CCHeaderController">
	<apex:outputPanel id="ccHeader" layout="block" style="margin:40px;">
		<apex:outputText value="Hello {!userName}!" />
                <apex:form >
		       <apex:commandButton value="Change Name" action="{!changeName}" rerender="ccHeader" />
	        </apex:form>
        </apex:outputPanel>
</apex:component>
public with sharing class CCHeaderController {
	
	public String userName {get; set;}
	
	public CCHeaderController() {
		userName = UserInfo.getName();	
	}

        public PageReference changeName() {
		userName = 'Another Name';
		return null;
	}
}
After clicking the button

After clicking the button

There you go. I have to admit I have not tested the IE5 and IE6 part to see how well it works there, but my guess is that if your users are using IE5 or IE6, you have bigger problems to worry about.

bf4ki

Building Custom Salesforce.com Communities

As developers, we do not always hear “don’t reinvent the wheel”.

Edited-Caveman-cartoon

But what happens when the wheel is brown and we want it to be purple, or it only rotates clockwise and we need to go counter-clockwise.

One of the beautiful things about building a community on Salesforce is that most of the core functionality (Chatter) is already pre-built for you. Sometimes we just want to extend that functionality, or customize the look-and-feel of the community to fit your company’s style. Communities allow us add Visualforce and Apex (based on existing capabilities of Force.com Sites) to build pretty much whatever we want, but we don’t necessarily want to use it for everything.

bf4ki

Before building our community, I saw many examples of highly custom and complex sites built on Salesforce Communities, but I did not find a whole lot of resources to help guide developers as to how they were accomplished. The series of blog posts I hope to show some of the techniques I have developed to work around some of the “quirks”, we shall call them, of trying to build highly customized and complex communities while sometimes staying true to that “don’t reinvent the wheel” mentality.