Skip to main content
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 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>
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.