Start tracking your progress
Trailhead Home
Trailhead Home

Build UI to Display a Record

Learning Objectives

After completing this unit, you’ll be able to:
  • Make a request to User Interface API to get record data and metadata.
  • Understand why and how to request form factors, layout types, and access modes.
  • Understand why and how to request child records.

Fetch a Record

Another power of User Interface API is that it pulls together the stuff you need to draw the UI. Do you know how many HTTP requests it takes to get the object metadata, layout metadata, and field data needed to display the Universal Containers record? One.

/ui-api/record-ui/{recordIds}

Let’s see what the request looks like in the Record Viewer code. Navigate to the /client-src/sagas/recordFetcher.js file. You can look at it on GitHub or on your local machine.

This line constructs the UI API target URL.
let recordViewUrl = action.creds.instanceUrl + '/services/data/v45.0/ui-api/record-ui/' 
  + action.recordId + '?formFactor=' + action.context.formFactor + '&layoutTypes=Full&modes=View,Edit';

The request sends one record ID, action.recordId, which is the record that the user selects from the Recent Items list. (The endpoint supports multiple record IDs, but the Record Viewer app requests only one.)

The response includes the layout information, which tells you were the fields go and which UI sections they live in. It also tells you which sections are collapsed, which we call the layout user state.

To specify what to include in the layout information, the request uses these parameters: formFactor, layoutTypes, and modes.

The form factor changes the layout of the fields. Choose a form factor that matches the type of hardware the app is running on. Large is a layout for desktop clients. Medium is a layout for tablets, and Small is a layout for phones. In the large and medium form factors, sections have a two-column layout. In the small form factor, sections have a one-column layout.

The layout type determines how many fields are returned. The possible layout types are Full and Compact. The default full layout includes all the fields from the page layout assigned to the current user. The compact layout includes all the fields from the compact layout assigned to the current user. You can edit both layout types in Setup. Regardless of layout type, the response includes only fields that the user has access to.

The mode corresponds to the task the user is performing: Create, View, or Edit. The layout information is different depending on the mode. For example, in create mode, the layout doesn’t include the System Information section, which includes fields like Created By and Last Modified By.

The Record Viewer app requests the view and edit modes, so if a user clicks Edit, the app already has the information it needs to render the UI.

Tip

Tip

To return child records without having to construct queries that join two records, use the childRelationships parameter. The response is paginated and includes one level of child relationships. For example, this request returns an Account record and its Opportunity child records.

/ui-api/record-ui/001R0000003IG0vIAG?childRelationships=Account.Opportunities

Send Requests and Update Global State

The RecordViewer app sends REST requests to UI API endpoints and asynchronously updates its Redux state with that information. Then the React component tree… wait for it… reacts to the state change and updates the UI.

Redux sagas manage the app’s REST requests. The /ui-api/record-ui/{recordIds} request is performed using the recordFetcher.js saga. This code constructs the REST URL and issues the request using the OAuth access token as the Bearer. It also sets the X-Chatter-Entity-Encoding header to false to ensure that special characters in the JSON are not returned HTML-escaped.

When the response returns, and the JSON is successfully parsed, the saga completes by sending a RECEIVE_RECORD action that includes the JSON response. In the Redux model, actions are issued to express an intention to change the global state. By separating the operations to make the external request, modify the state, and update the component, we keep things loosely coupled.

In Redux, reducers intercept actions and use them to transform aspects of the global state. In this case, to make an update to the “record” part of the global state, the record.js reducer intercepts the RECEIVE_RECORD action and processes its JSON.
/* /reducers/record.js */
case 'RECEIVE_RECORD':
      return {
        record: recordLayout.getLayoutModel(action.recordId, action.record),
        mode: 'View'
      }

The JSON payload is held in action.record. The resulting new state is available to components through state.record. This part of the state from the record.js reducer is assembled into the global state in reducers/index.js.

Parse the JSON Response and Display the Record

Now let’s look inside the recordLayout.getLayoutModel() helper that’s used in the record.js state reducer. It massages the record data, layouts, and object info from the /ui-api/record-ui/{recordIds} JSON to make a data structure. That data structure is the state that powers the app’s React components.

At the bottom of the /client-src/helpers/recordLayout.js file, getLayoutModel() assembles an object holding:
  • the layout information
  • a map of editValues used to track changes made to field values in the UI
  • the single objectInfo corresponding to the record at hand
  • the record ID

The recordLayout.getLayoutModel() function processes the layout information as it loops through the layouts. For each section in each layout in the response, we call getLayoutSectionModel(). This method retrieves the section header information from the section JSON. Then it loops through each row in the section, and within each row, it loops through each item in the layout.

For each layout item, getLayoutItemModel() assembles an object that holds:
  • a nested list of values (there can be multiple values in an item for things like addresses)
  • link information (links to other records)
  • custom link information (links to external URLs)

Each of the values holds information about what to display as text, the label, the field metadata, editability, and the corresponding UI API picklist URL, when appropriate.

There are a few special cases in this method because we do special things for picklist values, references, dates, and non-field items. In the typical case, for a layout item backed by a non-date field, we extract the displayValue and value properties from the corresponding entry in the record data JSON.
} else if (record.fields[compValue]) {
        var displayValue = record.fields[compValue].displayValue;
        let rawValue = record.fields[compValue].value;
        if (displayValue == null && rawValue != null) {
          displayValue = rawValue.toString();
        }
        values.push(
          {displayValue: displayValue,
           value: rawValue,
           label:component.label,
           field:compValue,
           fieldInfo,
           picklistUrl,
           editableForNew:item.editableForNew,
           editableForUpdate:item.editableForUpdate,
           isNull:displayValue == null})

Render Layout Components

As we’ve learned, the object returned from recordLayout.getLayoutModel() is saved in the global state as state.record.record. This state is used to render corresponding components in the UI. The top-level container RecordViewerWrapper.js checks to see if state.record.record is available, and passes it down to a nested RecordViewer.js component in the record property. Nested within the RecordViewer.js component, RecordSection.js and RecordRow.js render their sections and rows.

Look at RecordRow.jsgetViewItemCells() in particular. When we render each cell in the row, the happy path creates a <div> for each component in the item. We give the <div> a key because React requires it. We use the displayValue to render the text that you see on the screen.
{ item.values.map((component, i) => {
             if (component.displayValue && component.displayValue.length > 0) {
               if (component.fieldInfo.htmlFormatted) {
                 return (
                   <div key={'component' + itemLabel + ',' + i} dangerouslySetInnerHTML={{__html: component.displayValue}}></div>
                 )
               } else {
                 return (
                   <div key={'component' + itemLabel + ',' + i}>{component.displayValue}</div>
                 )
               }
             } else {
               return null
             }
           }
          )}

The result is that our app is dynamically powered by the layout and object info returned by UI API. Instead of hardcoding fields and field positions in the layout, use UI API to construct a UI that can rearrange itself as the admin updates layouts and objects in Salesforce.

retargeting