Start tracking your progress
Trailhead Home
Trailhead Home

Install the Sample App

Learning Objectives

After completing this unit, you’ll be able to:
  • Install the Record Viewer sample app.
  • Understand the structure of the Record Viewer app.
  • View a record in the Record Viewer app.

Install the Record Viewer App

Let’s get into the code! To learn about User Interface API, you’ll install the Record Viewer sample app. Use the Record Viewer app to create, read, update, and delete records. Because it’s a sample app, it also includes a user interface that displays the JSON responses from User Interface API. Create an Opportunity record in the Record Viewer sample app.
Tip

Tip

To complete the Hands-on Challenge at the end of this unit, you’ll need to complete these steps. If you don’t have a Trailhead Playground, scroll down to the Challenge and create one now.

  1. If you don’t have Git installed, do that first.
  2. Clone the Record Viewer Git repo. If you don’t know how to clone a repo, see Cloning a repository in GitHub Help.
  3. Record Viewer runs in a Docker container. Install the stable version of Docker Engine (Community).
  4. Launch Docker.

Record Viewer is a connected app, which is an app that communicates with Salesforce via APIs. The app doesn’t run on Salesforce Platform, but it’s connected over APIs. To define how the Record Viewer app authenticates with Salesforce, in Setup, create a connected app.

  1. In your Trailhead Playground, from Setup, enter App and select App Manager.
  2. Click New Connected App and enter these settings:
    • Connected App Name: RecordViewer
    • Contact Email: Your email address
  3. Select Enable OAuth Settings and enter these settings:
    • Callback URL: https://localhost:8443/oauth-redirect
    • Selected OAuth Scopes: Access and manage your data (api)
  4. Click Save.
  5. On the New Connected App page, click Continue.
  6. Copy and save the Consumer Key (also known as the OAuth client ID). You need it later.
To allow Record Viewer to make HTTP requests to Salesforce from a web browser, set up cross-origin resource sharing (CORS) in your Trailhead Playground.
  1. From Setup, enter CORS and select CORS.
  2. Click New.
  3. For Origin URL Pattern, enter https://localhost:8443
  4. Click Save.
Use a CLI to build the Record Viewer app and start the server.
cd /path/to/RecordViewer
docker-compose build && docker-compose up -d
To launch the Record Viewer app, in a browser, open https://localhost:8443.
Note

Note

The Record Viewer app uses a self-signed SSL certificate, so browsers may tell you that your connection is not private. It’s safe to click through and load the app. In Chrome, click Advanced, and Proceed to the site. In Firefox, click Advanced and add an exception. In Internet Explorer, click Continue to this website. On Safari, click Show Details and visit this website.

To authorize the Record Viewer to make requests to Salesforce, enter the Salesforce login URL, https://login.salesforce.com, and the consumer key that you saved from the connected app, and click Login.Login screen of Record Viewer app.

To grant the Record Viewer app permission to access your Salesforce org, verify that your username is correct, and click Allow.Allow Access screen.

Note

Note

Eventually, the user is logged out of Salesforce. To reauthorize the app, load the login page: https://localhost:8443/login. Enter the Salesforce login URL, https://login.salesforce.com, and the consumer key that you saved from the connected app.

Use the Record Viewer App

Use the Record Viewer to view, edit, create, and delete Salesforce records. Record Viewer uses User Interface API to perform these operations on Salesforce records. The app also gives you a handy way to look at the JSON response from User Interface API.

Before we look at a record in Record Viewer, let’s look at a record in Salesforce so we can compare.
  1. In a browser, open the Salesforce org you used to create the RecordViewer connected app.
  2. Click Accounts and click New.
  3. For Account Name, enter Universal Containers
  4. For Phone, enter 206 555 1212 and click Save.
  5. To see the record detail page of the new record, click Details.
Account record detail in Lightning Experience
Now let’s look at that same record in the Record Viewer app.
  1. In a browser, open https://localhost:8443.
  2. Under Recent Items, click Universal Containers.
    Note

    Note

    If you don’t see Universal Containers, refresh the page.

The Record Viewer app shows the record detail page.Account record detail in the Record Viewer app.

We’ve talked about the power of UI API, which is that its responses respect metadata changes made in the Salesforce org. So let’s remove a field from the Account layout in the Salesforce org and see what happens in Record Viewer.

Universal Containers has a phone number, but the record detail page also has a Fax field. No one has a fax machine anymore, so let’s remove that from the page layout.

  1. In your Trailhead Playground, from Setup, enter Object and select Object Manager.
  2. Click Account | Page Layouts | Account Layout.
  3. In the page layout editor, click to remove the Fax field and click Save.

Page layout editor with Fax field selected.

Now return to the Record Viewer app and click View Record to reload the Universal Containers record. Voila! The Fax field disappears and you didn’t change a line of code.Account record detail in Record Viewer app with no fax field.

In this case, you removed the field from the layout. In a real-world scenario, the admin doesn’t have to tell you when they change a layout, your app just responds!

Record Viewer App Structure

To work with User Interface API, you can use any web framework or language that can make HTTP requests. Polymer, React, Angular, Vue, iOS, Android—use the technology of your choice.

The Record Viewer app is a single-page app built using React, Redux, and Redux-Saga. We’ll quickly go over the basics of these technologies as we look at the app’s file structure. If you aren’t familiar with React and Redux, that's OK. Just try to pick up the general ideas.

Your goal is to learn about how to access User Interface API resources and how to handle the responses. Every framework and language has slightly different ways to complete those tasks, but the core concepts are the same and the API behaves the same.

The Record Viewer app was built using:

  • Pug—generates HTML pages from server-side templates.
  • React—a JavaScript library for building custom components and combining them to compose user interfaces
  • Redux—holds the state of a React app in a store, which is a single, immutable JavaScript object. React components dispatch JavaScript objects called actions, which are intercepted by reducers. Reducers are JavaScript functions that use actions as inputs to update the Redux state.
  • Redux-Saga—like reducers, sagas are JavaScript functions that run when they intercept an action. Sagas manage asynchronous requests to User Interface API. At the end of an asynchronous operation, sagas dispatch an action, which can be intercepted by a reducer that updates the store. Sagas prevent tangles of callbacks.
  • Node.js—a server-side runtime for JavaScript apps

Let’s see where these abstract concepts live in the real world. Launch your favorite IDE and open the RecordViewer folder that you cloned from GitHub. Record Viewer app file structure in VS Code. An actions folder, a components, folder, a containers folder, a helpers folder, a reducers folder, and a sagas folder.

First, let’s jump down to the views folder, which holds the Pug HTML templates. The app has three views: the login page, where you entered the Salesforce login information; the record view page, which is the main container for the app; and an error page, which hopefully you haven’t seen. The fourth Pug template is an OAuth authentication redirect.

If you authenticate successfully, the recordView.pug template loads and calls renderRoot, which is defined in the root.js file. From this point on, the Record Viewer app runs client-side code loaded into this single web page.
<!-- /views/recordView.pug -->

doctype html
html
  block head
    title Record Viewer
    // Metadata and style sheet links removed to keep sample short

    script(type='text/javascript', src='https://code.jquery.com/jquery-2.0.3.min.js')
    script(type='text/javascript', src='root.js')

  body
    .slds-grid.slds-wrap
      nav.slds-col.slds-size--1-of-1
        .slds-page-header
          p.slds-page-header__title.slds-truncate.slds-align-middle
            a(href='/recordView') Record Viewer
          p.slds-page-header__info powered by User Interface API
    #root
    script.
      $( document ).ready(function() {
        let rootNode = document.getElementById("root");
        renderRoot("#{targetUrl}", "#{accessToken}", "#{recordId}", rootNode);
      });
The /client-src/root.js file bootstraps the app. It connects the root RecordViewerWrapper component with the root of the Redux state reducers and the Saga middleware. Remember, reducers update state and sagas make requests to User Interface API and dispatch actions holding the results.
/* Excerpt from /client-src/root.js */

// Root renderer for record viewer.
global.renderRoot = function(instanceUrl, accessToken, recordId, rootNode) {
  const sagaMiddleware = createSagaMiddleware()
  const store = createStore(rootReducer, {login:{instanceUrl, accessToken}, record:{recordId}}, applyMiddleware(sagaMiddleware));
  sagaMiddleware.run(rootSaga);

  ReactDOM.render(
    <Provider store={store}>
      <div>
        <RecordViewerWrapper />
      </div>
    </Provider>,
    rootNode);
}
The RecordViewerWrapper.js React component lives in the containers folder. This component is a top-level, connected component that wraps the RecordViewer component. The mapStateToProps function indicates how to bind Redux state to properties of the RecordViewer.
/* Excerpt from /client-src/containers/RecordViewerWrapper.js */

// Presentational Component that uses state to decide how to
// construct the RecordViewer.
const mapStateToProps = (state) => {
  if (state.record.record) {
    return {
      screen: 'RECORD',
      record: state.record.record,
      headerRecordId: state.header.recordId,
      mode: state.record.mode,
      context: state.context,
      prevMode: state.record.prevMode,
      creds: state.login,
      picklists : state.picklists,
      depGraph: state.depGraph,
      rawjson: state.rawjson,
      error: state.error
    }
  } else if (state.record.recordId) {
    return {
      screen: 'FETCH_RECORD',
      recordId: state.record.recordId,
      mode: 'View',
      context: state.context,
      creds: state.login,
      rawjson: state.rawjson,
      error: state.error
    }
  }
 
  // Additional mappings removed to shorten sample
RecordViewWrapper also has a mapDispatchToProps function that maps Redux actions to React properties so that React components can dispatch actions in response to various events. Remember actions? They’re used as inputs to reducers, which are functions that change the app’s state.
/* Excerpt from /client-src/containers/RecordViewerWrapper.js */

const mapDispatchToProps = (dispatch) => {
  return {
    onFetchRecord: (creds, id, context) => {
      dispatch(actions.fetchRecord(creds, id, context))
    },
    onRecordClick: (creds, id, context) => {
      dispatch(actions.fetchRecord(creds, id, context))
    },
    onViewRecordClick: (creds, id, context) => {
      dispatch(actions.fetchRecord(creds, id, context))
    },
    onFormFactorSelect: (formFactor, recordId) => {
      dispatch(actions.updateFormFactor(formFactor, recordId))
    },

   // Additional mappings removed to shorten sample
Finally, the connect() method connects these two maps with the simple RecordViewer component.
/* Excerpt from /client-src/containers/RecordViewerWrapper.js */

const RecordViewerWrapper = connect(
  mapStateToProps,
  mapDispatchToProps
) (RecordViewer)

export default RecordViewerWrapper

The actions, reducers, and sagas live (quite comfortably) in the actions, reducers, and sagas folders.

An action is a simple object that must have a type property, and may have other properties. The type property identifies the action. All the actions are defined in the /actions/index.js file.
/* Excerpt from /client-src/actions/index.js */

export const fetchEntities = (creds) => {
  return {
    type: 'FETCH_ENTITIES',
    creds
  }
}

export const receiveEntities = (entities) => {
  return {
    type: 'RECEIVE_ENTITIES',
    entities,
    receivedAt: Date.now()
  }
}
Let’s follow the happy path of the fetchEntities action. The rootSaga.js file registers all the sagas in the Record Viewer app. When an action is dispatched to the store and its type matches a string in a takeEvery function, the function kicks off the corresponding saga. The FETCH_ENTITIES action type spawns the entitiesFetcher saga.
/* /client-src/sagas/rootSaga.js */

import { takeEvery } from 'redux-saga'

import recentItemsFetcher from './recentItemsFetcher'
import recordFetcher from './recordFetcher'
import recordDeleter from './recordDeleter'
import recordUpdater from './recordUpdater'
import entitiesFetcher from './entitiesFetcher'
import recordCreator from './recordCreator'
import cloneDefaultsFetcher from './cloneDefaultsFetcher'
import createDefaultsFetcher from './createDefaultsFetcher'
import picklistsFetcher from './picklistsFetcher'
import depGraphValueUpdater from './depGraphValueUpdater'

export default function* rootSaga() {
  yield takeEvery('FETCH_RECORD', recordFetcher)
  yield takeEvery('FETCH_RECENT_ITEMS', recentItemsFetcher)
  yield takeEvery('DELETE_RECORD', recordDeleter)
  yield takeEvery('SAVE_RECORD', recordUpdater)
  yield takeEvery('FETCH_ENTITIES', entitiesFetcher)
  yield takeEvery('CREATE_RECORD', recordCreator)
  yield takeEvery('FETCH_CREATE_DEFAULTS', createDefaultsFetcher);
  yield takeEvery('FETCH_CLONE_DEFAULTS', cloneDefaultsFetcher);
  yield takeEvery('FETCH_PICKLISTS', picklistsFetcher);
  yield takeEvery('UPDATE_DEP_GRAPH_FIELD_VALUE', depGraphValueUpdater)
}
The entitiesFetcher.js saga makes a request to User Interface API to get a list of objects that the API supports. The UI API resource is /ui-api/object-info. We’ll talk about how to build the full URL in a later unit. If you’re familiar with Salesforce REST APIs, you already understand it because they all use the same pattern. Also like other Salesforce REST APIs, User Interface API uses OAuth 2.0. The Bearer token is passed in the Authorization header. We set the X-Chatter-Entity-Encoding header to false so that the response isn’t encoded.
/* /client-src/sagas/entitiesFetcher.js */

import { call, put } from 'redux-saga/effects'

import { receiveEntities } from '../actions'

export default function* entitiesFetcher(action) {

  let mruUrl = action.creds.instanceUrl + '/services/data/v45.0/ui-api/object-info'

  let req = {
    method: 'GET',
    headers: {
      'Authorization' : 'Bearer ' + action.creds.accessToken,
      'X-Chatter-Entity-Encoding': false}
    }

  try {
    const response = yield call(fetch, mruUrl, req)
    const responseJson = yield response.json()
    yield put(receiveEntities(responseJson))
  } catch(err) {
    console.error('Describe sobjects error: ' + JSON.stringify(err))
  }
}
Do you see the line yield put(receiveEntities(responseJson))? Look for receiveEntities in the previous code sample from /actions/index.js. Do you see the receiveEntities action? Yep, if the UI API operation is successful, the saga sends the receiveEntities action holding the JSON response. The entities.js reducer intercepts the action and uses it as an input to update the Redux state.
/* /client-src/reducers/entities.js */

const entities = (state = {sobjects: []}, action) => {
  switch (action.type) {
    case 'RECEIVE_ENTITIES':
      return {
        sobjects: action.entities.objects,
        receivedAt: action.receivedAt
      }
    default:
      return state
  }
}

export default entities

The reducer updates a part of the Redux state. After the change is made, React components that subscribe to that bit of state are automatically updated.

In the Record Viewer app, React components that construct HTML live in the components folder. These components define the pieces of the app’s user interface. The CreateableEntitiesList.js component defines UI that displays a menu in the Record Viewer app. The menu contains the list of supported objects returned from the call to /ui-api/object-info. The RecordViewer app receives the entities and displays them in the CreateableEntitiesList component.Create New Record pop-up menu with a list of objects to create.
Tip

Tip

The RecordViewer code often uses the term entities, which means objects. So when you see something like CreateableEntitiesList, you can translate that to a list of creatable objects. In other words, you can create a record from any of the objects in this list! These objects are supported by User Interface API.

In React, you compose components to build UI, which means that parent components can be composed of nested child components. When you develop an app, divide functionality into simple components and combine them to form more complex components.

When you look at the component names in the components folder, you can tell that the Record Viewer app is all about working with records—Record.js, RecordButton.js, RecordSection.js. Look at the component code and you see nested child components. For example, the RecordSection component includes nested RecordRow components.
/* Excerpt from /client-src/components/RecordSection.js */

// Component that displays a Record section.
const RecordSection = ({section, error, editValues, picklists, onFieldValueUpdate, allowEdit, index, objectInfo, onEditDepGraph, uiMode, recordView}) => {

  return (
    <tbody>
      { section.useHeading &&
       <tr>
         <td colSpan="4" key={'sectionHeading' + index} className="slds-text-heading--small slds-p-left--medium slds-p-top--small slds-p-bottom--medium">
           {section.heading}
         </td>
       </tr>
      }
      {section.rows.map((row, i) =>
        <RecordRow
          key={'sectionRow' + index + ',' + i}
          allowEdit={allowEdit}
          uiMode={uiMode}
          picklists={picklists}
          onFieldValueUpdate={onFieldValueUpdate}
          error={error}
          editValues={editValues}
          row={row}
          sectionIndex={index}
          rowIndex={i} 
          objectInfo={objectInfo}
          recordView={recordView}
          onEditDepGraph={onEditDepGraph}/>
      )}
   </tbody>
  );
}
On your own, go to your IDE and look at the Record component (Record.js). It includes a <RecordSection>, a <DepGraphEditor>, and several <RecordButton> components.

Last, but certainly not least, the helpers folder holds some JavaScript helper functions. These functions transform the responses from User Interface API into data models that power the Record Viewer UI. The recordLayout.js code builds the internal data model for layouts, and the depGraphHelper.js code builds the dependent picklist editor. We’ll dig into these files later.

retargeting