Skip to content

[iFrame] Add the srcdoc tip calculator example to the iframe catalog - #3008

Draft
josemontespg wants to merge 1 commit into
catalogs-iframe-componentsfrom
catalogs-example-srcdoc-tip-calculator
Draft

josemontespg wants to merge 1 commit into
catalogs-iframe-componentsfrom
catalogs-example-srcdoc-tip-calculator

Conversation

@josemontespg

@josemontespg josemontespg commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds srcdoc-tip-calculator.json (45_srcdoc-tip-calculator) to the iframe catalog examples and wires @a2ui/catalog-iframe into the Lit, Angular, and React explorer apps: a WebAppFrameSrcdoc app that reads the bill amount and tip percentage typed into two sibling TextFields, computes the total, formats it through the host formatCurrency function, and writes both back to the data model where a sibling Text shows the label. Its "Save receipt" button dispatches a receipt_saved action.

Changes

  • catalogs/iframe/examples/srcdoc-tip-calculator.json: the example, with the inline app written as documentation.
  • typescript/catalogs/iframe/tests/examples/srcdoc_tip_calculator.test.ts, catalog.test.ts, README.md, CHANGELOG.md: iframe catalog example registration and Karma end-to-end test.
  • renderers/{lit,angular,react}/a2ui_explorer: registers @a2ui/catalog-iframe (v1.0 IFRAME_CATALOG_ID), adds 45_srcdoc-tip-calculator to the example gallery, and adds Karma integration tests (45_srcdoc-tip-calculator.{spec.ts,test.tsx}).

Verification

  • yarn --cwd typescript/catalogs/iframe test: Karma specs pass.
  • yarn --cwd renderers/{lit,react}/a2ui_explorer test:karma: pass.
  • python3 specification/scripts/validate.py and ./scripts/fix_format.sh --check pass.

Related to #3157

@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from e7b42bf to a638cb2 Compare October 5, 2026 20:45
@josemontespg
josemontespg added this pull request to stack #3011 October 5, 2026 20:45

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new WebAppFrameSrcdoc tip calculator example along with corresponding unit and end-to-end tests to verify its behavior through the sandbox proxy. Feedback on the changes highlights a potential asynchronous race condition in the tip calculator's script when rapidly recalculating values, which could lead to stale formatted totals overwriting newer ones. A code suggestion is provided to track the active call ID and ensure only the latest result is applied.

"additionalProperties": false
}
},
"htmlContent": "<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <style>\n body { font-family: sans-serif; margin: 16px; }\n td { padding: 4px 16px 4px 0; }\n td:last-child { text-align: right; }\n .total td { border-top: 1px solid #999; font-weight: bold; }\n button { margin-top: 12px; }\n </style>\n </head>\n <body>\n <table>\n <tr><td>Bill</td><td id=\"amount\"></td></tr>\n <tr><td>Tip (<span id=\"percent\"></span>%)</td><td id=\"tip\"></td></tr>\n <tr class=\"total\"><td>Total</td><td id=\"total\">Waiting for the host...</td></tr>\n </table>\n <button id=\"save\" disabled>Save receipt</button>\n <script>\n var port = null;\n var bill = { amount: 0, tipPercent: 0, total: 0 };\n var nextCallId = 1;\n\n function toNumber(value) {\n var number = Number(value);\n return isNaN(number) ? 0 : number;\n }\n\n function cents(value) {\n return Math.round(value * 100) / 100;\n }\n\n function recalculate() {\n var tip = cents(bill.amount * bill.tipPercent / 100);\n bill.total = cents(bill.amount + tip);\n document.getElementById('amount').textContent = bill.amount.toFixed(2);\n document.getElementById('percent').textContent = bill.tipPercent;\n document.getElementById('tip').textContent = tip.toFixed(2);\n // \"total\" is listed in mutableData, so the host writes it back to /bill/total.\n port.postMessage({ type: 'a2ui_data_model_change', key: 'total', value: bill.total });\n // formatCurrency is listed in allowedFunctions, so the host evaluates it with the\n // surface catalog and answers with a2ui_function_result.\n port.postMessage({\n type: 'a2ui_function_call',\n call: 'formatCurrency',\n callId: 'total-' + nextCallId++,\n args: { value: bill.total, currency: 'USD', decimals: 2 }\n });\n port.postMessage({ type: 'a2ui_size_changed', height: document.body.scrollHeight + 32 });\n }\n\n function onPortMessage(event) {\n var msg = event.data || {};\n if (msg.type === 'a2ui_data_model_update' && (msg.key === 'amount' || msg.key === 'tipPercent')) {\n bill[msg.key] = toNumber(msg.value);\n recalculate();\n } else if (msg.type === 'a2ui_function_result' && msg.status === 'success') {\n document.getElementById('total').textContent = msg.result;\n // \"totalLabel\" is mutable too, so the sibling Text component shows the same string.\n port.postMessage({ type: 'a2ui_data_model_change', key: 'totalLabel', value: msg.result });\n }\n }\n\n // Only the handshake travels over the ambient window channel. Everything after it uses\n // the MessagePort transferred with a2ui_app_frame_init.\n window.addEventListener('message', function (event) {\n if (event.source !== window.parent) return;\n var msg = event.data || {};\n if (msg.type !== 'a2ui_app_frame_init' || !event.ports || !event.ports[0]) return;\n port = event.ports[0];\n port.onmessage = onPortMessage;\n var initial = (msg.value && msg.value.initialData) || {};\n bill.amount = toNumber(initial.amount);\n bill.tipPercent = toNumber(initial.tipPercent);\n document.getElementById('save').disabled = false;\n recalculate();\n });\n\n document.getElementById('save').addEventListener('click', function () {\n // \"receipt_saved\" is listed in allowedEvents, so the host forwards it to the agent.\n port.postMessage({ type: 'a2ui_action', action: 'receipt_saved', data: bill });\n });\n\n window.parent.postMessage({ type: 'a2ui_app_frame_ready' }, '*');\n </script>\n </body>\n</html>\n"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a potential asynchronous race condition in the tip calculator's script. When the user rapidly changes the bill amount or tip percentage, multiple recalculate() calls are triggered, each sending an asynchronous formatCurrency function call to the host. Because these responses can arrive out of order, a stale formatted total might overwrite a newer one, leading to incorrect UI states.

To prevent this, we should track the activeCallId of the latest request and only apply the result if the incoming msg.callId matches the active one.

Suggested change
"htmlContent": "<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <style>\n body { font-family: sans-serif; margin: 16px; }\n td { padding: 4px 16px 4px 0; }\n td:last-child { text-align: right; }\n .total td { border-top: 1px solid #999; font-weight: bold; }\n button { margin-top: 12px; }\n </style>\n </head>\n <body>\n <table>\n <tr><td>Bill</td><td id=\"amount\"></td></tr>\n <tr><td>Tip (<span id=\"percent\"></span>%)</td><td id=\"tip\"></td></tr>\n <tr class=\"total\"><td>Total</td><td id=\"total\">Waiting for the host...</td></tr>\n </table>\n <button id=\"save\" disabled>Save receipt</button>\n <script>\n var port = null;\n var bill = { amount: 0, tipPercent: 0, total: 0 };\n var nextCallId = 1;\n\n function toNumber(value) {\n var number = Number(value);\n return isNaN(number) ? 0 : number;\n }\n\n function cents(value) {\n return Math.round(value * 100) / 100;\n }\n\n function recalculate() {\n var tip = cents(bill.amount * bill.tipPercent / 100);\n bill.total = cents(bill.amount + tip);\n document.getElementById('amount').textContent = bill.amount.toFixed(2);\n document.getElementById('percent').textContent = bill.tipPercent;\n document.getElementById('tip').textContent = tip.toFixed(2);\n // \"total\" is listed in mutableData, so the host writes it back to /bill/total.\n port.postMessage({ type: 'a2ui_data_model_change', key: 'total', value: bill.total });\n // formatCurrency is listed in allowedFunctions, so the host evaluates it with the\n // surface catalog and answers with a2ui_function_result.\n port.postMessage({\n type: 'a2ui_function_call',\n call: 'formatCurrency',\n callId: 'total-' + nextCallId++,\n args: { value: bill.total, currency: 'USD', decimals: 2 }\n });\n port.postMessage({ type: 'a2ui_size_changed', height: document.body.scrollHeight + 32 });\n }\n\n function onPortMessage(event) {\n var msg = event.data || {};\n if (msg.type === 'a2ui_data_model_update' && (msg.key === 'amount' || msg.key === 'tipPercent')) {\n bill[msg.key] = toNumber(msg.value);\n recalculate();\n } else if (msg.type === 'a2ui_function_result' && msg.status === 'success') {\n document.getElementById('total').textContent = msg.result;\n // \"totalLabel\" is mutable too, so the sibling Text component shows the same string.\n port.postMessage({ type: 'a2ui_data_model_change', key: 'totalLabel', value: msg.result });\n }\n }\n\n // Only the handshake travels over the ambient window channel. Everything after it uses\n // the MessagePort transferred with a2ui_app_frame_init.\n window.addEventListener('message', function (event) {\n if (event.source !== window.parent) return;\n var msg = event.data || {};\n if (msg.type !== 'a2ui_app_frame_init' || !event.ports || !event.ports[0]) return;\n port = event.ports[0];\n port.onmessage = onPortMessage;\n var initial = (msg.value && msg.value.initialData) || {};\n bill.amount = toNumber(initial.amount);\n bill.tipPercent = toNumber(initial.tipPercent);\n document.getElementById('save').disabled = false;\n recalculate();\n });\n\n document.getElementById('save').addEventListener('click', function () {\n // \"receipt_saved\" is listed in allowedEvents, so the host forwards it to the agent.\n port.postMessage({ type: 'a2ui_action', action: 'receipt_saved', data: bill });\n });\n\n window.parent.postMessage({ type: 'a2ui_app_frame_ready' }, '*');\n </script>\n </body>\n</html>\n"
"htmlContent": "\u003c!doctype html\u003e\n\u003chtml\u003e\n \u003chead\u003e\n \u003cmeta charset=\"utf-8\" /\u003e\n \u003cstyle\u003e\n body { font-family: sans-serif; margin: 16px; }\n td { padding: 4px 16px 4px 0; }\n td:last-child { text-align: right; }\n .total td { border-top: 1px solid #999; font-weight: bold; }\n button { margin-top: 12px; }\n \u003c/style\u003e\n \u003c/head\u003e\n \u003cbody\u003e\n \u003ctable\u003e\n \u003ctr\u003e\u003ctd\u003eBill\u003c/td\u003e\u003ctd id=\"amount\"\u003e\u003c/td\u003e\u003c/tr\u003e\n \u003ctr\u003e\u003ctd\u003eTip (\u003cspan id=\"percent\"\u003e\u003c/span\u003e%)\u003c/td\u003e\u003ctd id=\"tip\"\u003e\u003c/td\u003e\u003c/tr\u003e\n \u003ctr class=\"total\"\u003e\u003ctd\u003eTotal\u003c/td\u003e\u003ctd id=\"total\"\u003eWaiting for the host...\u003c/td\u003e\u003c/tr\u003e\n \u003c/table\u003e\n \u003cbutton id=\"save\" disabled\u003eSave receipt\u003c/button\u003e\n \u003cscript\u003e\n var port = null;\n var bill = { amount: 0, tipPercent: 0, total: 0 };\n var nextCallId = 1;\n var activeCallId = null;\n\n function toNumber(value) {\n var number = Number(value);\n return isNaN(number) ? 0 : number;\n }\n\n function cents(value) {\n return Math.round(value * 100) / 100;\n }\n\n function recalculate() {\n var tip = cents(bill.amount * bill.tipPercent / 100);\n bill.total = cents(bill.amount + tip);\n document.getElementById('amount').textContent = bill.amount.toFixed(2);\n document.getElementById('percent').textContent = bill.tipPercent;\n document.getElementById('tip').textContent = tip.toFixed(2);\n // \"total\" is listed in mutableData, so the host writes it back to /bill/total.\n port.postMessage({ type: 'a2ui_data_model_change', key: 'total', value: bill.total });\n // formatCurrency is listed in allowedFunctions, so the host evaluates it with the\n // surface catalog and answers with a2ui_function_result.\n activeCallId = 'total-' + nextCallId++;\n port.postMessage({\n type: 'a2ui_function_call',\n call: 'formatCurrency',\n callId: activeCallId,\n args: { value: bill.total, currency: 'USD', decimals: 2 }\n });\n port.postMessage({ type: 'a2ui_size_changed', height: document.body.scrollHeight + 32 });\n }\n\n function onPortMessage(event) {\n var msg = event.data || {};\n if (msg.type === 'a2ui_data_model_update' \u0026\u0026 (msg.key === 'amount' || msg.key === 'tipPercent')) {\n bill[msg.key] = toNumber(msg.value);\n recalculate();\n } else if (msg.type === 'a2ui_function_result' \u0026\u0026 msg.status === 'success' \u0026\u0026 msg.callId === activeCallId) {\n document.getElementById('total').textContent = msg.result;\n // \"totalLabel\" is mutable too, so the sibling Text component shows the same string.\n port.postMessage({ type: 'a2ui_data_model_change', key: 'totalLabel', value: msg.result });\n }\n }\n\n // Only the handshake travels over the ambient window channel. Everything after it uses\n // the MessagePort transferred with a2ui_app_frame_init.\n window.addEventListener('message', function (event) {\n if (event.source !== window.parent) return;\n var msg = event.data || {};\n if (msg.type !== 'a2ui_app_frame_init' || !event.ports || !event.ports[0]) return;\n port = event.ports[0];\n port.onmessage = onPortMessage;\n var initial = (msg.value \u0026\u0026 msg.value.initialData) || {};\n bill.amount = toNumber(initial.amount);\n bill.tipPercent = toNumber(initial.tipPercent);\n document.getElementById('save').disabled = false;\n recalculate();\n });\n\n document.getElementById('save').addEventListener('click', function () {\n // \"receipt_saved\" is listed in allowedEvents, so the host forwards it to the agent.\n port.postMessage({ type: 'a2ui_action', action: 'receipt_saved', data: bill });\n });\n\n window.parent.postMessage({ type: 'a2ui_app_frame_ready' }, '*');\n \u003c/script\u003e\n \u003c/body\u003e\n\u003c/html\u003e\n"

@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch 3 times, most recently from 1fc1d74 to ff00e67 Compare October 6, 2026 00:32
@josemontespg
josemontespg removed this pull request from stack #3011 October 7, 2026 23:18
@josemontespg
josemontespg changed the base branch from catalogs-docs-guide to main October 7, 2026 23:19
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from ff00e67 to 7091e22 Compare October 7, 2026 23:19
@josemontespg
josemontespg changed the base branch from main to catalogs-iframe-components October 7, 2026 23:19
@josemontespg
josemontespg added this pull request to stack #3055 October 7, 2026 23:19
@josemontespg
josemontespg removed this pull request from stack #3055 October 7, 2026 23:32
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 7091e22 to 4deb6b6 Compare October 7, 2026 23:33
@josemontespg josemontespg changed the title feat(catalogs): add the srcdoc tip calculator example to the iframe catalog [iFrame] feat(catalogs): add the srcdoc tip calculator example to the iframe catalog Oct 7, 2026
@josemontespg
josemontespg added this pull request to stack #3057 October 7, 2026 23:34
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 4deb6b6 to 610b72b Compare October 8, 2026 19:25
@josemontespg josemontespg changed the title [iFrame] feat(catalogs): add the srcdoc tip calculator example to the iframe catalog [iFrame] Add the srcdoc tip calculator example to the iframe catalog Oct 8, 2026
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 610b72b to 633e651 Compare October 8, 2026 20:22
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 633e651 to d60512b Compare October 8, 2026 20:31
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from d60512b to f941ffb Compare October 8, 2026 20:47
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch 2 times, most recently from 31b2fff to 70d7cae Compare October 8, 2026 21:34
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 5bab9e6 to 889ca3f Compare October 9, 2026 19:09
@josemontespg
josemontespg removed this pull request from stack #3114 October 9, 2026 19:12
@josemontespg
josemontespg added this pull request to stack #3115 October 9, 2026 19:12
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 889ca3f to aaa240b Compare October 9, 2026 20:23
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from aaa240b to 09cd5d3 Compare October 9, 2026 22:15
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 09cd5d3 to c0e9ac2 Compare October 9, 2026 22:37
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from c0e9ac2 to 3db162a Compare October 9, 2026 22:43
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 3db162a to 586a90a Compare October 9, 2026 22:54
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch 2 times, most recently from 57f28eb to cb52000 Compare October 12, 2026 18:57
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from cb52000 to 3e8e371 Compare October 12, 2026 21:29
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 3e8e371 to 3cf5b38 Compare October 12, 2026 23:08
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch 2 times, most recently from ab0b508 to 3644913 Compare October 13, 2026 19:48
@josemontespg
josemontespg removed this pull request from stack #3115 October 13, 2026 19:51
@josemontespg
josemontespg added this pull request to stack #3174 October 13, 2026 19:51
@josemontespg
josemontespg force-pushed the catalogs-example-srcdoc-tip-calculator branch from 3644913 to f9c31ab Compare October 13, 2026 19:58
…atalog

A WebAppFrameSrcdoc app that reads the bill from bound data, writes the
total back through mutableData, formats it with the basic catalog's
formatCurrency through allowedFunctions and reports the saved receipt as
an action. tests/examples/srcdoc_tip_calculator.test.ts runs the example
through the real sandbox proxy and drives it through the host data model.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant