Events and message protocol
The widget sends messages from inside the iframe to the parent page; widget.js turns them into bubbling CustomEvents on the container element. This way you can use the results in your own UI, send analytics or monitor errors.
Events
Section titled “Events”| Event | detail |
When |
|---|---|---|
mo:ready |
{ id, height, tool, title, features } |
The widget has loaded and rendered (once; again if the iframe is reloaded). |
mo:resize |
{ id, height } |
The content height changed. |
mo:input |
{ id, field, inputs } |
The user edited an input (debounced 250 ms). Calculator only. |
mo:result |
{ id, data: { result, breakdown }, meta, inputs, requestId } |
After a successful calculation. |
mo:error |
{ id, error: { code, message, fields? }, requestId } |
When a calculation fails. |
mo:select |
{ id, kind, key, title, url } |
A tool or category was picked in a search/category widget. |
idis the identifier the loader assigned to this widget instance.- In
mo:ready,toolis{ key, version, title }for a calculator widget andnullfor other kinds (and for older embed pages);titleis the iframe’s current title;featuresare the embed page’s capabilities (config,inputs,calculate,reset,input-events). - In
mo:input,inputsare the canonical inputs that would go into the request if the form is valid, otherwisenull.fieldis the name of the field the user edited. - In
mo:result,dataandmetahave the same shape as the calculation response;inputsare the request inputs of the calculation.requestIdis set when the result answers yourcalculate()call, otherwisenull. - In
mo:error,error.fieldsis a[{ field, reason }]list for validation errors.requestIdis set by the same logic. mo:select:kindistoolorcategory,keythe key of the pick,titleits title,urlthe target address. Details: Widget kinds.
const container = document.querySelector('[data-mo-tool="kdv"]');
container.addEventListener('mo:ready', (event) => { console.log('Ready:', event.detail.tool?.title, event.detail.features);});
container.addEventListener('mo:input', (event) => { // event.detail.inputs is set when the form is valid console.log(event.detail.field, event.detail.inputs);});
container.addEventListener('mo:result', (event) => { const { data, meta, inputs } = event.detail; console.log('Gross amount:', data.result.gross, '· tool version', meta.toolVersion, '· inputs', inputs);});
container.addEventListener('mo:error', (event) => { console.warn('Calculation error:', event.detail.error.code, event.detail.error.fields);});Because events bubble, you can also listen to all widgets on the page from one place:
document.addEventListener('mo:result', (event) => { // event.target: the relevant container element analytics.track('calculation', { tool: event.target.dataset.moTool, id: event.detail.id });});For mo:ready and mo:resize, the loader also adjusts the iframe height (between 120–10000 px) and remembers the last measured height. You do not need to manage the height separately.
Error codes
Section titled “Error codes”error.code is either one of the API error codes (VALIDATION_ERROR, RATE_LIMITED, CALCULATION_ERROR, …) or a local code when the API cannot be reached:
| Code | Meaning |
|---|---|
NETWORK_ERROR |
The API could not be reached. |
TIMEOUT |
The request timed out. |
INVALID_RESPONSE |
The response does not match the expected contract. |
calculate() calls can also be rejected on the loader side with the codes NOT_MOUNTED, NOT_READY, UNSUPPORTED, TIMEOUT and UNMOUNTED: JavaScript API.
error.message is the localized text the widget itself shows to the user; build your own UI logic on code.
postMessage protocol (v1)
Section titled “postMessage protocol (v1)”If you use a plain iframe or want to listen yourself without the loader, the embed page sends messages to the parent window in this form (targetOrigin: '*'):
{ source: 'mirac-ozdemir-widget', version: 1, id, type, ...payload }type |
Payload | When |
|---|---|---|
ready |
{ height, tool: { key, version, title }, features: string[] } (kind instead of tool for search/category) |
Once, after hydration. |
resize |
{ height } |
Whenever the document height changes (integer CSS px). |
input |
{ field, inputs } |
After a user edit (debounced 250 ms); inputs is null when the form is not valid. |
result |
{ data: { result, breakdown }, meta, inputs, requestId? } |
After every successful calculation. requestId is present when it answers a calculate command. |
error |
{ error: { code, message, fields? }, requestId? } |
When the calculation request fails. |
select |
{ kind: 'tool' | 'category', key, title, url } |
Search/category pages only. |
idis theidparameter from the address, sent back unchanged ([A-Za-z0-9_-], 1–64). It isnullif the parameter was not given.- The
sourceandversionfields are the immutable parts of the envelope; the payload cannot override them. - The height is always an integer rounded up (the content is never clipped).
- New message types and payload fields may be added within v1; ignore the ones you do not know (versioning policy).
Handling messages safely
Section titled “Handling messages safely”window.addEventListener('message', (event) => { if (event.origin !== 'https://mirac-ozdemir.com') return; // 1) sender origin if (event.source !== iframe.contentWindow) return; // 2) the iframe you expect const m = event.data; if (!m || m.source !== 'mirac-ozdemir-widget' || m.version !== 1) return; // 3) envelope // m.type and its payload can now be used safely});widget.js applies these three checks itself, plus the rule that the id must belong to a known instance: a frame on the page cannot resize or impersonate another widget. The payload of messages is never inserted into the DOM by the loader, only passed on as the event detail. When you insert texts from the payload into your page, use textContent as well.
Commands from your page to the iframe
Section titled “Commands from your page to the iframe”In the opposite direction (your page → embed), messages use this envelope and are posted to the iframe’s window with the site origin as targetOrigin:
{ source: 'mirac-ozdemir-widget-host', version: 1, id, type, ...payload }type |
Payload | Effect |
|---|---|---|
config |
{ theme?, header?, density?, accent?, radius?, view? } |
Presentation only; applied live. |
inputs |
{ values, autorun? } |
Fills the inputs (same sanitising rules as i.<field>). |
calculate |
{ requestId } |
Validates and calculates the form; a result or error message answers with the same requestId. |
reset |
{} |
Resets the form. |
The embed page accepts a command only if event.source === window.parent, the envelope is valid and the id equals its own id (never null); unknown types are ignored. Which commands are supported is in the features field of the ready message (config, inputs, calculate, reset). If you use the loader you do not need to send these by hand: update, setInputs, calculate and reset produce these messages for you (JavaScript API).
Example: showing the result in your own component
Section titled “Example: showing the result in your own component”document.addEventListener('mo:result', (event) => { const { result } = event.detail.data; document.querySelector('#summary').textContent = `Gross: ${result.gross} TRY`;});import { useEffect, useState } from 'react';
export function useWidgetResult() { const [result, setResult] = useState<Record<string, string> | null>(null); useEffect(() => { const handler = (event: Event) => setResult((event as CustomEvent).detail.data.result); document.addEventListener('mo:result', handler); return () => document.removeEventListener('mo:result', handler); }, []); return result;}