FDO plugins render their UI inside a sandboxed iframe hosted by the FDO application. Your plugin’s render() method produces the HTML that gets mounted in that iframe.
How render() works
render() must return a synchronous HTML string. FDO serializes this string and passes it through the iframe host pipeline before mounting it. Returning a Promise from render() is not supported and throws an error.
render(): string {
return `<div style="padding: 20px;">Hello from the plugin</div>`;
}
Do not return a Promise from render(). The SDK detects async return values and throws immediately:
"Method 'render' must return a synchronous string. Async render promises are not supported."
renderOnLoad()
renderOnLoad() lets you inject a script that runs after the iframe content is mounted. It must also be synchronous and return a string representation of a function.
renderOnLoad(): string {
return `() => {
console.log("Plugin iframe is ready");
document.getElementById("status").textContent = "Loaded";
}`;
}
The default implementation in FDO_SDK returns a no-op function string '() => {}', so you only need to override this when you have post-mount logic.
Using raw HTML strings
For simple layouts, returning a raw HTML string from render() is perfectly valid:
render(): string {
return `
<div style="padding: 20px; font-family: system-ui, sans-serif;">
<h1>My Plugin</h1>
<p>This content runs inside the FDO sandbox.</p>
<button onclick="handleClick()">Click me</button>
<script>
function handleClick() {
alert("Button clicked!");
}
</script>
</div>
`;
}
Using DOM helper classes
The SDK provides several classes that generate HTML strings programmatically. They handle attribute escaping, style objects, and class composition so you don’t have to write raw HTML by hand.
DOMText
DOMButton
DOMNested
DOMInput
DOMText creates text-level elements: headings, paragraphs, spans, labels, and more.import { DOMText } from "@anikitenko/fdo-sdk";
const text = new DOMText();
text.createHText(1, "Page Title");
// → <h1>Page Title</h1>
text.createPText("A paragraph of text.");
// → <p>A paragraph of text.</p>
text.createStrongText("Bold label");
// → <strong>Bold label</strong>
text.createLabelText("Name:", "name-input", { style: { display: "block" } });
// → <label for="name-input" style="display: block;">Name:</label>
DOMButton creates button elements with click handlers.import { DOMButton } from "@anikitenko/fdo-sdk";
const button = new DOMButton();
button.createButton(
"Save",
() => window.createBackendReq("save", {}),
{
style: {
padding: "8px 16px",
backgroundColor: "#007bff",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}
);
DOMNested creates container elements: div, form, ordered/unordered lists, and more.import { DOMNested } from "@anikitenko/fdo-sdk";
const nested = new DOMNested();
nested.createBlockDiv(["<p>Child content</p>"], {
style: { padding: "16px", backgroundColor: "#f5f5f5" }
});
nested.createList([
nested.createListItem(["First item"]),
nested.createListItem(["Second item"]),
]);
DOMInput creates form inputs, selects, checkboxes, and option elements.import { DOMInput } from "@anikitenko/fdo-sdk";
const input = new DOMInput("username", {
style: { padding: "8px", width: "300px" }
});
input.createInput("text");
// → <input type="text" id="username" name="username" ... />
const themeSelect = new DOMInput("theme", {});
themeSelect.createSelect([
themeSelect.createOption("Light", "light", true),
themeSelect.createOption("Dark", "dark", false),
]);
All DOM helpers support an options parameter with style (an object of CSS properties), classes (additional CSS class names), and customAttributes (arbitrary HTML attributes).
Using host-injected CSS
FDO injects Pure CSS into the iframe runtime. You can use its grid and utility classes without any additional imports.
render(): string {
return `
<div class="pure-g" style="padding: 16px;">
<div class="pure-u-1-2">
<h2>Left Column</h2>
<p>Half-width content using the Pure CSS grid.</p>
</div>
<div class="pure-u-1-2">
<h2>Right Column</h2>
<p>The other half.</p>
</div>
</div>
`;
}
Only use host-injected globals in your render() and renderOnLoad() output. Do not import or require arbitrary npm packages in your UI strings — those run in the iframe runtime where arbitrary imports are not supported. Injected globals include Notyf, hljs, ace, Split, and window.* helpers.
Calling backend handlers from the UI
The FDO host injects window.createBackendReq into the iframe. Use it to send messages from your UI to registered handlers in your plugin backend.
// In your render() output:
render(): string {
return `
<button onclick="sendGreeting()">Greet</button>
<div id="result"></div>
<script>
async function sendGreeting() {
const response = await window.createBackendReq("greet", { name: "World" });
document.getElementById("result").textContent = response.message;
}
</script>
`;
}
The first argument is the handler name you registered in init() with PluginRegistry.registerHandler. The second argument is the data payload.
Waiting for DOM elements
Use window.waitForElement to defer code until a specific element is present in the DOM:
renderOnLoad(): string {
return `() => {
window.waitForElement("#my-widget", (el) => {
el.textContent = "Widget ready!";
});
}`;
}
This is useful for code that depends on elements rendered by inline scripts or async initialization steps.
Complete interactive example
The following example, based on examples/02-interactive-plugin.ts, shows a plugin with registered handlers and a UI that calls them:
import {
FDO_SDK,
FDOInterface,
PluginMetadata,
PluginRegistry,
DOMText,
DOMNested,
DOMButton,
DOMInput,
} from "@anikitenko/fdo-sdk";
export default class InteractivePlugin extends FDO_SDK implements FDOInterface {
private readonly _metadata: PluginMetadata = {
name: "Interactive Plugin Example",
version: "1.0.0",
author: "FDO SDK Team",
description: "Demonstrates interactive UI with buttons, forms, and message handlers",
icon: "widget-button"
};
private counter: number = 0;
get metadata(): PluginMetadata {
return this._metadata;
}
init(): void {
this.log("InteractivePlugin initialized!");
PluginRegistry.registerHandler("incrementCounter", (data: unknown) => {
this.counter++;
return { success: true, counter: this.counter };
});
PluginRegistry.registerHandler("submitForm", async (data: any) => {
const userName = data.userName || "Guest";
return {
success: true,
message: `Welcome, ${userName}!`,
timestamp: new Date().toISOString()
};
});
}
render(): string {
const domText = new DOMText();
const domNested = new DOMNested();
const domButton = new DOMButton();
return domNested.createBlockDiv([
domText.createHText(1, this._metadata.name),
// Counter section
domNested.createBlockDiv([
domText.createHText(3, "Counter"),
domText.createPText(`Current count: ${this.counter}`),
domButton.createButton(
"Increment",
() => window.createBackendReq("incrementCounter", {}),
{ style: { padding: "8px 16px", cursor: "pointer" } }
),
], { style: { padding: "16px", backgroundColor: "#f0f0f0", borderRadius: "4px" } }),
// Form section
`<script>
async function handleFormSubmit() {
const userName = document.getElementById("userName").value;
const result = await window.createBackendReq("submitForm", { userName });
document.getElementById("form-result").textContent = result.message;
}
</script>`,
], {
style: { padding: "20px", fontFamily: "Arial, sans-serif" }
});
}
}
new InteractivePlugin();
Register all handlers in init(), not inside render(). Handlers registered in render() would be re-registered on every render cycle, potentially causing duplicate registrations.