Skip to content

React ​

useRef + useEffect cleanup. Do not reconcile children of the host.

jsx
import { useEffect, useImperativeHandle, useRef, forwardRef } from "react";
import { mountVanillaEditor } from "velo-text";

/**
 * Peer: react. Mounts createEditor on a host element.
 * Do not write innerHTML / children into the host — the reconciler owns the DOM.
 */
export const PortableEditor = forwardRef(function PortableEditor(
  { document, theme = "light-neutral", editable = true, onChange, resolveAssetUrl, getVariableCatalog, getTemplateData, onImageFile },
  ref,
) {
  const host = useRef(null);
  const handle = useRef(null);

  useEffect(() => {
    if (!host.current) return;
    handle.current = mountVanillaEditor(host.current, {
      document, theme, editable, onChange, resolveAssetUrl, getVariableCatalog, getTemplateData, onImageFile,
    });
    return () => {
      handle.current?.destroy();
      handle.current = null;
    };
  }, []);

  useImperativeHandle(ref, () => ({
    getDocument: () => handle.current?.getDocument(),
    commands: () => handle.current?.commands,
    insertVariable: (path, format, fallback) => handle.current?.commands.insertVariable(path, format, fallback),
    destroy: () => handle.current?.destroy(),
  }), []);

  return <div ref={host} className="pde-editor" />;
});
jsx
import { useRef, useState } from "react";
import { createDocument, createIdGenerator, exportDocument } from "velo-text";
import { PortableEditor } from "./PortableEditor.jsx";

const MIME = {
  pdf: "application/pdf",
  odt: "application/vnd.oasis.opendocument.text",
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
};

function sampleDoc() {
  const g = createIdGenerator("react");
  const doc = createDocument({ idGenerator: g, clock: { nowIso: () => new Date().toISOString() } });
  doc.root.children.push({
    type: "paragraph",
    id: g.next(),
    children: [
      { type: "text", id: g.next(), text: "Hello " },
      { type: "variable", id: g.next(), path: "name", source: "{{name}}", valueType: "string" },
    ],
  });
  return doc;
}

export function App() {
  const editorRef = useRef(null);
  const [status, setStatus] = useState("ready");

  async function doExport(format) {
    const document = editorRef.current?.getDocument();
    if (!document) return;
    const chunks = [];
    await exportDocument({
      document,
      data: { name: "Ada" },
      format,
      sink: { write(c) { chunks.push(c); }, close() {} },
      options: { strict: false },
    });
    const a = window.document.createElement("a");
    a.href = URL.createObjectURL(new Blob(chunks, { type: MIME[format] }));
    a.download = `react.${format}`;
    a.click();
    setStatus(`exported ${format}`);
  }

  return (
    <div data-pde-theme="light-neutral">
      <p>{status}</p>
      <button type="button" onClick={() => editorRef.current?.insertVariable("name")}>Insert variable</button>
      <button type="button" onClick={() => doExport("pdf")}>PDF</button>
      <button type="button" onClick={() => doExport("odt")}>ODT</button>
      <button type="button" onClick={() => doExport("docx")}>DOCX</button>
      <PortableEditor
        ref={editorRef}
        document={sampleDoc()}
        theme="light-neutral"
        onChange={() => setStatus("changed")}
      />
    </div>
  );
}

Zero runtime dependencies — TypeScript strict — MIT