Retrieving Picklist Values Without Using Apex

Did you know that Lightning Web Components lets you retrieve picklist values for any object field without writing a single line of Apex? In this blog post, you’ll learn how you can combine an App Builder custom property, the component runtime context and the User Interface API to retrieve picklist values. I’ll present a fully generic approach that doesn’t involve hardcoding data such as the record type ID, the object name or the field name.

I’ll share the code of a sample picklist buttons component that uses that approach. This component is an alternative to the standard built-in Path feature. It’s interesting to use it when you have picklists that hold values that do not represent a sequential order (unlike opportunity stages for example).

Let’s dive into the different tools and techniques that make this possible. First, we’ll start with Lightning App Builder custom properties, then we’ll explore the information that is provided in the page context. We’ll finish with the User Interface API.

Lightning App Builder component properties

The first feature that you’re going to leverage in order to avoid hardcoding data is a pair of Lightning App Builder component properties. These allow admins to reuse the component at will by specifying properties declaratively when the component is deployed on a Lightning page.

To achieve this, you need to configure the component metadata file as the following:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
  <apiVersion>46.0</apiVersion>
  <isExposed>true</isExposed>
  <targets>
    <target>lightning__RecordPage</target>
  </targets>
  <targetConfigs>
    <targetConfig targets="lightning__RecordPage">
      <property name="qualifiedFieldName" type="String" label="Qualified Field Name" required="true"/>
      <property name="label" type="String" label="Label" required="true"/>
    </targetConfig>
  </targetConfigs>
</LightningComponentBundle>

You expose your component to the Lightning App Builder by setting the isExposed flag to true.

You restrict the use of your component to record pages with a lightning__RecordPage target. This is required for the next step where you will retrieve object and record context information.

Finally, you expose the qualifiedFieldName and label component properties to the Lightning App Builder. You’ll also have to declare the related properties with @api qualifiedFieldName and @api label in your component’s JavaScript. qualifiedFieldName holds the qualified picklist field name (Eg: Account.Rating).

Note that there is no built-in input validation in the App Builder that checks if the provided value is a picklist field name or if the value has the right format. If the admin enters an incorrect value in the Qualified Field Name field (an unknown field name, an unqualified field name, a field name that is not a picklist, etc), there will be an error. In that case, your component needs to displays a user-friendly error message but one that won’t prevent the page from being saved.

If you’d like to validate the user input, you can create a dynamic picklist parameter in the App Builder and use it as a datasource for your parameter. Use that to restrict user selection to picklist field names.

Component runtime context

Now that your component can be placed on record pages, you can benefit from some runtime context information. Among other things, you have access to the object name and the record Id with the following public properties:

@api recordId;
@api objectApiName;

These properties will automatically receive a value when the component is displayed. You can then use those values in your UI API calls to retrieve additional object and record information.

User Interface API

The UI API is the last (and main) piece of the puzzle. A great advantage of using Lightning Web Components over Aura is that you have access to many more UI API resources with the wire service. You gain access to valuable information with a minimal amount of code.

You’ll need to use three UI API wire adapters to retrieve your picklist values:

  • getPicklistValues allows us to retrieve all picklist values for a given record type. The record type ID isn’t directly exposed in the component context so you’ll need another UI API call to fetch it.
  • getRecord allows us to retrieve record data. You’ll use it to retrieve the current value of the picklist field and the record type ID.
  • getObjectInfo allow us to retrieve the object information. You’ll need this along with getRecord because in some cases the record type ID is not specified in the record data. In that case, you’ll need to figure out the default record type ID.

You’ll need to chain these API calls as the following to retrieve the current picklist value in a picklistValue tracked property and the list of all picklist values in a buttons tracked property:

Retrieving the object info

The first wire adapter that you need to call is getObjectInfo. Calling this wire allows you to retrieve the default record type ID for that record. If the record does not have a record type specified, you can fall back to that default value for that object.

@wire(getObjectInfo, {
  objectApiName: '$objectApiName'
})
getObjectInfo({ error, data }) {
  if (data) {
    this.defaultRecordTypeId = data.defaultRecordTypeId;
    // Check if we need to override record type with default
    if (this.hasRecordTypeId === false) {
      this.recordTypeId = this.defaultRecordTypeId;
    } 
  } else if (error) {
    // Handle error
  }
}

You pass it an objectApiName parameter with the value of the objectApiName property that you registered earlier. Note the specific syntax of the parameter value: a string with a $ prefix. This is how you pass a reactive parameter value. What this means is that the wire is called each time the parameter value changes.

You could ask yourself: is the value of objectApiName ever going to change? The answer to that question is yes, at least once. When the page is being rendered the property value isn’t set for a few milliseconds. You need to react to the value change (the property initialization) to call the wire with the right object name.

Retrieving the record

Once you have retrieved the object information, you can fetch the record data with the getRecord adapter. You pass it the record Id and the fields that you wish to retrieve. In your case, that’s just the picklist field name that you defined in the Lightning App Builder and that is stored in qualifiedFieldName. You extract and save two things from the wire data: the record type id in a recordTypeId property and the picklist field value in a picklistValue property.

@wire(getRecord, {
  recordId: '$recordId',
  fields: '$qualifiedFieldName'
})
getRecord({ error, data }) {
  if (data) {
    // Check if record data includes record type
    if (data.recordTypeInfo) {
      this.hasRecordTypeId = true;
      this.recordTypeId = data.recordTypeInfo.recordTypeId;
    } else { // Record type is missing from record data
      this.hasRecordTypeId = false;
      // Use default type if available (it could still be loading)
      if (this.defaultRecordTypeId) {
        this.recordTypeId = this.defaultRecordTypeId;
      }
    }
    // Get current picklist value
    const fieldName = this.getFieldName();
    this.picklistValue = data.fields[fieldName].value;
  } else if (error) {
    // Handle error
  }
}

Note that we normally recommend to avoid using dynamic field names (like qualifiedFieldName) when working with Salesforce data. Instead, you should import static field reference so that we guarantee dependency integrity (for example we won’t let you delete a record field that is referenced in a component). We’ll make an exception for our use case since you’re building a generic component. Just remember that this flexibility does imply that an admin can potentially delete or rename the picklist field and break the component. You’ve been warned!

As mentioned earlier, it may happen that the record data returned by the wire does not include the record type ID. This happens when a record is created at a time when there are no record types defined. In that case, you may fall back on the default record type ID that you fetch from the objectInfo property.

Retrieving the picklist values

At this point, you now have access to the record type ID and you can call the last wire adapter: getPicklistValues. This gives us the list of all picklist values for your record type.

@wire(getPicklistValues, {
  recordTypeId: '$recordTypeId',
  fieldApiName: '$qualifiedFieldName'
})
getPicklistValues({ error, data }) {
  if (data) {
    // Map picklist values to buttons
    this.buttons = data.values.map(plValue => {
      return {
        label: plValue.label,
        value: plValue.value
      };
    });
  } else if (error) {
    // Handle error
  }
}

With that final wire call, you now have access to all picklist values. In the case of our picklist buttons component, all you have to do is to map these values to the buttons property in order to render them as buttons.

Summary

That’s a wrap! You’ve learned how to retrieve picklist values in Lightning web components without writing a single line of Apex thanks to the combined use of App Builder parameters, page context and the UI APIs.

Another great advantage of not using Apex in this context is that you benefit from the Lightning Data Service when using the UI API. For example, if you change the picklist value in the record details component, you’ll see that your custom component is automatically updated with the new value.

Feel free to explore the code presented in this post in this sample picklist button component.

About the author

Philippe Ozil is a Lead Developer Evangelist at Salesforce where he focuses on the Salesforce Platform. He writes technical content and speaks frequently at conferences. He is a full stack developer and enjoys working on robotics and VR projects. Follow him on Twitter @PhilippeOzil or check his GitHub projects @pozil.