mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
12 KiB
12 KiB
Flow-LWC-Apex Triangle: LWC Perspective
The Triangle Architecture is a foundational Salesforce pattern where Flow, LWC, and Apex work together. This guide focuses on the LWC role in this architecture.
Architecture Overview
┌─────────────────────────────────────┐
│ FLOW │
│ (Orchestrator) │
└───────────────┬─────────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
│ screens │ actionCalls │
│ <componentInstance> │ actionType="apex" │
│ │ │
▼ ▼ ▲
┌─────────────────────────┐ ┌─────────────────────────┐ │
│ LWC │ │ APEX │ │
│ (UI Component) │───▶│ (Business Logic) │─────────┘
│ │ │ │
│ • Rich UI/UX ◀── YOU ARE HERE │
│ • User Interaction │ │ • @InvocableMethod │
│ • FlowAttribute │ │ • @AuraEnabled │
│ ChangeEvent │ │ • Complex Logic │
│ • FlowNavigation │ │ • DML Operations │
│ FinishEvent │ │ │
└─────────────────────────┘ └─────────────────────────┘
│ ▲
│ @AuraEnabled │
│ wire / imperative │
└──────────────────────────┘
LWC's Role in the Triangle
| Communication Path | Mechanism | Direction |
|---|---|---|
| Flow → LWC | inputParameters → @api |
One-way input |
| LWC → Flow | FlowAttributeChangeEvent |
Event-based output |
| LWC → Flow Nav | FlowNavigationFinishEvent |
Navigation command |
| LWC → Apex | @wire or imperative |
Async call |
| Apex → LWC | Return value / wire refresh | Response |
Pattern 1: Flow Screen Component
Use Case: Custom UI component for user selection within a guided Flow.
┌─────────┐ @api (in) ┌─────────┐
│ FLOW │ ────────────────▶ │ LWC │
│ Screen │ │ Screen │
│ │ ◀──────────────── │Component│
│ │ FlowAttribute │ │
└─────────┘ ChangeEvent └─────────┘
LWC JavaScript
import { LightningElement, api } from 'lwc';
import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';
export default class RecordSelector extends LightningElement {
// Input from Flow (read-only in component)
@api recordId;
@api availableActions = [];
// Output to Flow (must dispatch event to update)
@api selectedRecordId;
handleSelect(event) {
// Update local property
this.selectedRecordId = event.detail.id;
// CRITICAL: Dispatch event to update Flow variable
this.dispatchEvent(new FlowAttributeChangeEvent(
'selectedRecordId', // Must match @api property name
this.selectedRecordId
));
}
handleFinish() {
// Navigate Flow to next screen
if (this.availableActions.find(action => action === 'NEXT')) {
this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));
}
}
}
LWC meta.xml (Flow Target)
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>66.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Record Selector</masterLabel>
<description>Custom record selection component for Flow screens</description>
<targets>
<target>lightning__FlowScreen</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__FlowScreen">
<property name="recordId" type="String" label="Record ID" description="Parent record ID"/>
<property name="selectedRecordId" type="String" label="Selected Record ID" role="outputOnly"/>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Property Roles
| Role | Description | Flow Behavior |
|---|---|---|
| (none) | Input/Output | Editable in Flow builder |
role="inputOnly" |
Input only | Cannot be used as output |
role="outputOnly" |
Output only | Read-only in Flow builder |
Pattern 2: LWC Calling Apex
Use Case: LWC needs data or operations beyond Flow context.
Wire Pattern (Reactive, Cached)
import { LightningElement, api, wire } from 'lwc';
import getRecords from '@salesforce/apex/RecordController.getRecords';
export default class RecordList extends LightningElement {
@api recordId;
records;
error;
@wire(getRecords, { parentId: '$recordId' })
wiredRecords({ error, data }) {
if (data) {
this.records = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.records = undefined;
}
}
}
Imperative Pattern (On-Demand, Fresh)
import { LightningElement, api } from 'lwc';
import { FlowNavigationFinishEvent } from 'lightning/flowSupport';
import processRecord from '@salesforce/apex/RecordController.processRecord';
export default class RecordProcessor extends LightningElement {
@api recordId;
@api availableActions = [];
isProcessing = false;
async handleSubmit() {
this.isProcessing = true;
try {
const result = await processRecord({ recordId: this.recordId });
if (result.isSuccess) {
// Navigate Flow forward
this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));
}
} catch (error) {
// Handle error (show toast, etc.)
console.error('Processing failed:', error.body.message);
} finally {
this.isProcessing = false;
}
}
}
When to Use Each Pattern
| Pattern | Use When | Caching |
|---|---|---|
@wire |
Data should refresh when inputs change | Yes (cacheable=true) |
| Imperative | User-triggered action, DML needed | No |
Pattern 3: Full Triangle Integration
Use Case: LWC in Flow screen that calls Apex for complex operations.
import { LightningElement, api, wire } from 'lwc';
import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';
import getProducts from '@salesforce/apex/ProductController.getProducts';
import calculatePricing from '@salesforce/apex/PricingService.calculate';
export default class ProductSelector extends LightningElement {
@api accountId; // Input from Flow
@api selectedProducts; // Output to Flow
@api totalPrice; // Output to Flow
@api availableActions = [];
products;
selectedIds = new Set();
// Wire: Fetch products reactively
@wire(getProducts, { accountId: '$accountId' })
wiredProducts({ data, error }) {
if (data) this.products = data;
}
handleProductSelect(event) {
const productId = event.target.dataset.id;
if (this.selectedIds.has(productId)) {
this.selectedIds.delete(productId);
} else {
this.selectedIds.add(productId);
}
this.updateFlowOutputs();
}
async updateFlowOutputs() {
// Update selected products output
this.selectedProducts = Array.from(this.selectedIds);
this.dispatchEvent(new FlowAttributeChangeEvent(
'selectedProducts',
this.selectedProducts
));
// Imperative: Calculate pricing
const result = await calculatePricing({
productIds: this.selectedProducts
});
this.totalPrice = result.total;
this.dispatchEvent(new FlowAttributeChangeEvent(
'totalPrice',
this.totalPrice
));
}
handleNext() {
this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));
}
}
Jest Testing (Flow Integration)
import { createElement } from 'lwc';
import RecordSelector from 'c/recordSelector';
import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';
// Mock lightning/flowSupport
jest.mock('lightning/flowSupport', () => ({
FlowAttributeChangeEvent: jest.fn(),
FlowNavigationFinishEvent: jest.fn()
}), { virtual: true });
describe('c-record-selector', () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
});
it('dispatches FlowAttributeChangeEvent on selection', async () => {
const element = createElement('c-record-selector', {
is: RecordSelector
});
element.recordId = '001xx000003GYHAA2';
document.body.appendChild(element);
// Simulate user selection
const handler = jest.fn();
element.addEventListener('flowattributechange', handler);
const tile = element.shadowRoot.querySelector('[data-id]');
tile.click();
// Verify FlowAttributeChangeEvent was constructed
expect(FlowAttributeChangeEvent).toHaveBeenCalledWith(
'selectedRecordId',
expect.any(String)
);
});
it('dispatches FlowNavigationFinishEvent on finish', async () => {
const element = createElement('c-record-selector', {
is: RecordSelector
});
element.availableActions = ['NEXT', 'FINISH'];
document.body.appendChild(element);
const finishButton = element.shadowRoot.querySelector('.finish-button');
finishButton.click();
expect(FlowNavigationFinishEvent).toHaveBeenCalledWith('NEXT');
});
});
Deployment Order
When deploying integrated triangle solutions:
1. APEX CLASSES
└── @AuraEnabled controllers (LWC depends on these)
2. LWC COMPONENTS ← Deploy SECOND
└── meta.xml with lightning__FlowScreen target
└── JavaScript with Flow imports
3. FLOWS
└── Reference deployed LWC components
Common Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Missing FlowAttributeChangeEvent | Output never updates in Flow | Always dispatch event when output changes |
| Direct @api property mutation for outputs | Flow doesn't see changes | Use FlowAttributeChangeEvent |
| Mixing @wire and imperative for same data | Cache/freshness conflicts | Choose one pattern per data need |
| Calling Apex for Flow-available data | Unnecessary callouts | Pass via inputParameters |
| Hardcoded navigation actions | Breaks in different contexts | Check availableActions first |
Flow Events Reference
| Event | Purpose | Parameters |
|---|---|---|
FlowAttributeChangeEvent |
Update output property | (propertyName, value) |
FlowNavigationFinishEvent |
Navigate Flow | 'NEXT', 'BACK', 'PAUSE', 'FINISH' |
Related Documentation
| Topic | Location |
|---|---|
| Flow screen component template | experience-lwc-generate/assets/flow-screen-component/ |
| LWC best practices | experience-lwc-generate/references/lwc-best-practices.md |
| Apex triangle perspective | sf-apex/references/triangle-pattern.md |
| Flow triangle perspective | sf-flow/references/triangle-pattern.md |