Skip to main content

Bidirectional Messaging Between Host Application & Embedded Iframe

Upcoming Feature

This feature is under development and will be coming soon!

Introduction

When you embed a Holistics dashboard, it lives inside an iframe -- a separate browsing context from your application. By default, these two worlds can't talk to each other.

Embed events bridge that gap. They let your application and the embedded dashboard exchange messages using the browser's standard postMessage API, enabling use cases like:

  • Filter sync: Push filter values from your app into the dashboard, or listen when users change filters inside the iframe and apply those values elsewhere in your app.
  • Dashboard navigation: Build a custom menu (horizontal nav, tab-style switcher) in your app that triggers dashboard changes inside the iframe with a smooth, no-reload experience.
  • Error handling & token refresh: React to errors from the iframe -- show contextual messages to users, notify admins, or automatically refresh an expired JWT token to keep the session alive.

How it works

Communication flows in two directions between your application (the host) and the Holistics iframe (embedded analytics):

┌─────────────────────────────────┐       ┌─────────────────────────────────┐
│ YOUR APPLICATION │ │ HOLISTICS IFRAME │
│ (host app) │ │ (embedded dashboard) │
│ │ │ │
│ Send commands via │ host │ Receive & execute │
│ iframe.postMessage() ──────┼──to───┼─> (e.g,navigate, update filter) │
│ │ iframe│ │
│ │ │ │
│ Listen for events via │iframe │ Emit status & data via │
│ window.addEventListener <─────┼──to───┼── parent.postMessage() │
│ │ host │ │
└─────────────────────────────────┘ └─────────────────────────────────┘

Host to iframe: Your app sends commands (e.g., update filters, navigate to a different dashboard) by calling iframe.contentWindow.postMessage().

Iframe to host: The embedded dashboard emits events (e.g., filter changed, navigation completed) via parent.postMessage(), and your app listens with window.addEventListener("message", ...).

Supported events

UNDER DEVELOPMENT

Embed events are currently under design and development. The event names, payloads, and behavior described below may change before general availability.

Host to iframe

Your app sends these commands to control the embedded dashboard:

EventDescription
navigate_objectSwitch the iframe to a different dashboard or object without reloading
update_filterPush filter values into the embedded dashboard without reloading
refresh_tokenPass a new JWT token to extend the embed session without reloading
refresh_dashboardRefresh the current dashboard (e.g., after applying new filter values)

Iframe to host

The embedded dashboard emits these events to notify your app:

EventDescription
embed_loadedFires when the embed is ready; provide information about available dashboards and datasets in the embed portal
dashboard_loadedFires when a dashboard finishes loading; provide information about dashboard height and width so your app can adjust the iframe size (useful for preventing double scrollbars)
object_changedConfirms navigation succeeded; echoes back requestId so the host can track which action completed
filter_changedNotifies the host when a user changes a filter inside the embedded dashboard
embed_errorReports errors back to the host (e.g., invalid object name, permission denied, token expired)

Use cases

The examples below assume you have a Holistics dashboard embedded in an iframe like this:

<iframe
id="holistics-embed"
src="https://your-holistics-domain.com/embed/EMBED_KEY?_token=JWT_TOKEN"
style="width: 100%; height: 700px; border: 0"
allowfullscreen
></iframe>

All code samples reference this iframe by its id="holistics-embed" and use https://your-holistics-domain.com as the origin for postMessage calls.

Sync filters between your app and the dashboard

Your app has its own UI controls (dropdowns, date pickers, search bars) alongside an embedded dashboard. When a user interacts with filters on either side, you want both to stay in sync.

Example: An e-commerce platform has a region dropdown in the app's header bar. Below it, a sales dashboard is embedded. When a user picks "Europe" from the dropdown, the dashboard should filter to European data. Conversely, if the user changes the region filter inside the dashboard, the app's dropdown should update to match.

This works in two directions:

App to dashboard: Your app pushes filter values into the iframe when the user interacts with your UI.

Dashboard to app: The iframe emits a filter_changed event when the user changes a filter inside the dashboard, and your app listens and updates its own state.

// Listen for filter changes from the iframe
window.addEventListener("message", (event) => {
if (event.origin !== "https://your-holistics-domain.com") return;

if (event.data?.type === "filter_changed") {
const { name, value } = event.data.payload;
// Store filter state in your app
myApp.filters.set(name, value);
}
});
// Push filters back into the iframe
const iframe = document.getElementById("holistics-embed");

iframe.contentWindow.postMessage(
{
type: "update_filter",
payload: {
filters: { building_id: "123", date_range: "last_30_days" }
}
},
"https://your-holistics-domain.com"
);

You may want to build your own navigation for embedded dashboards -- a tab-style switcher, a sidebar menu, or dashboards nested inside your app's existing navigation hierarchy. Instead of reloading the iframe each time, use navigate_object to switch dashboards seamlessly.

Example: A SaaS platform has a horizontal tab bar with "Overview", "Sales", and "Support" tabs. Each tab maps to a different embedded dashboard. Clicking a tab sends a navigate_object command, and the iframe swaps the dashboard instantly without a full reload.

const iframe = document.getElementById("holistics-embed");

function switchDashboard(dashboardName) {
iframe.contentWindow.postMessage(
{
type: "navigate_object",
payload: { object_name: dashboardName },
requestId: `nav-${Date.now()}`
},
"https://your-holistics-domain.com"
);
}

// Wire up your custom tab navigation
document.querySelector('[data-tab="overview"]').onclick = () => switchDashboard("overview");
document.querySelector('[data-tab="sales"]').onclick = () => switchDashboard("sales_overview");
document.querySelector('[data-tab="support"]').onclick = () => switchDashboard("support_metrics");

The iframe confirms the navigation with an object_changed event, echoing back the requestId so your app can track which action completed and update the active tab state.

Trigger a dashboard refresh after external changes

When your app modifies data that the dashboard depends on (e.g., user submits a form, imports a file, or completes an action), you can tell the embedded dashboard to refresh so it reflects the latest data.

async function handleFormSubmit(data) {
await saveToDatabase(data);

const iframe = document.getElementById("holistics-embed");
iframe.contentWindow.postMessage(
{ type: "refresh_dashboard" },
"https://your-holistics-domain.com"
);
}

Track user interactions for analytics or logging

Listen for events from the embedded dashboard to understand how your users interact with analytics. You can log filter changes, navigation, and errors to your own analytics or monitoring system.

window.addEventListener("message", (event) => {
if (event.origin !== "https://your-holistics-domain.com") return;

const { type, payload } = event.data;

switch (type) {
case "filter_changed":
analytics.track("embed_filter_changed", payload);
break;
case "object_changed":
analytics.track("embed_navigation", payload);
break;
case "embed_error":
errorTracker.capture("embed_error", payload);
break;
}
});

Handle errors and refresh expired tokens

The iframe emits embed_error events when something goes wrong -- invalid object name, permission denied, or an expired token. Your app can listen for these and respond accordingly: show a contextual error message, notify an admin, or automatically refresh the token to keep the session alive.

const iframe = document.getElementById("holistics-embed");

window.addEventListener("message", (event) => {
if (event.origin !== "https://your-holistics-domain.com") return;

if (event.data?.type === "embed_error") {
const { code, message } = event.data.payload;

if (code === "token_expired") {
// Generate a new token and push it into the iframe
const newToken = await generateEmbedToken();
iframe.contentWindow.postMessage(
{
type: "refresh_token",
payload: { token: newToken }
},
"https://your-holistics-domain.com"
);
} else {
// Show error to the user or notify your team
showErrorToast(`Embed error: ${message}`);
notifyAdmin({ code, message });
}
}
});

Security best practices

Always validate the event.origin before processing any message. This prevents other iframes or windows from spoofing events:

window.addEventListener("message", (event) => {
// Only accept messages from your Holistics domain
if (event.origin !== "https://your-holistics-domain.com") return;

// Process event...
});

Open Markdown
Let us know what you think about this document :)