Skip to content

Custom elements and SDK

There are three levels of using the widget; all rest on the same loader (widget.js 1.1.0):

  1. Markup: <div data-mo-tool="kdv"> (Installation).
  2. Custom elements: <mo-calculator tool="kdv">: framework-free, attribute-driven.
  3. The SDK package: types, the loadWidget loader helper and React/Vue components.

The loader defines three custom elements: <mo-calculator>, <mo-search> and <mo-category>. Options are given as attributes with the same names without the data- prefix; for prefilled inputs use input-<field-in-kebab-case>.

<mo-calculator tool="kdv" locale="en" theme="inherit" accent="0a7d5a" input-amount="1000" autorun></mo-calculator>
<mo-search target="event" category="tax"></mo-search>
<mo-category category="tax" density="compact"></mo-category>
<script src="https://api.mirac-ozdemir.com/widget.js" async></script>
Element Key attribute Extra attributes
<mo-calculator> tool (required) view, autorun, inputs (JSON), input-<field>
<mo-search> (none) target, category
<mo-category> category (required) target

Common to all: locale, theme, header, height, accent, radius, density, title, lazy. Values and defaults are the same as their data-* counterparts (Installation, Widget kinds). An invalid value writes an English console warning as usual.

  • Updated live (the iframe is not reloaded): theme, header, density, radius, accent, view, height, title.
  • Rebuilt: tool, category, locale, target, autorun, inputs, lazy (and category for <mo-search>).
  • input-<field> attributes are read when the element connects to the page; changing them later is not reflected in the widget. To provide values at runtime use setInputs, or change the inputs attribute (JSON); that rebuilds.
const calc = document.querySelector('mo-calculator');
calc.setAttribute('theme', 'dark'); // live
calc.setAttribute('accent', 'e11d48'); // live
calc.setAttribute('locale', 'tr'); // the iframe is reloaded

mo:* events bubble from the element; you can attach a listener to the element or to document:

calc.addEventListener('mo:result', (event) => console.log(event.detail.data.result));

The element manages its internals itself: it replaces its existing content with a container <div data-mo-mounted> that carries the widget, and removes the widget when the element leaves the page. Give this inner container to calls of the JavaScript API such as update, setInputs and calculate:

const inner = calc.querySelector('[data-mo-mounted]');
await window.MiracOzdemirWidget.calculate(inner);
window.MiracOzdemirWidget.setInputs(inner, { amount: '2500' }, { autorun: true });

The event.target of the events is this inner container too.

The SDK includes HTMLElementTagNameMap types for the elements. You only need to import them once:

import '@mirac-ozdemir/widget/elements';
const calc = document.querySelector('mo-calculator'); // typed
// JSX (React): <mo-calculator tool="kdv" input-amount="100" />

React does not subscribe to custom DOM events through props; use the React components (below) or attach the mo:* listeners yourself with a ref.

@mirac-ozdemir/widget is a thin layer over the loader: it has no behaviour of its own; the loader manages the iframe and the postMessage protocol, and the SDK loads the loader once and provides the types.

Terminal window
# after `npm run build` in the SDK repository
npm install ../mirac-ozdemir-widget-sdk
# or a git dependency
npm install git+ssh://[email protected]/<org>/mirac-ozdemir-widget-sdk.git

React (>=18) and Vue (^3.3) are optional peer dependencies; install only the one you use. Entry points: . (loader helpers and types), ./react, ./vue, ./elements (types only).

import { loadWidget } from '@mirac-ozdemir/widget';
const widget = await loadWidget(); // window.MiracOzdemirWidget, typed
const iframe = widget.mount(document.querySelector('#calc')!);
document.querySelector('#calc')!.addEventListener('mo:result', (event) => {
console.log(event.detail.data.result, event.detail.meta.asOf); // typed
});
Option Default Meaning
apiOrigin https://api.mirac-ozdemir.com The origin that serves the loader.
version 'latest' latest loads /widget.js (cached 1 h); '1.1.0' loads the immutable /widget/v1.1.0.js; '1' loads /widget/v1.js.
integrity none SRI value (sha384-…) for the script tag.
timeoutMs 15000 Rejects with an Error after this long.

It resolves immediately if window.MiracOzdemirWidget already exists; concurrent calls share one script (and wait for a <script> for the same address that is already on the page); it appends <script async crossorigin="anonymous">; and it rejects with an Error on a load error, a timeout, or when the global is not defined. A failed load is forgotten, so calling again retries. Without a DOM (SSR) it rejects with a clear error and never touches window.

import { fetchManifest, loadPinnedWidget } from '@mirac-ozdemir/widget';
const widget = await loadPinnedWidget(); // the newest version in the manifest, with its integrity
const manifest = await fetchManifest(); // { latest, protocol, versions: [{ version, url, integrity, features }] }

loadPinnedWidget refuses a manifest whose protocol is not 1. The cost of pinning: the pinned loader does not change, so you get no fixes until you update the SDK or re-read the manifest. Details: Versioning policy.

The main entry point augments HTMLElementEventMap; addEventListener('mo:result', …) and the others come typed. The detail types are exported:

import type { ReadyDetail, InputDetail, ResultDetail, ErrorDetail, SelectDetail, WidgetError, CalculationResult } from '@mirac-ozdemir/widget';
el.addEventListener('mo:error', (event) => {
const { error, requestId } = event.detail; // ErrorDetail
console.warn(error.code, error.fields, requestId);
});

Other types: MiracOzdemirWidgetApi, WidgetOptions, WidgetPatch, WidgetConfig, WidgetInstance, WidgetManifest. calculate() rejects with a WidgetError object ({ code, message, fields? }), not an Error.

import { useRef } from 'react';
import { Calculator, WidgetSearch, WidgetCategory, useWidget } from '@mirac-ozdemir/widget/react';
function Vat() {
const ref = useRef<HTMLDivElement>(null);
const { ready, calculate, setInputs, reset, getResult } = useWidget(ref);
return (
<>
<Calculator
ref={ref}
tool="kdv"
locale="en"
theme="inherit"
accent="0a7d5a"
inputs={{ amount: '1000' }}
autorun
onResult={({ data }) => console.log(data.result)}
onError={({ error }) => console.warn(error.code, error.fields)}
/>
<button disabled={!ready} onClick={() => calculate().then((r) => console.log(r.data))}>Calculate</button>
</>
);
}
<WidgetSearch target="event" category="tax" onSelect={({ kind, key, url }) => router.push(url)} />
<WidgetCategory category="tax" />
  • Props: tool (category for WidgetCategory), locale, theme, header, height, accent, radius, density, view, autorun, inputs, title, lazy, className, style, ref; plus loader (LoadWidgetOptions) and onLoadError. Events: onReady, onInput, onResult, onError on the calculator; onReady, onSelect, onError on search/category.
  • Live props (theme, header, density, radius, accent, view, height, title) call update on the mounted widget; tool, locale, category and target rebuild it. Changed inputs are sent with setInputs.
  • The components render an empty div with data-* attributes and read no window during render, so they are SSR-safe. In the Next.js App Router use them in a client component ('use client').
  • useWidget(ref).ready follows the mo:ready event. calculate() rejects with a WidgetError object; before the widget is mounted it returns NOT_MOUNTED.
  • Without onLoadError, a failed script load is logged with console.error.
<script setup lang="ts">
import { ref } from 'vue';
import { Calculator, type CalculatorExposed } from '@mirac-ozdemir/widget/vue';
const calc = ref<CalculatorExposed | null>(null);
</script>
<template>
<Calculator
ref="calc"
tool="kdv"
locale="en"
:inputs="{ amount: '1000' }"
autorun
@result="(d) => console.log(d.data.result)"
@error="(d) => console.warn(d.error.code)"
/>
<button @click="calc?.calculate()">Calculate</button>
</template>

Calculator, WidgetSearch and WidgetCategory take the same props as the React components (without ref/style; class falls through) and emit ready, resize, input, result, error (calculator), ready, resize, select, error (search/category) and loadError. Calculator exposes calculate, setInputs, reset and getResult. In Nuxt use them inside <ClientOnly>.

The SDK is written against loader 1.1.0 and message protocol 1. Backward-compatible loader releases keep working; a /widget/v2.js with protocol 2 will need a new major version of the SDK. Options a loader does not know are ignored by it.