Supporting Dynamic Types in your Custom Property Editor

Some actions and screen components have input attributes of type SObject or SObject[].  When configuring an instance of one of these, you need to provide a concrete type in order to save the flow successfully.

The standard property editor does this by providing automatically generated mapping UI:

As the developer of a custom property editor, you have more flexibility. You can choose to provide a UI identical to the UI shown above. But you can also choose to handle the concrete mapping behind the scenes. For example, you might choose to ask the user a friendlier question like “What type of object do you want to use with this action?” and provide an object picker. Once the user has picked the object, you can fill in the dynamic type mappings via a normal CPE event dispatch.

When setting a type mapping, use this event:

const event = new CustomEvent('configuration_editor_type_mapping_changed', {
    composed: true,
    cancelable: false,
    bubbles: true,
    detail: {
        name, // name of the dynamic type. For actions, include the param name:  'Typename__param1'. For screens, just use the type name e.g "T"
        value, // concrete value type of the dynamic type, e.g 'Account'
    }
});

// for Screens the name attribute for event is just the dynamic type e.g “T”
// for Actions it’s the type followed by the param name e.g “T__paramName”

Retrieving Existing Dynamic Type Mappings

The CPE interface supports a new attribute dynamicTypeMappings:

* array of complex object containing type name-value of the dynamic data types
   * in Action or Screen
   * eg: [{
   *       typeName: 'T', // the type name
   *       typeValue: 'Account' // or any other sObject
   *     }]
   */

Here’s an example of a CPE that supports dynamic type mapping:

import { LightningElement, api, track } from "lwc";

export default class DynamicTypeCpe extends LightningElement {
  _inputVariables = [];
  _builderContext = {};
  _elementInfo = {};
  _typeMappings = [];

  _flowVariables;
  _elementType;
  _elementName;
  /* array of complex object containing name-value of a input parameter.
   * eg: [{
   *       name: 'prop1_name',
   *       value: 'value',
   *       valueDataType: 'string'
   *     }]
   */
  @api
  get inputVariables() {
    return this._inputVariables;
  }

  set inputVariables(variables) {
    this._inputVariables = variables || [];
    this.initializeValues();
  }

  
  @api
  get builderContext() {
    return this._builderContext;
  }

  set builderContext(context) {
    this._builderContext = context || {};
    if (this._builderContext) {
      const { variables } = this._builderContext;
      this._flowVariables = [...variables];
    }
  }

  /* contains the information about the LWC or Action in which
   * the configurationEditor is defined.
   * eg: {
   *       apiName: 'CreateCase', // dev name of the action or screen
   *       type: 'Action' // or 'Screen'
   *     }
   */
  @api
  get elementInfo() {
    return this._elementInfo;
  }

  set elementInfo(info) {
    this._elementInfo = info || {};
    if (this._elementInfo) {
      this._elementName = this._elementInfo.apiName;
      this._elementType = this._elementInfo.type;
    }
  }
  
  /* array of complex object containing type name-value of the dynamic data types
   * in Action or Screen
   * eg: [{
   *       typeName: 'T', // the type name
   *       typeValue: 'Account' // or any other sObject
   *     }]
   */
  @api
  get typeMappings() {
    return this._typeMappings;
  }

  set typeMappings(mappings) {
    this._typeMappings = mappings || {};
    this.initializeTypeMappings();
  }

  /* Return a promise that resolve and return errors if any
   *    [{
   *      key: 'key1',
   *      errorString: 'Error message'
   *    }]
   */
  @api
  validate() {
    const validity = [];
    return validity;
  }
  
  get options() {
    return [
      { label: "Account", value: "Account" },
      { label: "Case", value: "Case" },
      { label: "Contact", value: "Contact" }
    ];
  }

  get recordVariableOptions() {
    if (this.typeValue) {
      return this.updateRecordVariablesComboboxOptions(this.typeValue);
    }
    return [];
  }

  @track
  inputValue = "";

  @track
  typeValue = "";

  @track
  record;

  initializeTypeMappings() {
    this._typeMappings.forEach((typeMapping) => {
      if (typeMapping.name && typeMapping.value) {
        this.typeValue = typeMapping.value;
      }
    });
  }

  initializeValues() {
    this._inputVariables.forEach((variable) => {
      if (variable.name && variable.value) {
        if (variable.valueDataType === "reference") {
          this.inputValue = "{!" + variable.value + "}";
        } else {
          this.inputValue = variable.value;
        }
      }
    });
  }

  handleComboboxChange(event) {
    if (event && event.detail) {
      const newValue = event.detail.value;
      this.comboboxvalue = newValue;
      // for Screens the name atrribute for event is just the dynamic type e.g "T"
      // for Actions it's the type followed by the param name e.g "T__paramName"
      const name = this._elementType === "Screen" ? "T" : "T__record";
      const dynamicTypeChangeEvent = new CustomEvent(
        "configuration_editor_type_mapping_changed",
        {
          bubbles: true,
          cancelable: false,
          composed: true,
          detail: {
            name,
            value: newValue
          }
        }
      );
      this.dispatchEvent(dynamicTypeChangeEvent);
      this.updateRecordVariablesComboboxOptions(newValue);
    }
  }

  updateRecordVariablesComboboxOptions(objectType) {
    const variables = this._flowVariables.filter(
      (variable) => variable.objectType === objectType
    );
    let comboboxOptions = [];
    variables.forEach((variable) => {
      comboboxOptions.push({
        label: variable.name,
        value: "{!" + variable.name + "}"
      });
    });
    return comboboxOptions;
  }

  handleRecordChange(event) {
    if (event && event.detail) {
      const newValue = event.detail.value;
      this.inputValue = newValue;
      const valueChangedEvent = new CustomEvent(
        "configuration_editor_input_value_changed",
        {
          bubbles: true,
          cancelable: false,
          composed: true,
          detail: {
            name: "record",
            newValue,
            newValueDataType: "reference"
          }
        }
      );
      this.dispatchEvent(valueChangedEvent);
    }
  }
}

For more info, see https://unofficialsf.com/custom-property-editors-developers-guide/