Build UI for Dependent Picklists
Learning Objectives
- Make a request to User Interface API to get all picklist values for a record type.
- Name the property that contains a map of an object’s dependent fields.
- Name the property that contains a list of a field’s controlling fields.
What’s Special About Dependent Picklists?
It’s tricky to build a user interface for dependent picklist fields, but User Interface API
makes it easier. The values of a dependent picklist field are filtered based on a selection in
another picklist or checkbox (called the controlling field). Look at this screenshot of the
dependent picklist editor in the Record Viewer app. A Country picklist controls a State/Province
picklist (which depends on the Country picklist). When a user selects a country, the
State/Province values are filtered to match. When a user selects a state or a province, that
selection filters the City values.
You get the idea—dependent picklists make it faster to fill out a form because the values are filtered so you spend less time scrolling. And a dependent picklist editor makes it easier to edit related fields, especially on a mobile device. Imagine how difficult it could be to scroll from field to field as dependent values change. It’s much easier to change all the related picklist values in one dialog box.
But it’s these relationships between the controlling fields and the dependent fields that make it difficult to build a user interface. A picklist (or a checkbox) can control multiple dependent picklists. And a dependent picklist is controlled but it can also control. In fact, a dependent picklist can control several other dependent picklists. The dependent picklist field hierarchy is a tree. And an object can have a forest of trees.
- objectInfo.dependentFields contains a map of an object's dependent fields.
- objectInfo.fields[fieldName].controllingFields contains a list of a field's controlling fields.
GET /ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId}
The Record Viewer app uses these properties and resource to build its dependent picklist editor.
Use the Dependent Picklist Editor
To see the dependent picklist editor in the Record Viewer app, create a dependent relationship between at least two fields on an object.
In your Trailhead Playground, create a field dependency for the Account object. Then we’ll edit an account in the Record Viewer app to see the dependent picklist editor in action.
- From Setup, enter object and select Object Manager.
- Select .
- Click New.
- Let’s create a relationship between two existing fields. For the controlling field, select Rating. For the dependent field, select Customer Priority. Click Continue.
- Edit the field dependencies so that Hot includes High, Warm includes Low and Medium, and
Cold includes Low. Click Save.
- Open the Record Viewer app and select an account from the Recent Items List.
- Click Edit.
- Next to the Account Rating or Customer Priority fields, click the pencil icon.
The dependent picklist editor dialog box opens. Use it to select a value for both fields. When you select a value for one field, the values for the other field are filtered based on your selections in Setup.
How Dependent Picklists Are Exposed in User Interface API
Dependent picklist fields are part of a dependency tree, which is a hierarchy of controlling and dependent fields. Each node in a tree can have any number of child nodes. A node without any children is called a leaf.
Our sample dependency tree is simple because it has one root field, Rating, which controls one field CustomerPriority__c. If Rating controlled
another field, our dependency hierarchy would start to look like a tree. But with only two
fields, our dependency tree doesn’t have any branches, it’s just a trunk.
An object can have any number of these dependency trees. To build UI that lets users edit dependent picklists, you need to know the complete hierarchy of each tree.
For example, the Account object could have two dependency trees, one whose root is Country, and one whose root is Rating. These dependency trees from our examples are simple, because each controlling field controls only one dependent field. You can imagine, though, that the Country field, in addition to controlling the State/Province field, could also control a Language field and a Time Zone field.
As you’ve learned, User Interface API has two response properties that provide the field
dependency structure. The properties are both included in the objectInfo payload, which is part of the response from /ui-api/record-ui/{recordIds}. To see the JSON response, open an account object in
the Record Viewer app, and click Show JSON.
- objectInfo.dependentFields
- This property provides the dependent field trees for all the fields on an object. This
sample JSON has one root field, Rating, but an
object could have multiple root
fields.
"objectInfos" : { "Account" : { ... "dependentFields" : { "Rating" : { "CustomerPriority__c" : { } } }, ...
- When an object has an extended hierarchy of dependent fields, like our Country,
State/Province, City example, the dependentFields
property nests them until it reaches a leaf node, like
City.
"objectInfos" : { "Account" : { ... "dependentFields": { "Country__c": { "StateProvince__c": { "City__c": {} } } }, ...
- objectInfo.fields[fieldName].controllingFields
- This property provides the list of fields that control this particular field, starting
from the field that immediately controls this field. In our simple example, the CustomerPriority__c field has one controlling field,
Rating.
"objectInfos" : { "Account" : { ... "fields" : { ... "CustomerPriority__c" : { "apiName" : "CustomerPriority__c", ... "controllingFields" : [ "Rating" ], ...
- When an object has an extended hierarchy of controlling fields, like our Country,
State/Province, City example, the controllingFields
property lists them in order starting with the immediate controlling
field.
"objectInfos" : { "Account" : { ... "fields" : { ... "City__c" : { "apiName" : "City__c", ... "controllingFields" : [ "StateProvince__c", "Country__c"], ...
GET /ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId}
"CustomerPriority__c" : { "controllerValues" : { "Hot" : 0, "Warm" : 1, "Cold" : 2 }, "defaultValue" : null, "url" : "/services/data/v48.0/ui-api/object-info/Account/picklist-values/012000000000000AAA/CustomerPriority__c", "values" : [ { "attributes" : null, "label" : "High", "validFor" : [ 0 ], "value" : "High" }, { "attributes" : null, "label" : "Low", "validFor" : [ 1, 2 ], "value" : "Low" }, { "attributes" : null, "label" : "Medium", "validFor" : [ 1 ], "value" : "Medium" } ] },
The controllerValues property provides the immediate controlling field’s picklist values and indexes. So in our example, the field is Customer Priority, and the immediate controlling field is Rating. The Rating values are Hot, Warm, and Cold, and the indexes are 0, 1, and 2.
For each Customer Priority value (High, Low, and Medium), the validFor property contains the Rating values that, when selected in a picklist, include the Customer Priority value, instead of filtering it out.
For example, when Rating is Hot (0), the Customer Priority picklist is filtered to include only High. When the Rating is Warm (1), the Customer Priority picklist is filtered to include Low and Medium. When Rating is Cold (2), the Customer Priority picklist is filtered to include only Low.
Now that we understand what information we need and how to get it from User Interface API, let’s look at how the Record Viewer app builds its dependent picklist editor.
Get the Picklist Values
When a user clicks to Create, Clone, or Edit a record, the Record Viewer app prefetches the picklist values for all the fields on the object for the specified record type.
You can see the code in RecordViewerWrapper.js. Look for a call to dispatch(actions.fetchPicklists(creds, apiName, recordType)) for each scenario: onNewRecordClick, onCloneClick, and onEditClick.
/* /sagas/picklistFetcher.js */ let url = action.creds.instanceUrl + '/services/data/v48.0/ui-api/object-info/' + action.apiName + '/picklist-values/' + action.recordType + '/';
/* /reducers/picklists.js */ const picklists = (state = {values: undefined}, action) => { switch (action.type) { case 'RECEIVE_PICKLISTS': return { fieldValues : action.result.picklistFieldValues } case 'FETCH_PICKLISTS': // Clear values when new collection is requested. return { fieldValues: undefined } default: return state } } export default picklists
Display the Dependent Picklist Editor
When a record is in edit mode, the app displays a pencil icon beside dependent fields to
indicate that the user can open the dependent picklist editor.
When the user clicks the pencil icon, the app opens the dependent picklist editor as a modal
dialog box. In code, the dependent picklist editor is called the DepGraphEditor
component.
/* /components/RecordRow.js */ if (objectInfo && ((component.field && component.field in objectInfo.dependentFields) || (component.fieldInfo && component.fieldInfo.controllingFields.length > 0))) { // Last field in controlling fields is the root field let lastControllingIndex = component.fieldInfo.controllingFields.length - 1 let rootField = null if (component.field in objectInfo.dependentFields){ rootField = component.field } else { rootField = component.fieldInfo.controllingFields[lastControllingIndex] } // Retrieve the picklist fields that need to show up. const subfieldTree = objectInfo.dependentFields[rootField] const modalFields = getFlattenedTree(objectInfo, subfieldTree, rootField) // Open a modal on click of the input field. let fieldTree = {} fieldTree[rootField] = subfieldTree return ( <div> <label key={'componentInput' + itemLabel + ',' + i} onClick={(event) => onEditDepGraph(picklists, modalFields, editValues, fieldTree, uiMode.toString())} value={currPicklistValue}> {editValues[component.field].current} </label> <button className="fa fa-pencil" key={'componentInput' + itemLabel + 'button,' + i} onClick={(event) => onEditDepGraph(picklists, modalFields, editValues, fieldTree, uiMode.toString())}> </button> </div> ); }
To open the modal dialog box, the onClick() method invokes the method onEditDepGraph(), which dispatches the action EDIT_DEP_GRAPH, which changes the mode to EditDepGraph. The modal dialog box opens when the mode changes.
/* /reducers/record.js */ case 'EDIT_DEP_GRAPH': return { ...state, prevMode: action.prevMode, // save previous mode to return to on close mode: 'EditDepGraph' }
/* /components/Record.js */ <div> {uiMode === 'EditDepGraph' && <DepGraphEditor depGraph={depGraph} picklists={picklists} editValues={recordView.editValues} onFieldValueUpdate={onDepGraphFieldValueUpdate} onClose={() => onDepGraphClose(prevMode)}/> }
Update the Picklist Values
When a user selects a new value for a picklist field, the app must update the values of its child picklist fields. And if those children have children, the app must update them as well, all the way to leaf picklist fields, which don’t have children. To make these updates, it’s important to know the dependency subtree beneath the field whose value changed.
Also, if the preselected value for a field stays valid, the app must preserve that value.
/* /sagas/depGraphValueUpdater.js */ // Traverse down to the target field in the tree. const field = modalFieldsMap[action.field]; var treeNode = action.fieldTree; for (var i = field.controllingFields.length -1; i>=0; i--) { var controllingField = field.controllingFields[i] treeNode = treeNode[controllingField]; } treeNode = treeNode[action.field]; // Now treeNode is the subtree rooted at the target field.
Using this extracted subtree structure and the picklist values obtained when the user clicked to Create, Clone, or Edit a record, we construct the legal picklist values for each picklist field in the subtree.
To see how the app gets the legal picklist values for a particular field, see getLegalValues() in /helpers/depGraphHelper.js. To see how the code traverses the subtree and recursively populates the legal values for every child picklist field using breadth first search, look at the depGraphValueUpdater.js saga.
Congratulations, that’s all there is to it! While it’s true that we couldn’t make building UI for dependent picklists easier than slicing bread, hopefully it’s easier than baking a chocolate soufflé, and just as delicious.
To recap—to construct UI for a dependent picklist editor, User Interface API gives you all the information you need in three places, the objectInfo.dependentFields property, the objectInfo.fields[fieldName].controllingFields property, and the response from the /ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId} endpoint.
User Interface API gives you the power and flexibility to build custom Salesforce user interfaces that bring your customers joy. Hopefully, the Record Viewer app gets you off to a good start. Feel free to fork the GitHub repo and submit pull requests. We look forward to hearing about your apps!