diff --git a/docs/src/guide/client/react-usage-examples.md b/docs/src/guide/client/react-usage-examples.md index 83906da4..752b24cd 100644 --- a/docs/src/guide/client/react-usage-examples.md +++ b/docs/src/guide/client/react-usage-examples.md @@ -593,6 +593,83 @@ function renderUI(renderData = null) { ``` +#### Alternative: Using ui-request-render-data + +Instead of relying on the `ui-lifecycle-iframe-ready` lifecycle event, you can explicitly request render data when needed: + +```html + +``` + #### Passing Multiple Theme Variables ```tsx diff --git a/docs/src/guide/client/resource-renderer.md b/docs/src/guide/client/resource-renderer.md index 6d233705..2c1c9956 100644 --- a/docs/src/guide/client/resource-renderer.md +++ b/docs/src/guide/client/resource-renderer.md @@ -291,6 +291,41 @@ if (urlParams.get('waitForRenderData') === 'true') { } ``` +#### Alternative: Explicit Render Data Requests + +Instead of relying on the `ui-lifecycle-iframe-ready` lifecycle event, iframes can explicitly request render data when needed using the `ui-request-render-data` message type: + +```javascript +// In the iframe's script - explicit render data request +async function requestRenderData() { + return new Promise((resolve, reject) => { + const messageId = crypto.randomUUID(); + + window.parent.postMessage( + { type: 'ui-request-render-data', messageId }, + '*' + ); + + function handleMessage(event) { + if (event.data?.type !== 'ui-lifecycle-iframe-render-data') return; + if (event.data.messageId !== messageId) return; + + window.removeEventListener('message', handleMessage); + + const { renderData, error } = event.data.payload; + if (error) return reject(error); + return resolve(renderData); + } + + window.addEventListener('message', handleMessage); + }); +} + +// Use it when your iframe is ready +const renderData = await requestRenderData(); +renderUI(renderData); +``` + ### Automatically Resizing the Iframe The `autoResizeIframe` prop allows you to automatically resize the iframe to the size of the content. diff --git a/docs/src/guide/embeddable-ui.md b/docs/src/guide/embeddable-ui.md index 39c20fcf..b1297378 100644 --- a/docs/src/guide/embeddable-ui.md +++ b/docs/src/guide/embeddable-ui.md @@ -143,6 +143,7 @@ window.parent.postMessage( - [`ui-lifecycle-iframe-ready`](#ui-lifecycle-iframe-ready) - the iframe is ready to receive messages - [`ui-size-change`](#ui-size-change) - the iframe's size has changed and the host should adjust the iframe's size - [`ui-request-data`](#ui-request-data) - the iframe sends a request to the host to request data +- [`ui-request-render-data`](#ui-request-render-data) - the iframe requests render data from the host ### `ui-lifecycle-iframe-ready` @@ -205,6 +206,24 @@ window.parent.postMessage( See also [Asynchronous Data Requests with Message IDs](#asynchronous-data-requests-with-message-ids) +### `ui-request-render-data` + +- a message that the iframe sends to the host to request render data. The message can optionally include a `messageId` to allow the iframe to track the response. +- this message has no payload +- the host responds with a [`ui-lifecycle-iframe-render-data`](#ui-lifecycle-iframe-render-data) message containing the render data + +**Example:** + +```typescript +window.parent.postMessage( + { + type: "ui-request-render-data", + messageId: "render-data-123", // optional + }, + "*" +); +``` + ## Reserved Message Types (host to iframe) - [`ui-lifecycle-iframe-render-data`](#ui-lifecycle-iframe-render-data) - the host sends the iframe render data @@ -302,6 +321,43 @@ if (urlParams.get("waitForRenderData") === "true") { } ``` +### Alternative: Requesting Render Data On-Demand + +Instead of relying on the `ui-lifecycle-iframe-ready` lifecycle event, you can explicitly request render data when needed using `ui-request-render-data`: + +#### In the iframe: + +```typescript +// Request render data when ready +async function requestRenderData() { + return new Promise((resolve, reject) => { + const messageId = crypto.randomUUID(); + + window.parent.postMessage( + { type: "ui-request-render-data", messageId }, + "*" + ); + + function handleMessage(event) { + if (event.data?.type !== "ui-lifecycle-iframe-render-data") return; + if (event.data.messageId !== messageId) return; + + window.removeEventListener("message", handleMessage); + + const { renderData, error } = event.data.payload; + if (error) return reject(error); + return resolve(renderData); + } + + window.addEventListener("message", handleMessage); + }); +} + +// Use it when your iframe is ready +const renderData = await requestRenderData(); +renderUI(renderData); +``` + ## Asynchronous Data Requests with Message IDs Actions initiated from the iframe are handled by the host asynchronously (e.g., data requests, tool calls, etc.). It's useful for the iframe to get feedback on the status of the request and its result. This is achieved using a `messageId` to track the request through its lifecycle. Example use cases include fetching additional information, displaying a progress bar in the iframe, signaling success or failure, and more. diff --git a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx index 01653298..95fa246d 100644 --- a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx @@ -25,6 +25,7 @@ export const InternalMessageType = { UI_LIFECYCLE_IFRAME_READY: 'ui-lifecycle-iframe-ready', UI_LIFECYCLE_IFRAME_RENDER_DATA: 'ui-lifecycle-iframe-render-data', + UI_REQUEST_RENDER_DATA: 'ui-request-render-data', } as const; export const ReservedUrlParams = { @@ -111,6 +112,20 @@ export const HTMLResourceRenderer = ({ return; } + // if the iframe requests render data, send it + if (data?.type === InternalMessageType.UI_REQUEST_RENDER_DATA) { + postToFrame( + InternalMessageType.UI_LIFECYCLE_IFRAME_RENDER_DATA, + source, + origin, + data.messageId, + { + renderData: initialRenderData, + }, + ); + return; + } + if (data?.type === InternalMessageType.UI_SIZE_CHANGE) { const { width, height } = data.payload as { width?: number; height?: number }; if (autoResizeIframe && iframeRef.current) { diff --git a/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx b/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx index 38f99ba5..db32bd5c 100644 --- a/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx +++ b/sdks/typescript/client/src/components/__tests__/HTMLResourceRenderer.test.tsx @@ -533,6 +533,72 @@ describe('HTMLResource metadata', () => { '*', ); }); + + it('should respond to ui-request-render-data with render data', () => { + const iframeRenderData = { theme: 'dark', user: { id: '123' } }; + const resource = { + mimeType: 'text/uri-list', + text: 'https://example.com/app', + }; + const ref = React.createRef(); + render( + , + ); + expect(ref.current).toBeInTheDocument(); + const iframeWindow = ref.current?.contentWindow as Window; + const spy = vi.spyOn(iframeWindow, 'postMessage'); + + const messageId = 'test-message-id'; + dispatchMessage(iframeWindow, { + type: InternalMessageType.UI_REQUEST_RENDER_DATA, + messageId, + }); + + expect(spy).toHaveBeenCalledWith( + { + type: InternalMessageType.UI_LIFECYCLE_IFRAME_RENDER_DATA, + payload: { renderData: iframeRenderData }, + messageId, + }, + '*', + ); + }); + + it('should respond to ui-request-render-data with undefined when no render data', () => { + const resource = { + mimeType: 'text/uri-list', + text: 'https://example.com/app', + }; + const ref = React.createRef(); + render( + , + ); + expect(ref.current).toBeInTheDocument(); + const iframeWindow = ref.current?.contentWindow as Window; + const spy = vi.spyOn(iframeWindow, 'postMessage'); + + const messageId = 'test-message-id-2'; + dispatchMessage(iframeWindow, { + type: InternalMessageType.UI_REQUEST_RENDER_DATA, + messageId, + }); + + expect(spy).toHaveBeenCalledWith( + { + type: InternalMessageType.UI_LIFECYCLE_IFRAME_RENDER_DATA, + payload: { renderData: undefined }, + messageId, + }, + '*', + ); + }); }); // Helper to dispatch a message event