Help Instance Help

Frameworks and Templates

<m8ty-chat> is a standard custom element, so it works in every front-end framework and in server-rendered pages. Load the script once, render the element, and — for delegated shares — set the getTicket property on the element (it is a function, so it cannot be passed as an HTML attribute).

The examples assume a delegated share and a ticket endpoint POST /api/m8ty/ticket as described in Delegated Chat. For a public share omit everything related to getTicket.

Tab

For

React, Angular, Vue

Single-page applications built with a bundler

Velocity

Server-rendered Java pages (Apache Velocity templates)

Both integration types are covered: web component and iframe. Use the one enabled on your share.

Web component

For React, Angular and Vue, use this shared helper:

// m8ty-ticket.ts export async function fetchTicket(nonce: string): Promise<{ ticket: string }> { const response = await fetch("/api/m8ty/ticket", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nonce }), }); if (!response.ok) throw new Error("ticket request failed"); return response.json(); }

and add the script tag once to index.html:

<script type="module" src="https://rag.example.com/embed-assets/v1/m8ty-chat.js" crossorigin="anonymous"></script>
import { useEffect, useRef } from "react"; import type { M8tyChat } from "./m8ty-chat"; // copied m8ty-chat.d.ts import { fetchTicket } from "./m8ty-ticket"; declare module "react" { namespace JSX { interface IntrinsicElements { "m8ty-chat": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { share: string; locale?: string; theme?: string; }; } } } export function SupportChat() { const ref = useRef<M8tyChat>(null); useEffect(() => { const chat = ref.current; if (!chat) return; chat.getTicket = fetchTicket; const onError = (event: Event) => console.warn((event as CustomEvent).detail); chat.addEventListener("m8ty-error", onError); return () => chat.removeEventListener("m8ty-error", onError); }, []); return <m8ty-chat ref={ref} share="shr_XXXXXXXXXXXXXXXXXXXXXXXX" locale="de" style={{ display: "block", height: 600 }} />; }
// app.module.ts (or the standalone component): allow custom elements import { CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, AfterViewInit, ViewChild } from "@angular/core"; import type { M8tyChat } from "./m8ty-chat"; import { fetchTicket } from "./m8ty-ticket"; @Component({ selector: "app-support-chat", standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<m8ty-chat #chat share="shr_XXXXXXXXXXXXXXXXXXXXXXXX" locale="de" style="display:block;height:600px"></m8ty-chat>`, }) export class SupportChatComponent implements AfterViewInit { @ViewChild("chat") chat!: ElementRef<M8tyChat>; ngAfterViewInit(): void { this.chat.nativeElement.getTicket = fetchTicket; } }

If you use Angular's HttpClient with XSRF protection, call your ticket endpoint through it instead of fetch so the XSRF header is added.

// vite.config.ts — tell Vue that m8ty-chat is a custom element vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === "m8ty-chat" } } });
<!-- SupportChat.vue --> <script setup lang="ts"> import { onMounted, ref } from "vue"; import type { M8tyChat } from "./m8ty-chat"; import { fetchTicket } from "./m8ty-ticket"; const chat = ref<M8tyChat | null>(null); onMounted(() => { if (chat.value) chat.value.getTicket = fetchTicket; }); </script> <template> <m8ty-chat ref="chat" share="shr_XXXXXXXXXXXXXXXXXXXXXXXX" locale="de" style="display:block;height:600px" /> </template>

Fill the Velocity context in your controller once per request. The template needs no bundler and no separate helper.

import org.apache.velocity.VelocityContext; import org.apache.velocity.tools.generic.EscapeTool; VelocityContext context = new VelocityContext(); context.put("esc", new EscapeTool()); // Velocity Tools: HTML/URL escaping context.put("ragtyUrl", "https://rag.example.com"); // from your configuration context.put("shareId", "shr_XXXXXXXXXXXXXXXXXXXXXXXX"); context.put("locale", "de"); context.put("cspNonce", cspNonce); // only if your CSP uses nonces context.put("csrfToken", csrfToken); // delegated only context.put("currentUser", currentUser); // delegated only, used for #if template.merge(context, writer);
## support-chat.vm #if($currentUser) <meta name="m8ty-csrf" content="$esc.html($csrfToken)"> <script type="module" nonce="$esc.html($cspNonce)" src="$esc.html($ragtyUrl)/embed-assets/v1/m8ty-chat.js" crossorigin="anonymous"></script> <m8ty-chat id="support-chat" share="$esc.html($shareId)" locale="$esc.html($locale)" style="display:block;height:600px"></m8ty-chat> <script type="module" nonce="$esc.html($cspNonce)"> const chat = document.getElementById("support-chat"); const csrf = document.querySelector('meta[name="m8ty-csrf"]').content; chat.getTicket = async (nonce) => { const response = await fetch("/api/m8ty/ticket", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf }, body: JSON.stringify({ nonce }), }); if (!response.ok) throw new Error("ticket request failed"); return response.json(); // { ticket, expires_in } }; </script> #end
  • Output every value through $esc.html(…).

  • $currentUser is used only for the #if — never write a user ID into the page. The user is passed exclusively by your ticket endpoint inside the signed assertion.

  • The JavaScript contains no $ or # sequences, so Velocity leaves it untouched. If you extend it, avoid template literals (${…}) or wrap the block in #[[ … ]]# (unparsed content).

  • Public share: drop #if/#end, the <meta> tag and the second script; keep only the script tag and the element. An iframe variant is shown in Java server-side templates (Velocity).

  • Remove nonce="…" if your CSP does not use nonces.

iframe

Public share: render the <iframe> from Option B: iframe as plain markup in any framework or template — no script and no ticket are needed.

Delegated share: the chat inside the iframe asks your page for a ticket via postMessage. connectIframe (from the same m8ty-chat.js script, also available as window.M8tyEmbed.connectIframe) answers these requests with your getTicket function and returns a function that disconnects it.

import { useEffect, useRef } from "react"; import { fetchTicket } from "./m8ty-ticket"; const FRAME_URL = "https://rag.example.com/embed/shr_XXXXXXXXXXXXXXXXXXXXXXXX?locale=de"; export function SupportChatFrame() { const ref = useRef<HTMLIFrameElement>(null); useEffect(() => { let disconnect: (() => void) | undefined; let cancelled = false; void customElements.whenDefined("m8ty-chat").then(() => { const iframe = ref.current; if (cancelled || !iframe) return; iframe.src = FRAME_URL; // set src and connect in the same step disconnect = window.M8tyEmbed.connectIframe(iframe, { getTicket: fetchTicket }); }); return () => { cancelled = true; disconnect?.(); }; }, []); return ( <iframe ref={ref} title="Chat" allow="clipboard-write" referrerPolicy="strict-origin" style={{ width: "100%", height: 600, border: 0 }} /> ); }
import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from "@angular/core"; import { fetchTicket } from "./m8ty-ticket"; const FRAME_URL = "https://rag.example.com/embed/shr_XXXXXXXXXXXXXXXXXXXXXXXX?locale=de"; @Component({ selector: "app-support-chat-frame", standalone: true, template: `<iframe #frame title="Chat" allow="clipboard-write" referrerpolicy="strict-origin" style="width:100%;height:600px;border:0"></iframe>`, }) export class SupportChatFrameComponent implements AfterViewInit, OnDestroy { @ViewChild("frame") frame!: ElementRef<HTMLIFrameElement>; private disconnect?: () => void; private destroyed = false; async ngAfterViewInit(): Promise<void> { await customElements.whenDefined("m8ty-chat"); if (this.destroyed) return; const iframe = this.frame.nativeElement; iframe.src = FRAME_URL; // set src and connect in the same step this.disconnect = window.M8tyEmbed.connectIframe(iframe, { getTicket: fetchTicket }); } ngOnDestroy(): void { this.destroyed = true; this.disconnect?.(); } }

Setting src in code avoids Angular's resource-URL sanitizing for [src] bindings and loads the frame only once the listener is connected.

<!-- SupportChatFrame.vue --> <script setup lang="ts"> import { onBeforeUnmount, onMounted, ref } from "vue"; import { fetchTicket } from "./m8ty-ticket"; const FRAME_URL = "https://rag.example.com/embed/shr_XXXXXXXXXXXXXXXXXXXXXXXX?locale=de"; const frame = ref<HTMLIFrameElement | null>(null); let disconnect: (() => void) | undefined; let unmounted = false; onMounted(async () => { await customElements.whenDefined("m8ty-chat"); if (unmounted || !frame.value) return; frame.value.src = FRAME_URL; // set src and connect in the same step disconnect = window.M8tyEmbed.connectIframe(frame.value, { getTicket: fetchTicket }); }); onBeforeUnmount(() => { unmounted = true; disconnect?.(); }); </script> <template> <iframe ref="frame" title="Chat" allow="clipboard-write" referrerpolicy="strict-origin" style="width:100%;height:600px;border:0" /> </template>

Context as in the web component tab (esc, ragtyUrl, shareId, locale, cspNonce, csrfToken, currentUser).

## support-chat-frame.vm #if($currentUser) <meta name="m8ty-csrf" content="$esc.html($csrfToken)"> <iframe id="support-chat" data-src="$esc.html($ragtyUrl)/embed/$esc.url($shareId)?locale=$esc.url($locale)" title="Chat" allow="clipboard-write" referrerpolicy="strict-origin" style="width:100%;height:600px;border:0"></iframe> <script type="module" nonce="$esc.html($cspNonce)" src="$esc.html($ragtyUrl)/embed-assets/v1/m8ty-chat.js" crossorigin="anonymous"></script> <script type="module" nonce="$esc.html($cspNonce)"> const frame = document.getElementById("support-chat"); const csrf = document.querySelector('meta[name="m8ty-csrf"]').content; await customElements.whenDefined("m8ty-chat"); frame.src = frame.dataset.src; // set src and connect in the same step window.M8tyEmbed.connectIframe(frame, { getTicket: async (nonce) => { const response = await fetch("/api/m8ty/ticket", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf }, body: JSON.stringify({ nonce }), }); if (!response.ok) throw new Error("ticket request failed"); return response.json(); }, }); </script> #end

The iframe is rendered with data-src and loaded once the listener is connected, so the first ticket request is answered immediately. For a public share use the plain iframe from Java server-side templates (Velocity).

Keep referrerpolicy="strict-origin": the iframe uses it to determine your page origin, and connectIframe answers only that iframe and only towards the Ragty origin.

Single-page applications

  • Mount the chat (element or iframe) only after the user is logged in (delegated) and unmount it on logout. Unmounting aborts running requests; for the iframe call the function returned by connectIframe.

  • Several <m8ty-chat> elements on one page are supported (for example two different shares).

  • Changing the share attribute restarts the chat with the new share.

  • Server-side rendering: render the element as plain HTML; it activates in the browser once the script is loaded. Set getTicket on the client only.

12 November 2026