Skip to content

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.

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.
  • id is the identifier the loader assigned to this widget instance.
  • In mo:ready, tool is { key, version, title } for a calculator widget and null for other kinds (and for older embed pages); title is the iframe’s current title; features are the embed page’s capabilities (config, inputs, calculate, reset, input-events).
  • In mo:input, inputs are the canonical inputs that would go into the request if the form is valid, otherwise null. field is the name of the field the user edited.
  • In mo:result, data and meta have the same shape as the calculation response; inputs are the request inputs of the calculation. requestId is set when the result answers your calculate() call, otherwise null.
  • In mo:error, error.fields is a [{ field, reason }] list for validation errors. requestId is set by the same logic.
  • mo:select: kind is tool or category, key the key of the pick, title its title, url the 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.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.

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.
  • id is the id parameter from the address, sent back unchanged ([A-Za-z0-9_-], 1–64). It is null if the parameter was not given.
  • The source and version fields 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).
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.

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`;
});