Skip to main content
1 of 4
Robs
  • 9.4k
  • 21
  • 119
  • 224

TypeError when using Penpal in Lightning Web Component to call Visualforce page via iframe

I am trying to create a LWC version of Doug Ayers library for doing promise-based service component for calling REST API from Lightning Aura Components.

Basically, the LWC communicates using Penpal via an iframe with a Visualforce page which uses jsforce to interact with the Salesforce REST API's.

There are 3 elements to this:

  1. LWC: Caller (caller.js)
  2. LWC: Parent (api.js)
  3. VF page: Child (LC_APIPage.page)

The caller.js code:

restRequest() {
    const lwcApi = this.template.querySelector('c-api');
    lwcApi.restRequest({
        'url': '/services/data/v45.0/tooling/query/?q=SELECT+Id,+Name+FROM+ApexClass+ORDER+BY+Name',
        'method': 'get',
        'body': null
    }).then(function (results) {
        console.log(JSON.stringify(results));
    }).catch(function (err) {
        console.error('LWC_API: error when calling restRequest', err);
    });
}

The caller.html code:

<template>
    <c-api></c-api>
    <lightning-button label='rest request' onclick={restRequest}></lightning-button>
</template>

The catch is getting an error:

TypeError

With no details.

Reviewing the api.js component, I thinik the issue is in method makePenpalRequest around line:

p = child.restRequest(request);

Because when I do console.log(child.restRequest) I get undefined, but child is a Promise as expected.

The LWC api.js code is below:

/* eslint-disable @lwc/lwc/no-async-operation */
import { LightningElement, wire, api } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import PP from '@salesforce/resourceUrl/penpal'
import getVisualforceDomainURL from '@salesforce/apex/LC_VisualforceDomainController.getVisualforceDomainURL';

export default class Api extends LightningElement {

    _penpal = {};
    penpalInitialized = false;
    iframeSrc = '';

    @wire(getVisualforceDomainURL) wiredDomainUrl({ error, data }) {
        if (data) {
            this.iframeSrc = data + '/apex/LC_APIPage';
            console.log(this.iframeSrc);
            this.initializePenpal();
        } else if (error) {
            console.error('LWC_API: error getting Visualforce Domain URL', error);
            this.iframeSrc = undefined;
        }
    }

    connectedCallback() {

        Promise.all([
            loadScript(this, PP)
        ]).then(() => { });
    }

    initializePenpal() {

        let initialized = this.penpalInitialized;
        var self = this;

        // Since the iframe source is calculated asynchronously,
        // we listen to the component's render events and each time
        // check if the iframe is ready, and if so, then we initialize
        // penpal to connect this component to the iframe.
        // Since we only want to do this once, we also set the initialized flag.
        if (!initialized) {

            const container = this.template.querySelector('div');
            const iframeElmt = document.createElement('iframe');
            iframeElmt.src = this.iframeSrc;
            container.appendChild(iframeElmt);

            if (iframeElmt != null) {

                this.penpalInitialized = true;

                // eslint-disable-next-line no-undef
                const connection = Penpal.connectToChild({
                    // The iframe to which a connection should be made
                    iframe: iframeElmt
                });

                this._penpal.connection = connection;

                Promise.all([connection]).then(function (child) {
                    // Cache a reference to the child so that we can
                    // use it in the restRequest/fetchRequest methods,
                    // as well as be able to destroy it when this component unrenders.
                    self._penpal.child = child[0];
                }).catch(function (err) {
                    console.error('LWC_API: Error establishing connection to iframe', err);
                    this.penpalInitialized = false;
                });

            } // else, iframe source is empty, keep waiting
        }
    }

    // Makes a Salesforce REST API request and returns a promise that resolves to the response.

    @api restRequest(request) {

        var self = this;

        var defaultRequest = {
            'method': 'get'
        };

        var defaultHeaders = {
            'Content-Type': 'application/json'
        };

        request = Object.assign({}, defaultRequest, request);
        request.headers = Object.assign({}, defaultHeaders, request.headers);

        return this.getPenpalChild().then(function (child) {
            return self.makePenpalRequest('rest', child, request);
        });
    }

    @api fetchRequest(request) {
        return this.getPenpalChild().then(function (child) {
            return this.makePenpalRequest('fetch', child, request);
        });
    }

    // ------------------------------------------------------------

    /**
     * For internal use.
     * Returns a promise waiting for the parent-child postmate handshake to complete
     * then resolves with reference to the postmate child for making requests.
     */
    getPenpalChild() {

        var child;
        var self = this;
        var timerId;
        var timeout = 10000; // ten seconds
        var pollFrequency = 500; // half a second
        var startTime = new Date().getTime();
        var endTime = startTime + timeout;

        return new Promise(function (resolve, reject) {

            child = self._penpal.child;

            if (child) {

                resolve(child);

            } else {

                // all time values in milliseconds
                timerId = setInterval(function () {

                    child = this._penpal.child;

                    if (child) {

                        // parent-child postmate handshake now complete
                        clearInterval(timerId);
                        resolve(child);

                    } else {

                        // check if we have exceeded our timeout
                        let currentTime = new Date().getTime();
                        if (currentTime > endTime) {
                            clearInterval(timerId);
                            reject('LWC_API: Timeout trying to establish connection to iframe');
                        }
                        // else, keep polling
                    }

                }, pollFrequency);

            }

        });

    }

    /**
     * For internal use.
     * Returns a promise waiting for the parent-child postmate request to complete
     * then resolves with response from the child iframe.
     */
    makePenpalRequest(requestType, child, request) {

        var p;

        if (requestType === 'rest') {
            console.log('makePenpalRequest');
            console.log(child);
            console.log(child.restRequest);
            p = child.restRequest(request);
        } else if (requestType === 'fetch') {
            p = child.fetchRequest(request);
        } else {
            p = Promise.resolve({
                success: false,
                data: 'LWC_API: Invalid request type: ' + requestType
            });
        }

        console.log('makePenpalRequest');

        return p.then(function (response) {

            console.log(response);

            if (response.success) {
                return response.data;
            }
            throw new Error(response.data);

        });

    }

    disconnectedCallback() { // unrender
        // this.superUnrender();
        // When component unrenders then cleanup penpal
        // resources by destroying the connection and nulling out
        // the helper's cached reference to the connection and child.
        // This ensures that the helper.handleXyzRequest(..) methods
        // wait appropriately for the new parent-child handshake to complete
        // when this component is re-initialized and scripts are loaded.
        if (this._penpal && this._penpal.connection) {
            this._penpal.connection.destroy();
            this._penpal = {};
        }
    }

}

The api.html code:

<template>
    <div lwc:dom="manual" class="slds-hide"></div>
</template>

Question

  1. What is causing the error?
  2. How do I fix it?
Robs
  • 9.4k
  • 21
  • 119
  • 224