@hj5230/xframe
v0.2.1
Published
Type-safe cross-frame communication via postMessage
Maintainers
Readme
@hj5230/xframe
Type-safe cross-frame communication via postMessage.
Provides IframeController (host side) and IframeGuest (guest side) with a generic { type, payload } message protocol, plus ready-to-use Preact hooks.
Installation
npm install @hj5230/xframe
# or
pnpm add @hj5230/xframeMessage protocol
All messages follow a discriminated union shape. You define two maps — one for each direction — and the library enforces them end-to-end:
// Define payload maps (type key → payload shape)
type HostToGuest = {
'set-title': { text: string };
ping: {};
};
type GuestToHost = {
ready: {};
pong: { ts: number };
click: { target: string };
};Core classes
Framework-agnostic. Import from @hj5230/xframe.
IframeController<TOutMap, TInMap> — host side
import { IframeController } from '@hj5230/xframe';
const ctrl = new IframeController<HostToGuest, GuestToHost>(
document.querySelector('iframe')!,
);
// Send a typed message to the guest
ctrl.send('set-title', { text: 'Hello' });
// Listen for a specific type — payload is narrowed automatically
ctrl.on('pong', ({ ts }) => console.log('latency', Date.now() - ts));
// Wildcard — receives full { type, payload } object
ctrl.on('*', (msg) => console.log(msg.type, msg.payload));
// Clean up when done
ctrl.destroy();IframeGuest<TOutMap, TInMap> — guest side
import { IframeGuest } from '@hj5230/xframe';
const guest = new IframeGuest<GuestToHost, HostToGuest>();
// Announce readiness
guest.send('ready', {});
// Listen for commands from the host
guest.on('set-title', ({ text }) => (document.title = text));
guest.on('ping', () => guest.send('pong', { ts: Date.now() }));
guest.destroy();Preact hooks
Import from @hj5230/xframe/preact. Requires preact >= 10 as a peer dependency.
useIframeController — host side
import { useRef } from 'preact/hooks';
import { useIframeController } from '@hj5230/xframe/preact';
const Host = () => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [log, setLog] = useState<string[]>([]);
const { send, ready } = useIframeController<HostToGuest, GuestToHost>(
iframeRef,
{
// Per-type handlers — payload is narrowed
pong: ({ ts }) => console.log('latency', Date.now() - ts),
click: ({ target }) => console.log('clicked', target),
// Wildcard — receives full { type, payload }
'*': (msg) => setLog((prev) => [...prev, msg.type]),
},
{ readyEvent: 'ready' }, // sets `ready = true` when guest sends this type
);
return (
<>
<button disabled={!ready} onClick={() => send('set-title', { text: 'Hi' })}>
Send
</button>
<iframe ref={iframeRef} src="/guest.html" />
</>
);
};useIframeGuest — guest side
import { useState } from 'preact/hooks';
import { useIframeGuest } from '@hj5230/xframe/preact';
const Guest = () => {
const [title, setTitle] = useState('');
const { send } = useIframeGuest<GuestToHost, HostToGuest>(
{
'set-title': ({ text }) => setTitle(text),
// `send` is passed as the second argument for reply patterns
ping: (_payload, send) => send('pong', { ts: Date.now() }),
},
{ readyMessage: { type: 'ready', payload: {} } }, // sent automatically on mount
);
return (
<div onClick={(e) => send('click', { target: (e.target as HTMLElement).tagName })}>
<h1>{title}</h1>
</div>
);
};API reference
IframeController<TOutMap, TInMap>
| Method | Description |
|--------|-------------|
| send(type, payload) | Send a typed message to the guest |
| on(type, cb) | Subscribe to a specific message type; returns unsubscribe function |
| on('*', cb) | Subscribe to all messages; cb receives { type, payload } |
| destroy() | Remove all listeners and the window message handler |
IframeGuest<TOutMap, TInMap>
Same API as IframeController, except send posts to window.parent.
useIframeController(iframeRef, handlers?, options?)
| Parameter | Type | Description |
|-----------|------|-------------|
| iframeRef | RefObject<HTMLIFrameElement> | Ref to the iframe element |
| handlers | HandlerMap | Optional map of type → (payload, send) => void; '*' key for wildcard |
| options.readyEvent | string | Message type that flips ready to true |
Returns { send, ready }.
useIframeGuest(handlers?, options?)
| Parameter | Type | Description |
|-----------|------|-------------|
| handlers | GuestHandlerMap | Optional map of type → (payload, send) => void |
| options.readyMessage | ToMessage<TOutMap> | Message sent on mount |
Returns { send }.
Types
import type { BaseMap, ToMessage } from '@hj5230/xframe';
// BaseMap — the constraint for your payload maps
type BaseMap = Record<string, unknown>;
// ToMessage — derives the discriminated union from a map
type ToMessage<M extends BaseMap> = {
[K in keyof M & string]: { type: K; payload: M[K] };
}[keyof M & string];License
MIT
