Custom elements and SDK
There are three levels of using the widget; all rest on the same loader (widget.js 1.1.0):
- Markup:
<div data-mo-tool="kdv">(Installation). - Custom elements:
<mo-calculator tool="kdv">: framework-free, attribute-driven. - The SDK package: types, the
loadWidgetloader helper and React/Vue components.
Custom elements
Section titled “Custom elements”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.
What happens when an attribute changes
Section titled “What happens when an attribute changes”- Updated live (the iframe is not reloaded):
theme,header,density,radius,accent,view,height,title. - Rebuilt:
tool,category,locale,target,autorun,inputs,lazy(andcategoryfor<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 usesetInputs, or change theinputsattribute (JSON); that rebuilds.
const calc = document.querySelector('mo-calculator');calc.setAttribute('theme', 'dark'); // livecalc.setAttribute('accent', 'e11d48'); // livecalc.setAttribute('locale', 'tr'); // the iframe is reloadedEvents and programmatic calls
Section titled “Events and programmatic calls”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.
TypeScript types
Section titled “TypeScript types”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.
# after `npm run build` in the SDK repositorynpm install ../mirac-ozdemir-widget-sdk# or a git dependencyReact (>=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).
loadWidget
Section titled “loadWidget”import { loadWidget } from '@mirac-ozdemir/widget';
const widget = await loadWidget(); // window.MiracOzdemirWidget, typedconst 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.
Manifest and SRI
Section titled “Manifest and SRI”import { fetchManifest, loadPinnedWidget } from '@mirac-ozdemir/widget';
const widget = await loadPinnedWidget(); // the newest version in the manifest, with its integrityconst 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.
Typed events
Section titled “Typed events”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(categoryforWidgetCategory),locale,theme,header,height,accent,radius,density,view,autorun,inputs,title,lazy,className,style,ref; plusloader(LoadWidgetOptions) andonLoadError. Events:onReady,onInput,onResult,onErroron the calculator;onReady,onSelect,onErroron search/category. - Live props (
theme,header,density,radius,accent,view,height,title) callupdateon the mounted widget;tool,locale,categoryandtargetrebuild it. Changedinputsare sent withsetInputs. - The components render an empty
divwithdata-*attributes and read nowindowduring render, so they are SSR-safe. In the Next.js App Router use them in a client component ('use client'). useWidget(ref).readyfollows themo:readyevent.calculate()rejects with aWidgetErrorobject; before the widget is mounted it returnsNOT_MOUNTED.- Without
onLoadError, a failed script load is logged withconsole.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>.
Compatibility
Section titled “Compatibility”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.