@uniweb/frame-bridge
v0.2.3
Published
Promise-based iframe communication library with automatic dimension reporting, URL sync, and JSON-LD injection
Readme
Frame Bridge
Iframe communication library for Uniweb. Provides a bidirectional, promise-based messaging protocol between parent and child frames with origin validation, dimension reporting, URL synchronization, and custom action handlers.
Two Use Cases
Editor (primary) — Rich bidirectional protocol between the Uniweb editor and the dynamic-runtime preview iframe. Messengers are constructed programmatically with all embedding features off (the defaults), and handlers are registered dynamically via setHandler/setHandlers.
Embedding (secondary) — A non-Uniweb parent page hosts a Uniweb site in an iframe. Auto-init <script> tags opt into embedding features (URL sync, auto-resize, JSON-LD injection) for a drop-in experience.
Installation
npm install @uniweb/frame-bridgeOr use auto-init scripts via CDN for the embedding use case:
<!-- Parent page -->
<script src="https://cdn.jsdelivr.net/npm/@uniweb/frame-bridge/dist/auto/parent.min.js"></script>
<!-- Child iframe -->
<script src="https://cdn.jsdelivr.net/npm/@uniweb/frame-bridge/dist/auto/child.min.js"></script>Programmatic Usage
Parent
import { ParentMessenger } from '@uniweb/frame-bridge/parent'
const messenger = new ParentMessenger({
allowedOrigins: ['https://app.example.com'],
// Embedding features — all default to false
autoResize: true,
urlSync: true,
jsonLD: true,
// Callbacks
onIframeReady: (id, { origin, route, dimensions, metadata }) => {
console.log(`Iframe ${id} ready at ${route.path}`)
},
onRouteChange: (id, { path, title }) => {
console.log(`Iframe navigated to ${path}`)
},
onDimensionUpdate: (id, { width, height }) => {
console.log(`Iframe resized to ${height}px`)
},
// Custom action handlers
actionHandlers: {
userSelected: (iframeId, { userId }) => {
return { success: true }
},
},
})
// Send messages
messenger.sendToChild('iframe-id', 'navigate', { path: '/users/123' })
messenger.sendToAllChildren('setTheme', { theme: 'dark' })
// Query iframe state
messenger.getIframe('iframe-id') // { origin, dimensions, route, metadata }
messenger.getAllIframes()
// Update handlers after construction
messenger.setHandler('userSelected', (id, params) => { /* ... */ })
messenger.setHandlers({ action1: fn1, action2: fn2 })
// Cleanup
messenger.destroy()Child
import { ChildMessenger } from '@uniweb/frame-bridge/child'
const messenger = new ChildMessenger({
allowedOrigins: ['https://parent.example.com'],
// Reporting features — all default to false
dimensionReporting: true,
routeReporting: true,
// Custom route getter (for SPAs)
getRoute: () => ({
path: window.location.pathname,
title: document.title,
}),
// Callbacks
onParentReady: (response) => {
console.log('Connected to parent')
},
onNavigate: ({ path }) => {
window.history.pushState({}, '', path)
},
// Custom action handlers
actionHandlers: {
loadUser: ({ userId }) => {
return { user: { id: userId, name: 'John' } }
},
},
// Extra data sent with announce
metadata: { version: '1.0' },
})
// Manual updates
messenger.updateRoute('/search/results', 'Search Results')
messenger.updateDimensions()
messenger.updateJSONLD({ '@context': 'https://schema.org', '@type': 'WebPage' })
// Send messages
const result = await messenger.sendToParent('userSelected', { userId: 123 })
// Update handlers after construction
messenger.setHandlers({
myAction: (params) => { /* can access current state */ },
})
// Cleanup
messenger.destroy()React Pattern
Construct the messenger once in useState, register handlers in useEffect so they can access current React state:
import { useState, useEffect } from 'react'
import { ChildMessenger } from '@uniweb/frame-bridge/child'
function App() {
const [count, setCount] = useState(0)
const [messenger] = useState(() => new ChildMessenger({
allowedOrigins: ['https://parent.example.com'],
}))
useEffect(() => {
messenger.setHandlers({
getCount: () => ({ count }), // always reads current state
navigate: ({ path }) => {
window.history.pushState({}, '', path)
},
})
return () => messenger.destroy()
}, [messenger, count])
return <div>{/* ... */}</div>
}Embedding with Auto-Init Scripts
The auto-init scripts create a window.FrameBridge object with all embedding features enabled. No imports needed.
Parent page:
<iframe src="https://app.example.com" data-messenger-id="main"></iframe>
<script src="https://cdn.../parent.min.js"></script>
<script>
window.FrameBridge.on('routeChange', (id, { path, title }) => {
console.log('Iframe navigated to:', path)
})
window.FrameBridge.on('iframeReady', (id, info) => {
console.log('Iframe registered:', id)
})
</script>Child iframe:
<script src="https://cdn.../child.min.js"></script>
<script>
window.FrameBridge.on('parentReady', (response) => {
console.log('Connected to parent')
})
</script>The auto-init parent enables autoResize, urlSync, and jsonLD. The auto-init child enables dimensionReporting and routeReporting.
Architecture
The library is split into parent and child messengers that communicate via postMessage.
Parent-side (src/parent/):
ParentMessenger.js— Main parent frame messengerIframeRegistry.js— Tracks registered iframe metadata (origin, dimensions, route)URLSyncManager.js— Syncs parent URL with iframe routes, handles browser navigationJSONLDInjector.js— Injects structured data from iframes into parent<head>auto-init.js— Self-initializing IIFE for CDN usage
Child-side (src/child/):
ChildMessenger.js— Main iframe messengerDimensionReporter.js— ResizeObserver-based dimension reporting (accounts for body margin/padding)RouteReporter.js— Watches for route changes via pushState/replaceState patchingauto-init.js— Self-initializing IIFE for CDN usage
Shared (src/shared/):
BaseMessenger.js— Abstract base class with promise-based postMessage wrapperOriginValidator.js— Validates message origins (supports wildcards likehttps://*.example.com)constants.js— Action types, defaults, error messagesutils.js— Logger, debounce, iframe detection utilities
Message Flow
Initialization — Child announces itself to parent (
ANNOUNCE). Parent responds with iframe ID and optional initial route. Child starts reporters if enabled.Route updates (opt-in) — Child navigates internally and sends
UPDATE_ROUTE. TheonRouteChangecallback always fires. IfurlSyncis enabled, parent also updates the URL query param. Browser back/forward sendsNAVIGATEto child.Dimension updates (opt-in) — ResizeObserver detects changes. Child sends
UPDATE_DIMENSIONS. Parent auto-resizes iframe ifautoResizeis enabled.Custom actions — Both sides can register
actionHandlersfor bidirectional RPC. All messages return promises.
Build Outputs
Rollup generates multiple formats in dist/:
- ESM (
dist/esm/) — For modern bundlers - UMD (
dist/umd/) — For universal module systems - IIFE (
dist/auto/) — Auto-initializing scripts for<script>tags (minified and unminified)
Each format has separate bundles for the full library (index), parent-only (parent), and child-only (child).
API Reference
ParentMessenger
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
| allowedOrigins | string[] | Same-origin only | Allowed child origins |
| autoResize | boolean | false | Auto-resize iframes to content |
| urlSync | boolean | false | Sync parent URL with iframe routes |
| urlParamKey | string | 'path' | Query param key for routes |
| preserveOtherParams | boolean | true | Keep other query params when syncing |
| jsonLD | boolean | false | Inject JSON-LD from iframes into <head> |
| onIframeReady | function | - | (iframeId, { origin, dimensions, route, metadata }) |
| onRouteChange | function | - | (iframeId, { path, title }) |
| onDimensionUpdate | function | - | (iframeId, { width, height }) |
| actionHandlers | object | {} | Custom action handlers |
| timeout | number | 5000 | Message timeout (ms) |
| logLevel | number\|string | 3 ('INFO') | Logging verbosity |
Methods
| Method | Returns | Description |
|---|---|---|
| sendToChild(iframeId, action, params) | Promise | Send message to specific iframe |
| sendToAllChildren(action, params) | Promise | Send message to all iframes |
| getIframe(iframeId) | object\|null | Get iframe metadata |
| getAllIframes() | object[] | Get all iframe metadata |
| setHandler(action, fn) | void | Set/replace single action handler |
| setHandlers(handlers) | void | Set/replace multiple action handlers |
| setLogLevel(level) | void | Change log level |
| destroy() | void | Cleanup and remove listeners |
ChildMessenger
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
| allowedOrigins | string[] | Same-origin only | Allowed parent origins |
| dimensionReporting | boolean | false | Auto-report dimensions on resize |
| dimensionThreshold | number | 1 | Min px change to trigger report |
| routeReporting | boolean | false | Auto-report route changes |
| getRoute | function | Default getter | Returns { path, title } |
| onParentReady | function | - | (response) |
| onNavigate | function | - | ({ path }) |
| actionHandlers | object | {} | Custom action handlers |
| metadata | object | {} | Extra data sent with announce |
| timeout | number | 5000 | Message timeout (ms) |
| logLevel | number\|string | 3 ('INFO') | Logging verbosity |
Methods
| Method | Returns | Description |
|---|---|---|
| sendToParent(action, params) | Promise | Send message to parent |
| updateRoute(path, title?) | void | Manually report route change |
| updateDimensions() | void | Manually trigger dimension report |
| updateJSONLD(jsonld) | void | Send JSON-LD structured data |
| setHandler(action, fn) | void | Set/replace single action handler |
| setHandlers(handlers) | void | Set/replace multiple action handlers |
| setLogLevel(level) | void | Change log level |
| destroy() | void | Cleanup and remove listeners |
If ChildMessenger is constructed outside an iframe, it creates a no-op instance (isActive = false) with a console warning.
License
MIT
