<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 getTicketproperty 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.
// 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);
$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 }} />
);
}
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.