-
Notifications
You must be signed in to change notification settings - Fork 41
example iframe initiated window
docs/examples/connect-to-window.md shows the case where the parent shell drives the conversation — a title-bar button mounted via registerTitleBarButton calls wp.os.connect() and pushes data into a sibling iframe. This recipe covers the inverse topology: code running inside a chromeless wp-admin iframe (a Gutenberg PluginSidebar, a custom meta-box, a settings page) needs to open or talk to a sibling window — without the parent shell having to register anything per-iframe-route.
The classic case: Live Preview. The user is editing a post; a sidebar button inside the Gutenberg iframe should open a "Preview" window next to it and stream content changes live.
| Need | Use | Why |
|---|---|---|
| "Open a sibling window from inside this iframe" |
wp.os.send( 'request-open-window', { url } ) — parent listens via Window.on
|
Symmetric Window.send / wp.os.send channel, no per-target wiring. |
| "Connect back to the parent so I can publish updates" | wp.os.iframe.requestConnection({ topics }) |
Iframe initiates; parent's HOOKS.IFRAME_CONNECTION_REQUEST filter decides accept/reject. |
| "What window am I living in?" |
wp.os.iframe.windowId / await wp.os.iframe.whenWindowId()
|
Resolved after the parent's first handshake. |
| "Did the parent shell even hear me?" |
wp.os.iframe.publish now console.warns on dropped messages |
Watch DevTools. |
// In your plugin's shell-side bundle (the main `desktop.min.js`-loaded entry).
//
// We listen on the publish channel for `request-open-preview` from any
// iframe window (Gutenberg post editor). When it arrives, open a sibling
// Preview window AND wire the live content forwarder.
import { addAction, HOOKS } from 'openstation';
addAction( HOOKS.WINDOW_OPENED, 'my-plugin/wire-editor', ( e ) => {
// Iframe window ids are slugified admin filenames plus identity
// params: post.php → `post-php`, post.php?post=123 → `post-php-post-123`,
// post-new.php → `post-new-php`.
if (
! e.windowId.startsWith( 'post-php' ) &&
! e.windowId.startsWith( 'post-new-php' )
) {
return;
}
const editor = wp.os.windowManager.getById( e.windowId );
if ( ! editor ) return;
editor.on( 'request-open-preview', async ( payload ) => {
const previewId = `preview-of-${ editor.id }`;
// `registerWindow` takes a single definition object, opens (or
// focuses) the window immediately, and resolves with its
// DesktopWindow handle. `previewRender` is defined in step 3.
const preview = await wp.os.registerWindow( {
id: previewId,
title: 'Live Preview',
render: previewRender,
} );
// Open a connection back to the editor's iframe so we can forward
// its content updates to the preview window's native render.
const conn = wp.os.connect( editor.id, {
topics: [ 'editor:content' ],
} );
conn.subscribe( 'editor:content', ( html ) => {
preview.send( 'preview:html', html );
} );
} );
} );// Loaded by your plugin on post.php / post-new.php (via enqueue_block_editor_assets).
//
// Inside Gutenberg, register a sidebar button that:
// a) Asks the parent shell to open the Preview window.
// b) Starts streaming editor content over the connection.
import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/edit-post';
import { useSelect, subscribe } from '@wordpress/data';
import { Button } from '@wordpress/components';
function PreviewSidebar() {
const [ streaming, setStreaming ] = useState( false );
const open = async () => {
// Guard before awaiting the window id. `whenWindowId()` never
// rejects — if the parent shell never sends a handshake (e.g.
// cross-origin parent, page opened outside OpenStation) the
// Promise hangs forever. `isParentReachable()` resolves that
// ambiguity synchronously.
if ( ! wp.os.iframe.isParentReachable() ) {
return; // Not running inside OpenStation — bail silently.
}
const myWindowId = await wp.os.iframe.whenWindowId();
// Tell the parent shell to open a Preview window paired with us.
// `wp.os.send( channel, payload )` posts a `os-window-publish`
// message — the parent's `Window.on('request-open-preview')` handler
// (wired in step 1) fires.
wp.os.send( 'request-open-preview', {
sourceWindowId: myWindowId,
} );
// Now start publishing editor content. The parent's connection's
// subscriber forwards each batch into the preview window.
setStreaming( true );
};
useEffect( () => {
if ( ! streaming ) return;
let last = '';
const unsub = subscribe( () => {
const editor = wp.data.select( 'core/editor' );
const html = editor.getEditedPostContent();
if ( html === last ) return;
last = html;
wp.os.iframe.publish( 'editor:content', html );
} );
return unsub;
}, [ streaming ] );
return (
<PluginSidebar name="my-plugin/preview" title="Preview">
<Button isPrimary onClick={ open }>
{ streaming ? 'Streaming…' : 'Open live preview' }
</Button>
</PluginSidebar>
);
}
registerPlugin( 'my-plugin-preview', { render: PreviewSidebar } );There is no separate register-now/open-later step — wp.os.registerWindow( def ) registers AND opens in one call (step 1 makes it), so the render callback is just a plain function referenced from the definition:
function previewRender( body, ctx ) {
body.innerHTML = '<iframe id="preview-frame" style="width:100%; height:100%; border:none"></iframe>';
const iframe = body.querySelector( '#preview-frame' );
// Listen for content updates forwarded by the parent from the
// editor iframe (step 1 wires the forwarder).
ctx.window.on( 'preview:html', ( html ) => {
iframe.srcdoc = html;
} );
}-
PluginSidebar runs inside the Gutenberg iframe. That iframe has the standalone iframe-bridge installed (auto-enqueued on every admin page for OpenStation users), which exposes
wp.os.send,wp.os.iframe.publish, andwp.os.iframe.windowId. -
Step 2's
wp.os.sendturns into aos-window-publishpostMessage to the parent. The parent'sWindow.on(...)subscribers for THIS window's id fire — step 1's handler is one of them. -
Step 1 opens the Preview window via
wp.os.registerWindow( def )— which opens (or focuses) the window immediately and resolves with itsDesktopWindowhandle — and then opens a typed connection back to the editor's iframe viawp.os.connect(editor.id, { topics }). The connection handshakes throughos-bridge-handshake/…-ack. -
Step 2's
wp.os.iframe.publishfans editor content out over every open connection. The parent'sconn.subscribe(...)fires, and we forward into the preview window's native channel viapreview.send(...). -
Step 3 listens on the preview's own channel via
ctx.window.on(...)— no postMessage on this side; it's all in-process.
If wp.os.send(...) (or wp.os.iframe.publish(...)) does nothing visible:
-
No connection open →
publishlogs aconsole.warnwhen there are zero connections. Open DevTools (in the iframe's frame), look for[openstation] wp.os.iframe.publish dropped. -
No subscriber → no warning by design. Add an
addAction(HOOKS.CONNECTION_OPENED, …)log on the parent side to confirm the connection actually opened. -
Cross-origin iframe → bridges hard-filter on
window.location.origin. The Gutenberg editor-canvas (nested iframe inside post.php) usessrcdocwhich inherits the parent's origin, so it's fine; arbitrary cross-origin iframes silently drop. Seebridge-protocol.mdfor the explicit non-goal.
-
connect-to-window.md— the inverse topology (parent-initiated). -
code-editor-open.md— sibling-window opens via a different protocol (os-code-openpostMessage). -
../bridge-protocol.md— full message catalog.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config