Rob on Salesforce
Embed Flows in Lightning Web Components (LWC)

Flows and Lightning Web Components: they’re the future of the Salesforce platform. So embedding a Flow in an LWC should be easy, right? Well, not quite.

At the time of this post (May 2021), you can’t embed a Flow into a Lightning Web Component. The roadmap for Flows (this video, 15:30) suggests support is coming in the Spring 22 release.

The <iframe> approach

A recent project couldn’t wait that long, so we used the iframe approach from Alex Edelstein at UnofficialSF.

You embed your Flow in a Visualforce page, then embed the Visualforce page in an iframe in the LWC. So it goes: LWC > Iframe > Visualforce page > Flow.

The code is in the ScreenFlow subdirectory of Alex’s LightningFlowComponents repository.

The Visualforce page looks like:

<apex:page id="screenFlow" showHeader="false" sidebar="false" lightningStylesheets="true">
    <html>
    <head>
        <apex:includeLightning/>
    </head>
    <body class="slds-scope">
    <div id="screenFlow"/>
    <script>
        let statusChange = function (event) {
            parent.postMessage({
                flowStatus: event.getParam("status"),
                flowParams: event.getParam("outputVariables"),
                flowOrigin: "{!$CurrentPage.parameters.origin}"
            }, "{!$CurrentPage.parameters.origin}");
        };
        $Lightning.use("c:screenFlowApp", function () {
            // Create the flow component and set the onstatuschange attribute
            $Lightning.createComponent("lightning:flow", {"onstatuschange": statusChange},
                "screenFlow",
                function (component) {
                    component.startFlow("{!$CurrentPage.parameters.flowname}", {!$CurrentPage.parameters.params});
                }
            );
        });
    </script>
    </body>
    </html>
</apex:page>

And the LWC looks like this:

<template>
    <template if:true={url}>
        <iframe width={width} height={height} src={fullUrl} frameborder="0"></iframe>
    </template>
</template>
import {LightningElement, api} from 'lwc';

export default class ScreenFlow extends LightningElement {
    @api width;
    @api height;
    @api flowName;
    @api name;
    @api flowParams;
    url;

    connectedCallback() {
        window.addEventListener("message", (event) => {
            if (event.data.flowOrigin !== this.url) {
                return;
            }
            const moveEvt = new CustomEvent('flowstatuschange', {
                detail: {
                    flowStatus: event.data.flowStatus,
                    flowParams: event.data.flowParams,
                    name: this.name,
                    flowName: this.flowName
                }
            });
            this.dispatchEvent(moveEvt);
        });
        let sfIdent = 'force.com';
        this.url = window.location.href.substring(0, window.location.href.indexOf(sfIdent) + sfIdent.length);
    }

    get fullUrl() {
        let params = (this.flowParams ? '&params=' + encodeURI(this.flowParams) : '');
        let origin = (this.url ? '&origin=' + encodeURI(this.url) : '');
        return this.url + '/apex/screenFlow?flowname=' + this.flowName +  params+origin;
    }
}

It’s complicated, but generally works well. A user can’t tell that you’re jumping through hoops to display the flow. With one problem.

The Problem

The problem with this approach is that the iframe will not resize to fit the content of the Flow. If your Flow dynamically displays more fields, they’ll either be hidden (bad) or the user will have to scroll the little iframe window (also bad).

To address this, we need to do a few things:

  1. (Visualforce page) Watch the Flow for any changes, e.g. new fields being displayed
  2. (Visualforce page) Capture the new height of the document following the change
  3. (Visualforce page) Communicate the new height back to the parent LWC
  4. (LWC) Listen for the message from the Visualforce page and resize the iframe

Watching the Flow for changes

When should we update the height of the iframe?

You could use setInterval to keep checking the height of the embedded Visualforce page’s content. This works, but it’s not ideal. If you set it to a second or more, you’ll have a noticeable lag between the content expanding and the iframe height responding. If you set it lower, your browser has to check the height several times a second — even if the page content hasn’t changed at all.

A better bet is to use a MutationObserver to watch for changes to the page. You need to set it up with three things:

  • The target node to observe, i.e. the HTML element containing your flow
  • A config object with the subtree option to detect changes within that target node
  • A callback function to run when the change (mutation) is observed

The Listener JavaScript:

function listenForContentChanges() {
  // Create a new observer instance and pass in the callback to run when the mutation is observed
  const observer = new MutationObserver(handleContentChange);

  // Define which element to observe
  const targetNode = document.getElementById('my-flow-container');

  // Options for the observer (which mutations to observe)
  const config = { childList: true, subtree: true };

  // Start observing the target node for configured mutations
  observer.observe(targetNode, config);
}

function handleContentChange() {
    // TODO: capture document height and pass this to the parent LWC
}

Capture height of iframe

Now we need to implement the handleContentChange() function. Let’s start by capturing the height of the document now that it’s changed. We don’t need to reinvent the wheel here - Googling the subject shows that there are different ways to calculate the height, but the safest bet is probably document.body.scrollHeight.

function handleContentChange() {
    let documentHeight = document.body.scrollHeight;
    // TODO: pass this to the parent LWC
}

Communicate back to the LWC

Now we know the height of the document, we need to tell the parent page to update the iframe height accordingly. The ScreenFlow Visualforce page demonstrates how to do this: use the window.postMessage() method.

function handleContentChange() {
    let documentHeight = document.body.scrollHeight;
    parent.postMessage({
        height: documentHeight
    }, "{!$CurrentPage.parameters.origin}");
}

Listen for the change in the LWC and update the iframe height

Now we’re on the home straight. We can tweak our LWC’s existing event listener to handle height messages:

connectedCallback() {
    window.addEventListener("message", (event) => {
        if (event.data.flowOrigin !== this.url) {
            return;
        }

        if (event.data.height) {
            this.handleFlowHeightChange(event.data.height);
        }

        if (event.data.flowStatus) {
            this.handleFlowStatusChange(event);
        }
    });
    let sfIdent = 'force.com';
    this.url = window.location.href.substring(0, window.location.href.indexOf(sfIdent) + sfIdent.length);
}

handleFlowHeightChange(height) {
    this.height = height;
}

handleFlowStatusChange(event) {
    const moveEvt = new CustomEvent('flowstatuschange', {
            detail: {
                flowStatus: event.data.flowStatus,
                flowParams: event.data.flowParams,
                name: this.name,
                flowName: this.flowName
            }
        });
    this.dispatchEvent(moveEvt);
}

The LWC’s HTML already allows setting the height with height={height}, so we’re done!

Gotcha 1: trying to do this from the LWC

You might be wondering “why do this in the Visualforce page? Why not have the LWC watch the Visualforce page for height changes?”

I did initially try that approach. However, Lightning Locker blocks the LWC accessing the DOM of the iframe document. You need the VF page to report its own height.

Gotcha 2: CORS

Salesforce hosts LWCs and Visualforce pages on different domains. If the LWC doesn’t receive the messages from your Visualforce page and your browser console reports CORS errors, enable Cross-Origin Requests between the two domains in the Salesforce setup menu.

Gotcha 3: Date pickers

If your Flow contains date picker input fields, you may find that the iframe does not expand to display the calendar view. To fix this, you can use the following CSS for the Visualforce page:

lightning-datepicker {
    overflow: visible;
}

Summary

  • If you can’t wait for native Flow support in LWCs, you can use an iframe as a workaround
  • This is great except that your iframe won’t resize with your flow
  • With steps above, you can fix this and create a seamless experience for your users

Last modified on 2021-04-30