Comctx

Iframe

Build a Comctx connection between a host page and an iframe using the real file layout from this repository.

Example setup

This example is a browser project that uses an iframe-based communication flow.

It includes:

  • a host page
  • an iframe page
  • a message channel based on window.postMessage

What this page covers

This page is aligned with the real examples/iframe source in this repository. The host page is the Injector, and the iframe page is the Provider.

Source

File layout

The real example is organized like this:

index.html
iframe.html
main.ts
main.ts
adapter.ts
counter.ts

Installation

Install comctx in your project:

pnpm add comctx
npm install comctx
yarn add comctx
bun add comctx

Create the shared counter service

Create counter.ts:

counter.ts
counter.ts
export default class Counter {
  public value = 0

  async getValue() {
    return this.value
  }

  async onChange(callback: (value: number) => void) {
    let oldValue = this.value
    setInterval(() => {
      const newValue = this.value
      if (oldValue !== newValue) {
        callback(newValue)
        oldValue = newValue
      }
    })
  }

  async increment() {
    return ++this.value
  }

  async decrement() {
    return --this.value
  }
}

This is the real service implementation used by the iframe example.

Why this service matters

The example uses async methods and an onChange callback so the host page and iframe page can stay in sync through the Comctx boundary.

Create the adapters

Create adapter.ts:

adapter.ts
adapter.ts
import { Adapter, SendMessage, OnMessage } from 'comctx'

export default class ProvideAdapter implements Adapter {
  sendMessage: SendMessage = (message) => {
    window.parent.postMessage(message, '*')
  }

  onMessage: OnMessage = (callback) => {
    const handler = (event: MessageEvent) => callback(event.data)
    window.parent.addEventListener('message', handler)
    return () => window.parent.removeEventListener('message', handler)
  }
}
adapter.ts
import { Adapter, SendMessage, OnMessage } from 'comctx'

export class InjectAdapter implements Adapter {
  sendMessage: SendMessage = (message) => {
    window.postMessage(message, '*')
  }

  onMessage: OnMessage = (callback) => {
    const handler = (event: MessageEvent) => {
      callback(event.data)
    }
    window.addEventListener('message', handler)
    return () => window.removeEventListener('message', handler)
  }
}

The real example keeps both adapters in the same file.

Production note

The repository example uses '*' for simplicity. In production, prefer explicit origins and validate incoming messages before forwarding them.

Create the iframe page entry

Create src/iframe/main.ts:

main.ts
main.ts
import './style.css'
import { defineProxy } from 'comctx'
import Counter from '../service/counter'
import ProvideAdapter from '../service/adapter'

// Register the proxy object
void (async () => {
  const [provideCounter] = defineProxy(() => new Counter(), {
    namespace: '__iframe-example__'
  })

  const counter = provideCounter(new ProvideAdapter())

  document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
    <div>
      <h1>I am an iframe page</h1>
      <div class="card">
        <h4>Value: <span data-testid="value" id="value">${counter.value}</span></h4>
      </div>
    </div>
  `

  counter.onChange((value) => {
    document.querySelector<HTMLSpanElement>('#value')!.textContent = `${value}`
  })
})().catch(console.error)

This is the Provider side. The iframe owns the real Counter instance and updates its own UI when the value changes.

Create the host page entry

Create src/index/main.ts:

main.ts
main.ts
import { name, description } from '../../package.json'

import './style.css'

import createElement from '../utils/createElement'

import { defineProxy } from 'comctx'
import { InjectAdapter } from '../service/adapter'
import type Counter from '../service/counter'

void (async () => {
  const [, injectCounter] = defineProxy(() => ({}) as Counter, {
    namespace: '__iframe-example__'
  })

  // Use the proxy object
  const counter = injectCounter(new InjectAdapter())

  const initValue = await counter.getValue()

  document.querySelector<HTMLDivElement>('#app')!.insertBefore(
    createElement(`
      <div>
        <h1>${name}</h1>
        <p>${description}</p>
        <div class="card">
          <button data-testid="decrement" id="decrement" type="button">-</button>
          <div data-testid="value" id="value">${initValue}</div>
          <button data-testid="increment" id="increment" type="button">+</button>
        </div>
      </div>
    `),
    document.querySelector<HTMLDivElement>('#iframe')!
  )

  document.querySelector<HTMLButtonElement>('#decrement')!.addEventListener('click', async () => {
    await counter.decrement()
  })

  document.querySelector<HTMLButtonElement>('#increment')!.addEventListener('click', async () => {
    await counter.increment()
  })

  counter.onChange((value) => {
    document.querySelector<HTMLDivElement>('#value')!.textContent = value.toString()
  })
})().catch(console.error)

This is the Injector side. The host page receives a proxy and updates its UI through remote method calls and callback notifications.

Add the HTML entry files

The real example uses two HTML files.

index.html
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Index Page</title>
  </head>
  <body>
    <div id="app">
      <iframe data-testid="iframe" id="iframe" src="./iframe.html" frameborder="0"></iframe>
    </div>
    <script type="module" src="/src/index/main.ts"></script>
  </body>
</html>
iframe.html
iframe.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IFrame Page</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/iframe/main.ts"></script>
  </body>
</html>

The host page loads iframe.html, and the iframe page boots its own Provider entry from src/iframe/main.ts.

Finish

Once everything is connected, the flow looks like this:

  1. the host page calls counter.increment()
  2. Comctx turns that call into a message
  3. the InjectAdapter sends the message through window.postMessage
  4. the iframe page runs the real method on Counter
  5. the result is sent back
  6. both pages update through onChange

A working setup should satisfy all of these:

  • both sides use the same namespace
  • the host page loads iframe.html
  • the iframe page registers the Provider before handling calls
  • the host page injects the proxy through InjectAdapter
  • both sides listen on the expected window message channel

Next steps

After this page, continue with:

On this page