'How do I post a message to a iframe (not webview) in React-Native?

I'm currently working with React-Native and need some ideas on how to solve an issue I'm having involving iframes (not WebView).

I have a functional component written in TypeScript that uses React.createElement to create the iframe and trying to call/invoke a function after the iframe has been created/mounted without re-rendering it. My original plan was to try and inject JavaScript similar to how a WebView does it, however I've been unable to do this without re-rending. So, my plan B is to message the iframe using postMessage where during onMessage a function will run window[stringFunction](parameters) (using window to run a function by using its name in string form (similar to eval)).

This should work, however I need access to the iframe's contentWindow to use postMessage, unless there is a different method to communicate with the iframe without re-rendering it. React-Native uses 'ref' instead of 'window.getElementById' to reference components after mounting/creation, however ref doesn't provide access to the iframe's contentWindow or postMessage like window.getElementById does...

My question is how I get access to postMessage to send a message to the iframe and do it without re-rending it?

Component:

// Component (Removed majority of unrelated code)

import * as React from 'react';

interface SourceHTML {  // Source object passed by source prop
    html: string;
}

type IFrameProps = {  // Props
    source: SourceHTML,
}

export interface Ref {  // Methods - Accessible by parent when using ref.current
    injectJavaScript?: (value: string) => void,
}

const IFrameComponent = React.forwardRef<Ref, IFrameProps>((props, ref) => {
    const iframeRef = React.useRef(null);   // IFrame reference
    const { source } = props;

    React.useImperativeHandle(ref, () => ({
        injectJavaScript: (message: string) => {
            if (iframeRef && iframeRef.current) {

                if(message.toString().includes("true")) {
                    const pieces: Array<string> = message.toString().split(";")[0].trim().split("(")
                    const stringFunction = pieces[0].trim()
                    const parameters = pieces[1].replace(")", "")
                    const messageObj = {
                        action: "evaluateFunction",
                        stringFunction: stringFunction,
                        parameters: parameters,
                    }
                    iframeRef.current.contentWindow.postMessage(messageObj, '*');   // <---- This does not work...
            }
        }
    }}))

    const handleInjectedJavaScript = (html) => {
        if (html) {
            const javascript = `
                function evaluateFunction(stringFunction, parameters) {
                    window[stringFunction](parameters)
                }

                window.addEventListener("message", (event) => {
                    alert("Message recieved")
                    if(typeof message === 'object' && message.action === "evaluateFunction") {
                        alert("Message is an object")
                        const {stringFunction: stringFunction, parameters: parameters} = message;
                        evaluateFunction(stringFunction, parameters);
                    }
                })
            `
            if(!html.includes(javascript)) {
                html = html.replace('</body>', `<script>${javascript}</script></body>`);
                alert(!html.includes(javascript))
            }
        }
    }

    return (
        React.createElement(
            'iframe',
            {
                ref: iframeRef,
                id: "IFrame",
                srcDoc: handleInjectedJavaScript(source.html),
                frameBorder: '0',
                seamless: true,
            }
        )
    )
})

IFrameComponent.displayName = 'IFrame';

export {IFrameComponent}
export default IFrameComponent;

Then to use:

// Simple Alert (Example)

const webViewRef = React.useRef();

const html = `
 <html>
    <body>
        <script>
            function AlertMessage(message) {
                alert(message);
            }
        </script>
    </body>
 </html>
`;

webViewRef.current.injectJavaScript("alert('Hello');true;");

<IFrameComponent 
    ref={webViewRef}
    source={{ html: html }}
/>

This example does not reflect the end result and is a very stripped down version to help isolate the issue. I've removed all the other features. Also, this example will ONLY work on the web.

I plan on releasing the full version to Github (it will be my first repository) once it has been tested and is functioning like intended. This project is based on React-Native Web WebView, however it has been completely re-written as a functional component with TypeScript AND works on android, ios and web AND has postMessage (from iframe to React-Native), onLoadStart, onLoad and onLoadEnd all working properly.

Once I've got postMessage working the other way (React-Native to iframe), then it should be compatible with the majority of npm/yarn packages that work on the native WebView. For example, this would allow you to use react-native-signature-canvas and have it fully-functional on the web.

Thank you for your time and I appreciate any help/ideas you are able to provide.

- Logan King (aka DarkComet)

Github profile



Solution 1:[1]

I decided to search one last time and decided to check out the TypeScript library files and I found the reference to HTMLElement and HTMLIFrameElement... There it was, it's interface:

interface HTMLIFrameElement extends HTMLElement {

...

/**
     * Retrieves the document object of the page or frame.
     */
    readonly contentDocument: Document | null;
    /**
     * Retrieves the object of the specified.
     */
    readonly contentWindow: WindowProxy | null;

...

}

It verifies that contentWindow does exist and I found a simple mistake early in the code that was the issue.

I'll clean up the edges and finish up the Github repo page I am working on for this!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 DarkComet