Framework integrations
In single-page applications, container elements are created after the page has loaded; so you need to load the loader once and call mount/unmount in every component. The examples below follow the same pattern: load the script (if needed) → mount the container → unmount when the component is removed. To write less code you can use the custom elements or the SDK components in the last three tabs.
import { useEffect, useRef } from 'react';
const LOADER = 'https://api.mirac-ozdemir.com/widget.js';let loading: Promise<void> | null = null;
function loadWidget(): Promise<void> { if (window.MiracOzdemirWidget) return Promise.resolve(); loading ??= new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = LOADER; script.async = true; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load widget.js')); document.head.append(script); }); return loading;}
export function Calculator({ tool, locale = 'tr' }: { tool: string; locale?: 'tr' | 'en' }) { const ref = useRef<HTMLDivElement>(null);
useEffect(() => { const node = ref.current; if (!node) return; let active = true; loadWidget().then(() => active && window.MiracOzdemirWidget?.mount(node)).catch(console.error); return () => { active = false; window.MiracOzdemirWidget?.unmount(node); }; }, [tool, locale]);
return <div ref={ref} data-mo-tool={tool} data-locale={locale} />;}// components/calculator.tsx — App Router'use client';
import { useEffect, useRef } from 'react';
export function Calculator({ tool }: { tool: string }) { const ref = useRef<HTMLDivElement>(null);
useEffect(() => { const node = ref.current; const mount = () => node && window.MiracOzdemirWidget?.mount(node); if (window.MiracOzdemirWidget) { mount(); } else { const script = document.createElement('script'); script.src = 'https://api.mirac-ozdemir.com/widget.js'; script.async = true; script.addEventListener('load', mount); document.head.append(script); } return () => { if (node) window.MiracOzdemirWidget?.unmount(node); }; }, []);
return <div ref={ref} data-mo-tool={tool} />;}Do not use it directly in server components; the widget is client behavior. If you use CSP, add script-src and frame-src (Security).
<script setup lang="ts">import { onBeforeUnmount, onMounted, ref } from 'vue';
const props = defineProps<{ tool: string; locale?: 'tr' | 'en' }>();const host = ref<HTMLElement | null>(null);
onMounted(() => { const mount = () => host.value && window.MiracOzdemirWidget?.mount(host.value); if (window.MiracOzdemirWidget) return mount(); const script = document.createElement('script'); script.src = 'https://api.mirac-ozdemir.com/widget.js'; script.async = true; script.addEventListener('load', mount); document.head.append(script);});
onBeforeUnmount(() => host.value && window.MiracOzdemirWidget?.unmount(host.value));</script>
<template> <div ref="host" :data-mo-tool="props.tool" :data-locale="props.locale ?? 'tr'"></div></template>In Nuxt, use the component inside <ClientOnly>.
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
declare global { interface Window { MiracOzdemirWidget?: { mount(el: Element): unknown; unmount(el: Element): boolean } }}
@Component({ selector: 'app-calculator', standalone: true, template: `<div #host [attr.data-mo-tool]="tool" [attr.data-locale]="locale"></div>`,})export class CalculatorComponent implements AfterViewInit, OnDestroy { @Input() tool = 'kdv'; @Input() locale: 'tr' | 'en' = 'tr'; @ViewChild('host', { static: true }) host!: ElementRef<HTMLElement>;
ngAfterViewInit(): void { const mount = () => window.MiracOzdemirWidget?.mount(this.host.nativeElement); if (window.MiracOzdemirWidget) return void mount(); const script = document.createElement('script'); script.src = 'https://api.mirac-ozdemir.com/widget.js'; script.async = true; script.addEventListener('load', mount); document.head.append(script); }
ngOnDestroy(): void { window.MiracOzdemirWidget?.unmount(this.host.nativeElement); }}<script lang="ts"> import { onDestroy, onMount } from 'svelte';
export let tool = 'kdv'; let host: HTMLElement;
onMount(() => { const mount = () => window.MiracOzdemirWidget?.mount(host); if (window.MiracOzdemirWidget) return mount(); const script = document.createElement('script'); script.src = 'https://api.mirac-ozdemir.com/widget.js'; script.async = true; script.addEventListener('load', mount); document.head.append(script); });
onDestroy(() => host && window.MiracOzdemirWidget?.unmount(host));</script>
<div bind:this={host} data-mo-tool={tool}></div>The loader defines the <mo-calculator>, <mo-search> and <mo-category> elements; the element manages mount/unmount itself (mount when connected, unmount when detached). They can be used directly in Svelte, Angular (CUSTOM_ELEMENTS_SCHEMA) and Vue (compilerOptions.isCustomElement):
<mo-calculator tool="kdv" locale="en" theme="inherit" input-amount="1000" autorun></mo-calculator><script src="https://api.mirac-ozdemir.com/widget.js" async></script>When an attribute changes, most options update live, while others such as tool/locale rebuild the widget. Details: Custom elements and SDK.
import { useRef } from 'react';import { Calculator, useWidget } from '@mirac-ozdemir/widget/react';
export function Vat({ amount }: { amount: string }) { const ref = useRef<HTMLDivElement>(null); const { ready, calculate } = useWidget(ref);
return ( <> <Calculator ref={ref} tool="kdv" locale="en" theme="inherit" inputs={{ amount }} onResult={({ data }) => console.log(data.result)} /> <button disabled={!ready} onClick={() => calculate().then((r) => console.log(r.data.result))}>Calculate</button> </> );}Loading, mount/unmount and live updates are built into the component. The package is not published to npm yet; see the SDK.
<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)" /> <button @click="calc?.calculate()">Calculate</button></template>Content management systems
Section titled “Content management systems”| Platform | How |
|---|---|
| WordPress | Add a Custom HTML block in the block editor and paste the installation code. If the script is filtered, add <script … async> to the theme’s footer.php and leave the container in the post. |
| Webflow / Wix / Squarespace | Paste the container and the script together into an Embed / HTML element. If the script is blocked, use a plain iframe. |
| Google Tag Manager | Place the container on the page; add only <script src="…/widget.js" async></script> to a Custom HTML tag. Even if the tag runs after the page has loaded, the loader scans by itself. |
| Markdown / static site | If it allows HTML, write the container and the script directly. In Astro/Hugo/Jekyll, add the script at the end of the template. |
| Not supported (scripts and iframes do not work in email). Link to the tool page instead. |
Common tips
Section titled “Common tips”- When a component changes
toolorlocale,unmountthe widget andmountit again (the React example above does this with the dependency array), or callupdate(el, { tool, locale }). Options such as theme, accent colour, radius, density, title andviewneed no rebuild:updateapplies them without reloading the iframe. To follow your page’s theme usedata-theme="inherit". - To feed prefilled values from form state use
setInputs(el, values); to get the result on demand useawait calculate(el). - There is no
windowduring server-side rendering (SSR); mounting must be done only on the client. - You can put a fallback link inside the container in SSR; after hydration the widget replaces it with the iframe. In some frameworks, if the framework itself manages this child element, it may give a warning; in that case leave the container empty and put the fallback link next to it.