Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/src/guide/client/react-usage-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,83 @@ function renderUI(renderData = null) {
</script>
```

#### 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
<script>
// Alternative approach: 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
async function initializeWithTheme() {
try {
const renderData = await requestRenderData();
renderUI(renderData);
} catch (error) {
console.error('Failed to get render data:', error);
renderUI(); // Fallback to default rendering
}
}

// Initialize when ready
initializeWithTheme();

function renderUI(renderData = null) {
// Same renderUI function as above
const statusEl = document.getElementById('status');

if (renderData) {
// Apply custom CSS
if (renderData.customCss) {
const styleElement = document.createElement('style');
styleElement.textContent = renderData.customCss;
document.head.appendChild(styleElement);
}

// Use other render data
if (statusEl) {
statusEl.innerHTML = `
<strong>✅ Theme Applied!</strong><br>
Theme: ${renderData.theme || 'unknown'}<br>
Additional config: ${JSON.stringify(renderData.additionalConfig || {}, null, 2)}
`;
}

console.log('Render data received:', renderData);
} else {
// Default rendering without theme data
if (statusEl) {
statusEl.innerHTML = '<em>No theme data received - using defaults</em>';
}
}
}
</script>
```

#### Passing Multiple Theme Variables

```tsx
Expand Down
35 changes: 35 additions & 0 deletions docs/src/guide/client/resource-renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 56 additions & 0 deletions docs/src/guide/embeddable-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions sdks/typescript/client/src/components/HTMLResourceRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLIFrameElement>();
render(
<HTMLResourceRenderer
resource={resource}
iframeProps={{ ref }}
iframeRenderData={iframeRenderData}
/>,
);
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<HTMLIFrameElement>();
render(
<HTMLResourceRenderer
resource={resource}
iframeProps={{ ref }}
/>,
);
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
Expand Down