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}