1

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>

Reviewing the api.js component, the connection.promise does not resolve.

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;

                connection.promise.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.
                    console.log(child);
                    self._penpal.child = child;
                }).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?
1

but child is a Promise

No its not - you are assigning the object to your connection property. The actual Promise is on connection.promise. You are resolving later the same object which does not have the fetchRequest method.

Change the assignment from connection to connection.promise in your Promise.all and then it should be resolved correctly

  • you changed from Promise.all to single Promise thenable but still assigning child[0] as return value - therefore your child is undefined. change it to child and it should work – Renji-xD Jul 2 '19 at 23:10
  • Make sure to execute all Pre-Requisites Steps (cors, Clickjack Protection, whitelist etc.) - your 1:1 copy and pasted code works on my dev sandbox – Renji-xD Jul 3 '19 at 12:34
  • Thank you!! :-) – Robs Jul 3 '19 at 17:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.