Skip to content

React integration

@broadpaper/react wraps the framework-neutral editor and preview. Props mirror ReportDesignerOptions; the ref exposes the imperative API.

<ReportDesigner />

Complete, and it compiles — the Quick start defines dataSources, sampleData and brand in the files this imports:

tsx
import { useRef, useState } from "react";
import { ReportDesigner, type ReportDesignerInstance } from "@broadpaper/react";
import type { Issue, ReportTemplate } from "@broadpaper/core";
import { dataSources } from "./data-sources";
import { sampleData } from "./sample-data";
import { brand, whiteLabel } from "./themes";

export function Editor({ initialTemplate }: { initialTemplate?: ReportTemplate }) {
  const ref = useRef<ReportDesignerInstance>(null);
  const [template, setTemplate] = useState<ReportTemplate | undefined>(initialTemplate);
  const [issues, setIssues] = useState<Issue[]>([]);

  return (
    <div style={{ height: "100vh" }}>
      <button type="button" onClick={() => ref.current?.setMode("preview")}>
        Preview ({issues.length} checks)
      </button>
      <ReportDesigner
        ref={ref}
        template={template}
        dataSources={dataSources}
        sampleData={sampleData}
        theme={brand}
        themes={[brand, whiteLabel]}
        features={{ pdfExport: true, whiteLabel: true }}
        saveDescription="Saved to your workspace."
        onChange={setTemplate}
        onSave={async (t) => {
          const res = await fetch("/api/templates/investment-review", {
            method: "PUT",
            headers: { "content-type": "application/json" },
            body: JSON.stringify(t)
          });
          if (!res.ok) throw new Error(`Save failed (${res.status})`);
        }}
        onValidate={setIssues}
      />
    </div>
  );
}

The template prop is treated as the initial document plus a "load this" signal: passing a different object than the one the editor last emitted reloads it, so you can implement "open template" without remounting. Passing back the object you received from onChange does nothing, which is what makes the controlled-looking pattern above safe.

The ref exposes the full imperative APIgetTemplate(), setTemplate(), setTheme(), setMode(), undo(), redo(), validate(), exportPdf(), print(), getLayout(), registerBlock() — from the first render after mount.

<ReportPreview />

Read-only and paginated, through the same pipeline as the PDF. onLayout fires after each pagination:

tsx
import { useState } from "react";
import { ReportPreview } from "@broadpaper/react";
import type { ReportTemplate } from "@broadpaper/core";
import { dataSources } from "./data-sources";
import { exampleData } from "./sample-data";
import { brand } from "./themes";

export function PreviewPane({ template }: { template: ReportTemplate }) {
  const [pages, setPages] = useState(0);
  return (
    <>
      <p>{pages} page(s)</p>
      <ReportPreview
        template={template}
        data={exampleData}
        theme={brand}
        dataSources={dataSources}
        zoom={0.8}
        onLayout={(r) => setPages(r.paged.totalPages)}
      />
    </>
  );
}

Pass mode="design" to show binding tokens instead of values.

Custom inspector UIs

Blocks describe their inspector declaratively (see Custom blocks). When you need a bespoke control, wrap a React component with reactInspector, which receives the field's value and a setter:

tsx
import { reactInspector } from "@broadpaper/react";
import type { InspectorComponent } from "@broadpaper/core";

export const colourScale: InspectorComponent = reactInspector(({ value, setValue }) => (
  <input type="color" value={typeof value === "string" ? value : "#000000"} onChange={(e) => setValue(e.target.value)} />
));

Then reference it from the block's inspector:

ts
{ kind: "custom", prop: "scale", label: "Scale", component: colourScale }

Sizing

The editor fills its container; give the wrapper a height. It is a CSS grid with three columns (blocks, canvas, inspector) and is tested down to 1120 px wide. Panels can be collapsed from the toolbar, which is what makes narrower embeddings workable.