# Embed events: Control embedded dashboards with JavaScript > Control embedded Holistics dashboards with JavaScript: sync filters, navigate between dashboards, and handle errors through the postMessage API :::info Upcoming Feature This feature is under development and will be coming soon! ::: ## Introduction **Embed events** let you control embedded Holistics dashboards from your application using JavaScript. Send commands, listen for user interactions, and keep your app and dashboard in sync, all through the browser's standard [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) API. Use cases include: - **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 by showing contextual messages to users, notifying admins, or automatically refreshing 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): ![Embed events communication diagram](https://media.holistics.io/bf456ce7-embed-events.png) **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 :::warning UNDER DEVELOPMENT Embed events are currently under design and development. The event names, payloads, and behavior described below may change before general availability. ::: | Event | Direction | Description | |-------|-----------|-------------| | [`embed:loaded`](#embedloaded) | **iframe** to **host** | Emits when the embed is ready. Includes a list of all available dashboards and datasets. | | [`page:change`](#pagechange) | **host** to **iframe** | Switch the iframe to a different dashboard or object without reloading. | | [`page:changed`](#pagechanged) | **iframe** to **host** | Emits after a navigation action completes. Echoes back the current page path. | | [`dashboard:run`](#dashboardrun) | **host** to **iframe** | Refresh the current dashboard. | | [`dashboard:loaded`](#dashboardloaded) | **iframe** to **host** | Emits once when the dashboard finishes its initial load. Includes metadata and default filter values. `dashboard:filters:applied` does not emit during this load. | | [`dashboard:filters:apply`](#dashboardfiltersapply) | **host** to **iframe** | Apply filter values to the current dashboard. Only takes effect after `dashboard:loaded` has emitted. | | [`dashboard:filters:applied`](#dashboardfiltersapplied) | **iframe** to **host** | Emits when the user or `dashboard:filters:apply` changes filter values, after `dashboard:loaded`. | | [`embed:token:refresh`](#embedtokenrefresh) | **host** to **iframe** | Pass a new JWT token to extend the embed session. | | [`embed:error`](#embederror) | **iframe** to **host** | Emits when an error occurs inside the embed. | ### Host to iframe Your app sends these commands to control the embedded dashboard: #### `page:change` Switch the iframe to a different dashboard or object without reloading. Event structure: ```javascript { eventName: "page:change", payload: { path: string, } } ``` Event properties: Path to navigate to. Use /objects/{'{'}object_name{'}'} for a dashboard or dataset, or /ai for the Ask AI page. --- #### `dashboard:filters:apply` Apply filter values to the current dashboard and reload its data without reloading the iframe. This command only takes effect after `dashboard:loaded` has emitted. Event structure: ```javascript { eventName: "dashboard:filters:apply", payload: { filters: { [block_name]: { operator: string, values: string[], modifier?: string, } } } } ``` Event properties: Map of filter block names to filter values. Each entry includes the following properties:operator: Filter operator (e.g., "is", "is_in_the_range").values: Array of filter values to apply.modifier: Optional. Filter modifier (e.g., for relative date ranges). --- #### `dashboard:run` Refresh the current dashboard. Event structure: ```javascript { eventName: "dashboard:run", } ``` No payload required. --- #### `embed:token:refresh` Pass a new JWT token to extend the embed session without reloading the iframe. Event structure: ```javascript { eventName: "embed:token:refresh", payload: { token: string, } } ``` Event properties: The new JWT token to replace the current session token. --- ### Iframe to host The embedded dashboard emits these events to notify your app: #### `embed:loaded` Emits when the embed is ready. Provides a list of all dashboards and datasets available in the embed portal, useful for building custom navigation. Event structure: ```javascript { eventName: "embed:loaded", payload: { objects: { dashboards: [ { uname: string, id: string, title: string, path: string, workspace?: 'org' | 'personal', } ], datasets: [ { uname: string, id: string, title: string, } ], } } } ``` Event properties: List of dashboards available in the embed. Each item includes the following properties:uname: Unique identifier for the dashboard.id: Dashboard ID.title: Display title.path: URL path to the dashboard.workspace: Optional. 'org' if the dashboard belongs to the organization workspace, 'personal' if it belongs to a personal workspace. List of datasets available in the embed. Each item includes the following properties:uname: Unique identifier for the dataset.id: Dataset ID.title: Display title. --- #### `dashboard:loaded` Emits once when the dashboard finishes its initial load. At this point, dashboard metadata and default filter values are available to the host app. Note: `dashboard:filters:applied` only emits for filter changes made after this event. The initial filter values captured here do not trigger it. Event structure: ```javascript { eventName: "dashboard:loaded", payload: { dashboard: { uname: string, id: string, title: string, path: string, workspace?: 'org' | 'personal', }, filters: { [block_name]: { operator: string, value: unknown, modifier?: string, } } } } ``` Event properties: Metadata about the loaded dashboard:uname: Unique identifier for the dashboard.id: Dashboard ID.title: Display title.path: URL path to the dashboard.workspace: Optional. 'org' or 'personal'. The dashboard's current filter values at load time. Each entry includes the following properties:operator: Filter operator.value: Current filter value.modifier: Optional. Filter modifier. --- #### `page:changed` Emits after a navigation action completes. Echoes back the current page path so your app can stay in sync. Event structure: ```javascript { eventName: "page:changed", payload: { path: string, } } ``` Event properties: The current page path after navigation (e.g., /objects/{'{'}object_uname{'}'} or /ai). --- #### `dashboard:filters:applied` Emits whenever filter values change on the dashboard, either by the user or programmatically via `dashboard:filters:apply`. This event is only active after `dashboard:loaded` has emitted; filter changes during the initial load (including default filter values) do not trigger it. Only the changed filters are included in the payload. Event structure: ```javascript { eventName: "dashboard:filters:applied", payload: { dashboard: { uname: string, id: string, title: string, type: string, }, filters: { [block_name]: { operator: string, value: unknown, modifier?: string, } } } } ``` Event properties: Metadata about the dashboard where filters were applied:uname: Unique identifier for the dashboard.id: Dashboard ID.title: Display title.type: Dashboard type. Map of filter block names to their updated values. Only changed filters are included. Each entry includes the following properties:operator: Filter operator.value: Current filter value.modifier: Optional. Filter modifier. --- #### `embed:error` Emits when an error occurs inside the embed (e.g., invalid object name, permission denied, or token expired). Event structure: ```javascript { eventName: "embed:error", payload: { error: { message: string, } } } ``` Event properties: Human-readable description of the error. ## Use cases The examples below assume you have a Holistics dashboard embedded in an iframe like this: ```html ``` 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. ```js // Push filters back into the iframe const iframe = document.getElementById("holistics-embed"); iframe.contentWindow.postMessage( { eventName: "dashboard:filters:apply", payload: { filters: { building_id: { operator: "is", values: ["123"], }, date_range: { operator: "is_in_the_range", values: ["last_30_days"], }, }, }, }, "https://your-holistics-domain.com" ); ``` **Dashboard to app:** The iframe emits a `dashboard:filters:applied` event when the user changes a filter inside the dashboard, and your app listens and updates its own state. ```js // Listen for filter changes from the iframe window.addEventListener("message", (event) => { if (event.origin !== "https://your-holistics-domain.com") return; if (event.data?.eventName === "dashboard:filters:applied") { const { filters } = event.data.payload; // Store filter state in your app myApp.filters.set(filters); } }); ``` ### Navigate between dashboards without reloading the iframe 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 `page:change` 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 `page:change` command, and the iframe swaps the dashboard instantly without a full reload. ```js const iframe = document.getElementById("holistics-embed"); function switchDashboard(dashboardName) { iframe.contentWindow.postMessage( { eventName: "page:change", payload: { path: `/objects/${dashboardName}` }, }, "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 a `page:changed` event, echoing back the new path 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. ```js async function handleFormSubmit(data) { await saveToDatabase(data); const iframe = document.getElementById("holistics-embed"); iframe.contentWindow.postMessage( { eventName: "dashboard:run", payload: {} }, "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. ```js window.addEventListener("message", (event) => { if (event.origin !== "https://your-holistics-domain.com") return; const { eventName, payload } = event.data; switch (eventName) { case "dashboard:filters:applied": analytics.track("embed_filter_changed", payload); break; case "page: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. ```js const iframe = document.getElementById("holistics-embed"); window.addEventListener("message", async (event) => { if (event.origin !== "https://your-holistics-domain.com") return; if (event.data?.eventName === "embed:error") { const { error } = event.data.payload; if (error.code === "token_expired") { // Generate a new token and push it into the iframe without reloading const newToken = await generateEmbedToken(); iframe.contentWindow.postMessage( { eventName: "embed:token:refresh", payload: { token: newToken }, }, "https://your-holistics-domain.com" ); } else { // Show error to the user or notify your team showErrorToast(`Embed error: ${error.message}`); notifyAdmin(error); } } }); ``` ## Security best practices Always validate the `event.origin` before processing any message. This prevents other iframes or windows from spoofing events: ```js window.addEventListener("message", (event) => { // Only accept messages from your Holistics domain if (event.origin !== "https://your-holistics-domain.com") return; // Process event... }); ```