Part 2: Enhancing Salesforce Flow Screens with Dynamic Forms
Building the Lightning Web Component: Bringing the Form to Life
Now that we've explored how our Apex class fetches the Dynamic Form metadata, it's time to dive into the Lightning Web Component (LWC) that brings everything together. This component is the heart of our solution—it consumes the metadata, renders the form dynamically within a Flow screen, and handles user interactions.
We'll break down the LWC file by file, explaining what each part does and how it interacts with the Flow container and the Apex class.
1. JavaScript Controller (flexipageEditForm.js)
This is the main JavaScript file that controls the behavior of our component. It handles data fetching, parsing, state management, and user interactions.
Key Responsibilities:
- Fetching FlexiPage metadata using the Apex class.
- Parsing the metadata and building the form dynamically.
- Handling field value changes and visibility rules.
- Communicating with the Flow container and updating variables.
Let's break it down step by step.
Imports and Constants
import { LightningElement, api, track, wire } from 'lwc'; import getFieldValues from '@salesforce/apex/FlexiPageToolingService.getFieldValues'; import getFlexiPageMetadata from '@salesforce/apex/FlexiPageToolingService.getFlexiPageMetadata'; import { parseFlexiPageJson } from './utils'; import { FlowAttributeChangeEvent } from 'lightning/flowSupport'; import { getObjectInfo } from 'lightning/uiObjectInfoApi'; const invalidFields = ['CreatedById', 'LastModifiedById', 'Id'];
- Standard LWC Modules: We import necessary modules from lwc.
- Apex Methods: Importing getFieldValues and getFlexiPageMetadata from our Apex class.
- Utility Function: Importing parseFlexiPageJson from utils.js.
- Flow Support: Importing FlowAttributeChangeEvent to interact with the Flow.
- UI API: Importing getObjectInfo to retrieve object metadata.
- Constants: Define invalidFields to exclude certain fields.
Component Declaration and Properties
export default class FlexipageEditForm extends LightningElement { // Public properties exposed to the Flow or parent components @api recordId; @api objectApiName; @api flexiPageName; @api fieldPageName; // Alternative FlexiPage name @api altField; // Field to use as an alternative recordId @api debugEnabled = false; @api cardTitle = ''; @api showIcon = false; @api varRecord; // Variable to hold the record data @api flowContext; // Indicates if the component is used in a Flow @api saveLabel = 'Save'; @api excludedFields = ''; // Fields to exclude from rendering // Tracked properties for reactivity @track sections = []; @track iconUrl = ''; @track objectIcon = ''; @track recordData = {}; @track error; dataLoaded = false; _recOutput = {}; // Internal properties parsedSections = {}; excludedFieldsArray = []; fields = []; }
- Public Properties (@api): These are properties that can be set from the parent context, such as the Flow. They allow customization and data passing.
- Tracked Properties (@track): Properties that need to be reactive so the UI updates when they change.
- Internal Variables: Used for internal logic and data management.
Getters and Setters
@api get recOutput() { return this._recOutput; } set recOutput(value) { this._recOutput = value; }
- recOutput: This is a getter and setter for the recOutput property, allowing the component to expose data back to the Flow.
Lifecycle Hooks
connectedCallback() { console.log('ConnectedCallback: Initializing FlexiPageEditForm component'); if (!this.objectApiName) { console.log('objectApiName not provided. Defaulting to {{OBJECT API NAME HERE}}'); this.objectApiName = '{{OBJECT API NAME HERE}}'; } this.loadFlexiPageConfig(); }
- connectedCallback(): This method runs when the component is inserted into the DOM.
- Checks if objectApiName is provided; if not, defaults to 'Opportunity_Readiness__c'.
- Calls loadFlexiPageConfig() to start fetching the FlexiPage metadata.
Wire Adapters
@wire(getObjectInfo, { objectApiName: '$objectApiName' }) handleObjectInfo({ error, data }) { if (data) { // Extracts object icon information for display } else if (error) { console.error('Error fetching object info:', error); } }
- getObjectInfo: Retrieves metadata about the object specified by objectApiName.
- handleObjectInfo(): Processes the data to extract the object's icon URL, which can be displayed in the component.
Loading FlexiPage Configuration
loadFlexiPageConfig() { const flexiPageNameToUse = this.fieldPageName || this.flexiPageName; getFlexiPageMetadata({ developerName: flexiPageNameToUse }) .then(result => { this.config = JSON.parse(result); this.fetchFieldValues(); }) .catch(error => { console.error('Error loading FlexiPage config:', error); this.error = error; }); }
- Purpose: Fetches the FlexiPage metadata using the Apex method.
- Process:
- Determines which FlexiPage name to use.
- Calls getFlexiPageMetadata() with the developer name.
- Parses the JSON response and stores it in this.config.
- Calls fetchFieldValues() to retrieve field values.
Fetching Field Values
fetchFieldValues() { this.excludedFieldsArray = this.excludedFields ? this.excludedFields.split(',').map(field => field.trim().toLowerCase()) : []; getFieldValues({ recordId: this.recordId, objectApiName: this.objectApiName }) .then(data => { this.recordData = this.mapFieldValues(data.fieldValues); this.parsedSections = parseFlexiPageJson(this.config, this.recordData); this.calculateVisibility(this.parsedSections); this.sections = this.processSections(this.parsedSections, this.excludedFieldsArray); this.fields = this.collectFields(this.parsedSections, this.excludedFieldsArray); this.checkAltFieldAndRefetch(); this.dataLoaded = true; }) .catch(error => { console.error('Error fetching field values:', error); this.error = error; this.dataLoaded = true; }); }
- Purpose: Retrieves field values for the specified record and object.
- Process:
- Converts excludedFields into an array for easy checking.
- Calls getFieldValues() Apex method.
- Maps the returned field values for easy access.
- Parses the FlexiPage JSON to structure the sections and fields.
- Calculates field visibility based on visibility rules.
- Processes sections and collects fields for rendering.
- Checks for an alternate record ID (altField) and refetches if necessary.
- Sets dataLoaded to true to indicate that data is ready.
Mapping Field Values
mapFieldValues(data) { let mappedValues = {}; for (const key in data) { if (data.hasOwnProperty(key)) { const normalizedKey = key.toLowerCase(); mappedValues[normalizedKey] = data[key]; } } return mappedValues; }
- Purpose: Normalizes field names to lowercase for consistent access.
Handling Alternative Field (altField)
checkAltFieldAndRefetch() { const altFieldValue = this.recordData[this.altField?.toLowerCase()]; if (altFieldValue) { this.recordId = altFieldValue; this.fetchFieldValues(); } }
- Purpose: If altField is set and has a value, updates recordId and refetches field values.
- Use Case: Useful when the initial recordId is a placeholder and the actual ID is stored in another field.
Calculating Visibility
calculateVisibility(parsedSections) { Object.keys(parsedSections).forEach(sectionKey => { const section = parsedSections[sectionKey]; Object.keys(section.columns).forEach(columnKey => { const column = section.columns[columnKey]; Object.keys(column.fields).forEach(fieldKey => { const field = column.fields[fieldKey]; if (field.visibilityRule) { field.isVisible = this.evaluateVisibilityRule(field.visibilityRule); } else { field.isVisible = true; } }); }); }); }
- Purpose: Iterates over all fields and calculates their visibility based on the defined visibility rules.
- Process:
- Checks if a field has a visibilityRule.
- Calls evaluateVisibilityRule() to determine if the field should be visible.
Evaluating Visibility Rules
evaluateVisibilityRule(visibilityRule) { if (!visibilityRule || !visibilityRule.criteria) { return true; } const results = visibilityRule.criteria.map(criterion => { const leftFieldApiName = criterion.leftValue.replace('{!Record.', '').replace('}', '').toLowerCase(); const leftValue = this.recordData[leftFieldApiName]; const rightValue = criterion.rightValue; let conditionMet = false; // Evaluate the condition based on the operator switch (criterion.operator) { case 'CONTAINS': conditionMet = typeof leftValue === 'string' && leftValue.includes(rightValue); break; case 'EQUAL': conditionMet = leftValue === rightValue; break; // Additional cases... } return conditionMet; }); // Combine results based on booleanFilter let isVisible = true; const booleanFilter = visibilityRule.booleanFilter ? visibilityRule.booleanFilter.toUpperCase() : 'AND'; if (booleanFilter === 'AND') { isVisible = results.every(result => result); } else if (booleanFilter === 'OR') { isVisible = results.some(result => result); } return isVisible; }
- Purpose: Evaluates the visibility criteria for a field.
- Process:
- Parses each criterion in the visibilityRule.
- Compares the field values based on the operator (e.g., EQUAL, CONTAINS).
- Combines the results using the booleanFilter (AND/OR).
Processing Sections for Rendering
processSections(parsedSections, excludedFieldsArray) { let processedSections = []; Object.keys(parsedSections).forEach(sectionKey => { const section = parsedSections[sectionKey]; let processedColumns = []; Object.keys(section.columns).forEach(columnKey => { const column = section.columns[columnKey]; const visibleFields = Object.keys(column.fields).filter( fieldId => column.fields[fieldId].isVisible && !excludedFieldsArray.includes(fieldId.toLowerCase()) ); const fieldsToRender = visibleFields.map(fieldId => ({ fieldId: fieldId, isRequired: column.fields[fieldId].isRequired })); if (fieldsToRender.length > 0) { const className = `slds-col slds-size_1-of-2`; processedColumns.push({ side: column.side, fields: fieldsToRender, class: className }); } }); if (processedColumns.length > 0) { processedSections.push({ sectionName: section.label || 'Section', sectionId: sectionKey, columns: processedColumns, isOpen: true, class: 'slds-section slds-is-open' }); } }); return processedSections; }
- Purpose: Prepares the sections and fields for rendering in the template.
- Process:
- Filters out fields that are not visible or are excluded.
- Structures the data into sections and columns with appropriate classes.
Collecting Fields
collectFields(parsedSections, excludedFieldsArray) { let fields = []; Object.values(parsedSections).forEach(section => { Object.values(section.columns).forEach(column => { Object.keys(column.fields).forEach(fieldId => { if (!excludedFieldsArray.includes(fieldId.toLowerCase())) { fields.push(fieldId); } }); }); }); const uniqueFields = [...new Set(fields)]; return uniqueFields; }
Purpose: Collects all the field API names that need to be rendered.
Handling User Interactions
Toggling Sections:
toggleSection(event) { const sectionName = event.target.dataset.name; this.sections = this.sections.map(section => { if (section.sectionName === sectionName) { const isOpen = !section.isOpen; return { ...section, isOpen, class: `slds-section ${isOpen ? 'slds-is-open' : ''}` }; } return section; }); }
- Purpose: Handles the expand/collapse functionality for sections.
Handling Field Changes:
handleFieldChange(event) { const fieldName = event.target.fieldName; const value = event.target.value; // Update output and recordData this._recOutput = { ...this._recOutput, [fieldName]: value }; this.recordData = { ...this.recordData, [fieldName.toLowerCase()]: value }; // Recalculate visibility and update sections this.calculateVisibility(this.parsedSections); this.sections = this.processSections(this.parsedSections, this.excludedFieldsArray); this.fields = this.collectFields(this.parsedSections, this.excludedFieldsArray); // Dispatch event to Flow if necessary if (this.flowContext) { this.dispatchEvent(new FlowAttributeChangeEvent('recOutput', this._recOutput)); } }
- Purpose: Updates internal data structures when a field value changes.
- Process:
- Updates recOutput and recordData with the new value.
- Recalculates visibility rules.
- Updates the sections and fields for rendering.
- Dispatches FlowAttributeChangeEvent to update Flow variables if in Flow context.
2. HTML Template (flexipageEditForm.html)
This file defines the structure and layout of the component's user interface.
<template> <lightning-card title={cardTitle} icon-name={objectIcon}> <template if:true={isDataAvailable}> <template for:each={sections} for:item="section"> <section key={section.sectionId} class={section.class}> <div class="slds-section__title slds-m-around_small"> <button aria-expanded={section.isOpen} class="slds-button slds-section__title-action" data-name={section.sectionName} onclick={toggleSection}> <svg class="slds-section__title-action-icon slds-button__icon slds-button__icon_left" aria-hidden="true"> <use xlink:href="/_slds/icons/utility-sprite/svg/symbols.svg#switch"></use> </svg> <span class="slds-truncate" title={section.sectionName}>{section.sectionName}</span> </button> </div> <div class="slds-section__content slds-m-around_medium"> <lightning-record-edit-form record-id={recordId} object-api-name={objectApiName}> <div class="slds-grid slds-wrap slds-gutters"> <template for:each={section.columns} for:item="column"> <div key={column.columnId} class={column.class}> <template for:each={column.fields} for:item="field"> <lightning-input-field key={field.fieldId} field-name={field.fieldId} onchange={handleFieldChange} required={field.isRequired} ></lightning-input-field> </template> </div> </template> </div> </lightning-record-edit-form> </div> </section> </template> </template> <template if:false={isDataAvailable}> <c-stencil iterations="4" columns="2"></c-stencil> </template> </lightning-card> </template>
Key Points:
- Conditional Rendering:
- Uses <template if:true={isDataAvailable}> to render content only when data is loaded.
- Iterating Over Sections and Columns:
- Uses <template for:each={sections}> to loop through sections.
- Each section contains columns and fields.
- Section Toggle:
- The section header includes a button that calls toggleSection() when clicked.
- Dynamic Fields:
- Renders <lightning-input-field> components for each field.
- Binds field-name, onchange, and required properties dynamically.
- Record Edit Form:
- Uses <lightning-record-edit-form> to handle record context.
Interaction with the Component:
- The template uses the data structures prepared in the JavaScript controller to render the UI.
- User interactions (e.g., field changes, section toggling) are handled by event handlers in the JavaScript file.
3. Utility JavaScript (utils.js)
export function parseFlexiPageJson(flexiPageJson, recordData) { const sections = {}; // Object to store sections and their fields console.log('Starting to parse flexiPageJson:', flexiPageJson); // First pass: Collect sections and their labels flexiPageJson.flexiPageRegions.forEach(region => { region.itemInstances.forEach(itemInstance => { if (itemInstance.componentInstance && itemInstance.componentInstance.componentName === 'flexipage:fieldSection') { const sectionFacetId = itemInstance.componentInstance.componentInstanceProperties.find(prop => prop.name === 'columns').value; const sectionLabel = itemInstance.componentInstance.componentInstanceProperties.find(prop => prop.name === 'label').value || 'Unnamed Section'; if (!sections[sectionFacetId]) { sections[sectionFacetId] = { label: sectionLabel, columns: {} }; } } }); }); // Second pass: Collect columns and assign to sections flexiPageJson.flexiPageRegions.forEach(region => { region.itemInstances.forEach(itemInstance => { if (itemInstance.componentInstance && itemInstance.componentInstance.componentName === 'flexipage:column') { const columnFacetId = itemInstance.componentInstance.componentInstanceProperties.find(prop => prop.name === 'body').value; const sectionFacetId = region.name; const side = parseInt(itemInstance.componentInstance.identifier.replace('flexipage_column', ''), 10) % 2 === 0 ? 'right' : 'left'; if (sections[sectionFacetId]) { sections[sectionFacetId].columns[columnFacetId] = { side: side, fields: {} }; } } }); }); // Third pass: Assign fields to columns flexiPageJson.flexiPageRegions.forEach(region => { if (region.type === 'Facet') { region.itemInstances.forEach(itemInstance => { if (itemInstance.fieldInstance) { const fieldApiName = itemInstance.fieldInstance.fieldItem.replace('Record.', ''); const columnFacetId = region.name; const sectionFacetId = Object.keys(sections).find(sectionId => Object.keys(sections[sectionId].columns).includes(columnFacetId) ); if (sectionFacetId && sections[sectionFacetId].columns[columnFacetId]) { const fieldValue = itemInstance.fieldInstance?.fieldInstanceProperties?.find(prop => prop.name === 'value')?.value || ''; const isVisible = true; // Initially set all fields to visible const isRequired = itemInstance.fieldInstance?.fieldInstanceProperties?.find(prop => prop.name === 'uiBehavior')?.value === 'required'; const visibilityRule = itemInstance.fieldInstance?.visibilityRule; console.log(`Field: ${fieldApiName}, Value: ${fieldValue}, isVisible: ${isVisible}, isRequired: ${isRequired}, visibilityRule: ${JSON.stringify(visibilityRule)}`); sections[sectionFacetId].columns[columnFacetId].fields[fieldApiName] = { value: fieldValue, isVisible: isVisible, isRequired: isRequired, visibilityRule: visibilityRule // Add visibility rule to the field }; } } }); } }); // Remove sections with no fields Object.keys(sections).forEach(sectionKey => { const section = sections[sectionKey]; const hasFields = Object.values(section.columns).some(column => Object.keys(column.fields).length > 0); if (!hasFields) { delete sections[sectionKey]; } }); console.log('Parsed Sections:', JSON.stringify(sections, null, 2)); return sections; }
Purpose:
The parseFlexiPageJson function takes the raw FlexiPage JSON metadata and transforms it into a structured format that represents the sections, columns, and fields as they should be rendered in the form. This structured format makes it easier to iterate over and render the form dynamically in the component's template.
Process Breakdown:
Let's go through the function step by step.
1. Function Declaration and Initial Setup
export function parseFlexiPageJson(flexiPageJson, recordData) { const sections = {}; // Object to store sections and their fields console.log('Starting to parse flexiPageJson:', flexiPageJson); }
Parameters:
- flexiPageJson: The JSON metadata of the FlexiPage obtained from the Apex class.
- recordData: An object containing the current field values of the record (used for evaluating visibility rules).
Initialization:
- We initialize an empty object sections to store the parsed sections.
2. First Pass: Collect Sections and Their Labels
// First pass: Collect sections and their labels flexiPageJson.flexiPageRegions.forEach(region => { region.itemInstances.forEach(itemInstance => { if (itemInstance.componentInstance && itemInstance.componentInstance.componentName === 'flexipage:fieldSection') { const sectionFacetId = itemInstance.componentInstance.componentInstanceProperties.find(prop => prop.name === 'columns').value; const sectionLabel = itemInstance.componentInstance.componentInstanceProperties.find(prop => prop.name === 'label').value || 'Unnamed Section'; if (!sections[sectionFacetId]) { sections[sectionFacetId] = { label: sectionLabel, columns: {} }; } } }); });
Objective: Identify all the sections defined in the FlexiPage and extract their labels.
Process:
- Iterate over flexiPageRegions in the FlexiPage JSON. Each region represents a top-level area in the page layout.
- For each region, iterate over its itemInstances.
- Check if the itemInstance is a componentInstance and if its componentName is 'flexipage:fieldSection'. This indicates that it's a section component.
- Extract the sectionFacetId from the component's properties. This ID links to the columns within the section.
- Extract the sectionLabel from the component's properties. If no label is provided, default to 'Unnamed Section'.
- Add an entry to the sections object using the sectionFacetId as the key, initializing its label and an empty columns object.
Example:
After this pass, sections might look like:
{ 'Facet-Section1': { label: 'Opportunity Information', columns: {} }, 'Facet-Section2': { label: 'Additional Details', columns: {} } }
3. Second Pass: Collect Columns and Assign to Sections
// Second pass: Collect columns and assign to sections
flexiPageJson.flexiPageRegions.forEach(region => {
region.itemInstances.forEach(itemInstance => {
if (itemInstance.componentInstance && itemInstance.componentInstance.componentName === 'flexipage:column') {
const columnFacetId = itemInstance.componentInstance.componentInstanceProperties.find(prop => prop.name === 'body').value;
const sectionFacetId = region.name;
const side = parseInt(itemInstance.componentInstance.identifier.replace('flexipage_column', ''), 10) % 2 === 0 ? 'right' : 'left';
if (sections[sectionFacetId]) {
sections[sectionFacetId].columns[columnFacetId] = { side: side, fields: {} };
}
}
});
});
Objective: Identify columns within each section and assign them accordingly.
Process:
- Again, iterate over flexiPageRegions and their itemInstances.
- Check if the itemInstance is a componentInstance with componentName of 'flexipage:column'.
- Extract the columnFacetId from the component's properties. This ID links to the fields within the column.
- The sectionFacetId is obtained from the region.name, which links the column back to its parent section.
- Determine the side (left or right) based on the identifier. This helps in rendering columns appropriately.
- If the sectionFacetId exists in sections, add the column to the columns object within that section, initializing an empty fields object.
Example:
After this pass, sections might look like:
{ 'Facet-Section1': { label: 'Opportunity Information', columns: { 'Facet-Column1': { side: 'left', fields: {} }, 'Facet-Column2': { side: 'right', fields: {} } } }, // Other sections... }
4. Third Pass: Assign Fields to Columns
// Third pass: Assign fields to columns flexiPageJson.flexiPageRegions.forEach(region => { if (region.type === 'Facet') { region.itemInstances.forEach(itemInstance => { if (itemInstance.fieldInstance) { const fieldApiName = itemInstance.fieldInstance.fieldItem.replace('Record.', ''); const columnFacetId = region.name; const sectionFacetId = Object.keys(sections).find(sectionId => Object.keys(sections[sectionId].columns).includes(columnFacetId) ); if (sectionFacetId && sections[sectionFacetId].columns[columnFacetId]) { const fieldValue = itemInstance.fieldInstance?.fieldInstanceProperties?.find(prop => prop.name === 'value')?.value || ''; const isVisible = true; // Initially set all fields to visible const isRequired = itemInstance.fieldInstance?.fieldInstanceProperties?.find(prop => prop.name === 'uiBehavior')?.value === 'required'; const visibilityRule = itemInstance.fieldInstance?.visibilityRule; console.log(`Field: ${fieldApiName}, Value: ${fieldValue}, isVisible: ${isVisible}, isRequired: ${isRequired}, visibilityRule: ${JSON.stringify(visibilityRule)}`); sections[sectionFacetId].columns[columnFacetId].fields[fieldApiName] = { value: fieldValue, isVisible: isVisible, isRequired: isRequired, visibilityRule: visibilityRule // Add visibility rule to the field }; } } }); } });
Objective: Assign individual fields to their respective columns within sections, capturing necessary properties.
Process:
- Iterate over flexiPageRegions where region.type is 'Facet'.
- For each region, iterate over itemInstances.
- Check if the itemInstance is a fieldInstance, indicating that it represents a field.
- Extract the fieldApiName by removing the 'Record.' prefix from fieldItem.
- The columnFacetId is obtained from region.name.
- Determine the sectionFacetId by finding which section contains the current columnFacetId.
If both sectionFacetId and the corresponding column exist, proceed to extract field properties:
- fieldValue: The default value of the field, if any.
- isVisible: Initially set to true; actual visibility will be calculated later.
- isRequired: Determined by checking if uiBehavior is set to 'required'.
- visibilityRule: Any visibility rules associated with the field.
Add the field to the fields object within the corresponding column.
Example:
After this pass, a section might look like:
'Facet-Section1': { label: 'Opportunity Information', columns: { 'Facet-Column1': { side: 'left', fields: { 'Name': { value: '', isVisible: true, isRequired: true, visibilityRule: null }, // Other fields... } }, // Other columns... } }
5. Clean Up: Remove Sections with No Fields
// Remove sections with no fields Object.keys(sections).forEach(sectionKey => { const section = sections[sectionKey]; const hasFields = Object.values(section.columns).some(column => Object.keys(column.fields).length > 0); if (!hasFields) { delete sections[sectionKey]; } });
Objective: Ensure that we only keep sections that have fields to render.
Process:
- Iterate over each section in sections.
- Check if any of the columns within the section have fields.
- If a section has no fields in any of its columns, remove it from sections.
6. Final Output and Return
console.log('Parsed Sections:', JSON.stringify(sections, null, 2)); return sections;
Objective: Log the final parsed sections and return the sections object.
Interaction with the Component:
- The sections object returned by parseFlexiPageJson is used by the component to render the form dynamically.
- The structure of sections aligns with how the component's template iterates over sections, columns, and fields.
- By transforming the complex nested JSON into a manageable structure, the component can easily access field properties, such as isRequired and visibilityRule, and render the form accordingly.
Understanding the Data Structure:
The sections object has the following structure:
{ 'SectionFacetId': { label: 'Section Label', columns: { 'ColumnFacetId': { side: 'left' or 'right', fields: { 'FieldApiName': { value: 'Field Value', isVisible: true or false, isRequired: true or false, visibilityRule: { /* Visibility Rule Object */ } }, // Other fields... } }, // Other columns... } }, // Other sections... }
4. Component Metadata Configuration (flexipageEditForm.js-meta.xml)
This XML file defines the metadata for the component, specifying its properties and where it can be used.
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>61.0</apiVersion> <isExposed>true</isExposed> <masterLabel>Opp. Readiness Edit Form</masterLabel> <description>Dynamic form component based on Flexipage layout</description> <targets> <target>lightning__FlowScreen</target> </targets> <targetConfigs> <targetConfig targets="lightning__FlowScreen"> <property name="objectApiName" type="String" label="Object API Name"/> <property name="recordId" type="String" label="Record ID" role="inputOnly"/> <property name="flexiPageName" type="String" label="Flexipage Name" description="Developer name of the Flexipage to use"/> <property name="cardTitle" type="String" label="Card Title" description="Title of the card component"/> <property name="showIcon" type="Boolean" label="Show Icon" description="Show object icon"/> <property name="recOutput" type="@salesforce/schema/Opportunity_Readiness__c" /> </targetConfig> </targetConfigs> </LightningComponentBundle>
Key Points:
- apiVersion: Specifies the API version the component is compatible with.
- isExposed: Indicates that the component can be used outside its own namespace (e.g., in Flows).
- targets: Defines where the component can be used; in this case, lightning__FlowScreen.
- targetConfigs: Configures properties for the specified targets.
Properties Exposed to the Flow:
- objectApiName: The API name of the object to operate on.
- recordId: The ID of the record to edit.
- flexiPageName: The developer name of the FlexiPage to use for rendering the form.
- cardTitle: The title displayed on the component card.
- showIcon: Boolean to determine if the object icon should be displayed.
- recOutput: Output variable to pass data back to the Flow.
Bringing It All Together
Our custom Lightning Web Component serves as a dynamic and flexible bridge between Salesforce's Dynamic Forms and Flow screens. By consuming the metadata of a FlexiPage, it renders a form that adapts based on user input and defined visibility rules.
How It Interacts with the Flow Container:
- Input Properties: The Flow passes in parameters like recordId, objectApiName, and flexiPageName to customize the component's behavior.
- Output Variables: The component updates Flow variables (like recOutput) using FlowAttributeChangeEvent, allowing the Flow to use the data entered by the user.
- Real-Time Updates: As users interact with the form, changes are immediately available to the Flow context.
How It Interacts with the Apex Class:
- Data Fetching: The component calls Apex methods to retrieve FlexiPage metadata and field values.
- Asynchronous Operations: Uses Promises (.then() and .catch()) to handle asynchronous Apex calls.
- Error Handling: Captures and displays errors that occur during data fetching.
Benefits of This Approach:
- Centralized Management: Administrators can manage fields, sections, and visibility rules in the Lightning App Builder, reducing maintenance overhead.
- Dynamic Rendering: The form adapts to changes in the metadata without code changes.
- Enhanced User Experience: Provides a guided and interactive interface within Flows, improving user satisfaction and data quality.
Wrapping Up
By dissecting each file and understanding the interactions between the component, the Flow container, and the Apex class, we've built a powerful tool that extends the capabilities of Salesforce Flows. This solution addresses the business challenge of creating scalable, dynamic, and user-friendly Readiness Assessments.
Discussions
Login to Post Comments