Skip to content
On this page

Last updated:

Simple iframe bridge

A simple iframe bridge to communicate between the main window and an iframe. This is useful for loading a web app in an iframe and communicating with it.

Outside iframe

typescript
async function iframeCall(iframe: Window|null, type: string, data?: any){
    return new Promise<any>((resolve, reject) => {
        if(!iframe){
            console.warn("Iframe window is not available");
            return;
        }
        iframe.postMessage({type, data}, "*");
        const listener = (event: MessageEvent) => {
            const {type: eventType, data: eventData} = event.data;
            if(eventType === type){
                resolve(eventData);
                window.removeEventListener("message", listener);
            }
        }
        window.addEventListener("message", listener);
        setTimeout(() => {
            reject(new Error("Timeout waiting for iframe response"));
            window.removeEventListener("message", listener);
        }, 5000);
    })
}

export interface IframeBridge<TType extends string = string, TData extends object | undefined = object | undefined>{
    window: Window|null
    call: (type: TType, data?: TData) => Promise<any>
}
export async function initIFrameBridge<TType extends string = string, TData extends object | undefined = object | undefined>(iframe: HTMLIFrameElement, initCommand: string|null = 'init'){
    const res: IframeBridge<TType, TData> = {
        window: null as  Window|null,
        async call(type: TType, data?: TData){
            if(!res.window) throw new Error("Iframe window is not available");
            return await iframeCall(res.window, type, data);
        },
    }
    return new Promise<IframeBridge<TType, TData>>((resolve, reject) => {
        iframe.onload = async () => {
            if (iframe.contentWindow) {
                if(initCommand) await iframeCall(iframe.contentWindow, initCommand)
                res.window = iframe.contentWindow;
                resolve(res)
                // await iframeCall(iframe.contentWindow, 'initEmpty')
            }else {
                console.error("Iframe window is not available");
                reject(new Error("Iframe window is not available"));
            }
        };
        iframe.onerror = reject;
    })
}

Inside iframe

typescript
class API{
    actions = {
        'init': async()=>{
            console.log('initialized')
        },
        'import': async(payload: any)=>{
            console.log('import', payload);
        },
        'export': async ()=>{
            console.log('export');
            return {data: 1234};
        }
    }
    parentWindow: Window|null = null;
    constructor() {
        window.addEventListener("message", async ({data, source}) => {
            if (!this.parentWindow && (source as Window).top === source && data?.type === 'init') {
                this.parentWindow = source as Window;
            }
            if (source !== this.parentWindow) {
                // console.error("Invalid source window:", source, this.parentWindow, data);
                // console.warn('Window message', data)
                return
            }
            const {type, payload} = data;
            let res = undefined;
            if(this.actions[type]) {
                res = await this.actions[type](payload);
            }
            this.parentWindow.postMessage({type, data: res}, '*');
        });
    }
}

Usage

sveltehtml
<script lang="ts">
  let bridge: IframeBridge | null = null;
  onMount(() => {
    const iframe = document.getElementById("pageNodeEditor") as HTMLIFrameElement;
    if (iframe) {
      initIFrameBridge(iframe, 'init').then(async (res)=>{
        bridge = res;
      })
    }
  });
</script>
{#if bridge !== null}
<button class="btn-export" on:click={()=>bridge.call('export', {data: 1234})}>Export</button>
{/if}

Made with ❤️ using the awesome vitepress