Build UI to Display a Record
Learning Objectives
- 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.
let recordViewUrl = action.creds.instanceUrl + '/services/data/v48.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 where 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.
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.
/* /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.
- 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.
- 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.
} 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.
{ 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.