afv-library/skills/experience-lwc-generate/references/triangle-pattern.md

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'

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