Angular integration
@broadpaper/angular provides standalone components that mount the same editor React uses. The editor runs outside Angular's zone, so canvas interactions never trigger host change detection.
Angular 21 or later. That is the version the package's Ivy declarations are emitted against, and it is the floor because every Angular below it has reached end of life or is about to: v20's LTS expires in November 2026 and v19's has already gone.
Zone.js and zoneless applications are both fine. The SDK has no opinion about your change detection and does nothing that would force one — which was not always true, and is worth saying plainly because it used to be a reason people had to go zoneless.
pnpm add @broadpaper/core @broadpaper/blocks @broadpaper/renderer @broadpaper/editor @broadpaper/angularThe designer
Every field the template binds to is declared on the class — nothing here refers to a value that does not exist:
import { Component } from "@angular/core";
import { ReportDesignerComponent } from "@broadpaper/angular";
import type { DataSource, Issue, ReportTemplate, Theme } from "@broadpaper/core";
const brand: Theme = {
id: "meridian",
name: "Meridian Wealth",
typography: { headingFont: "Inter, sans-serif", bodyFont: "Inter, sans-serif" },
colors: {
primary: "#1F3A5F",
secondary: "#4A6FA5",
accent: "#E0B04B",
text: "#1A1D21",
mutedText: "#6B7280",
surface: "#FFFFFF",
background: "#F5F6F8",
border: "#E2E5EA"
}
};
const whiteLabel: Theme = { ...brand, id: "hartwell", name: "Hartwell & Kane", colors: { ...brand.colors, primary: "#4A3728" } };
@Component({
selector: "app-template-editor",
standalone: true,
imports: [ReportDesignerComponent],
template: `
<bp-report-designer
[template]="template"
[dataSources]="dataSources"
[sampleData]="sampleData"
[theme]="theme"
[themes]="themes"
[onSave]="save"
(templateChange)="onChange($event)"
(validate)="issues = $event"
style="height: 100vh"
/>
`
})
export class TemplateEditorComponent {
/** The saved document, or undefined for a new one. */
template?: ReportTemplate;
/** The current, unsaved document. */
draft?: ReportTemplate;
issues: Issue[] = [];
theme = brand;
themes = [brand, whiteLabel];
dataSources: DataSource[] = [
{
id: "client",
label: "Client",
schema: {
firstName: { type: "string", label: "First name" },
surname: { type: "string", label: "Surname" },
isRetired: { type: "boolean", label: "Retired" }
}
}
];
sampleData = [
{
id: "example",
label: "Alice Chen",
data: { client: { firstName: "Alice", surname: "Chen", isRetired: false } }
}
];
onChange(t: ReportTemplate): void {
this.draft = t;
}
// An arrow property, not a method: the component passes it straight through
// to the editor, so it must not depend on how it is called.
save = async (t: ReportTemplate): Promise<void> => {
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(`Could not save (${res.status})`);
};
}Inputs mirror ReportDesignerOptions. Outputs are templateChange, selectionChange, validate, modeChange, themeChange and ready — the last emits the imperative API, which is how you drive the editor from a toolbar of your own:
import { Component } from "@angular/core";
import { ReportDesignerComponent } from "@broadpaper/angular";
import type { ReportDesignerApi } from "@broadpaper/editor";
@Component({
selector: "app-designer-with-toolbar",
standalone: true,
imports: [ReportDesignerComponent],
template: `
<button type="button" (click)="api?.setMode('preview')">Preview</button>
<button type="button" (click)="api?.undo()">Undo</button>
<bp-report-designer (ready)="api = $event" style="height: 90vh" />
`
})
export class DesignerWithToolbarComponent {
api?: ReportDesignerApi;
}Preview
template is required; everything else is optional. The layout output fires after each pagination, which is where a page count comes from:
import { Component, Input } from "@angular/core";
import { ReportPreviewComponent } from "@broadpaper/angular";
import type { LayoutResult } from "@broadpaper/renderer";
import type { DataSource, ReportData, ReportTemplate, Theme } from "@broadpaper/core";
@Component({
selector: "app-report-preview",
standalone: true,
imports: [ReportPreviewComponent],
template: `
<p>{{ pages }} page(s)</p>
<bp-report-preview
[template]="template"
[data]="data"
[theme]="theme"
[dataSources]="dataSources"
(layout)="onLayout($event)"
/>
`
})
export class AppReportPreviewComponent {
@Input({ required: true }) template!: ReportTemplate;
@Input() data?: ReportData;
@Input() theme?: Theme;
@Input() dataSources?: DataSource[];
pages = 0;
onLayout(result: LayoutResult): void {
this.pages = result.paged.totalPages;
}
}Custom inspector UIs
A block can hand one of its inspector fields to an Angular component. angularInspector(component, viewContainerRef) returns the InspectorComponent that defineBlock expects; your component receives the field's InspectorApi on an api input and calls api.setValue(...) to write back.
import { Component, Input, ViewContainerRef, inject } from "@angular/core";
import { angularInspector } from "@broadpaper/angular";
import type { InspectorApi, InspectorComponent } from "@broadpaper/core";
@Component({
selector: "app-colour-scale",
standalone: true,
template: `<input type="color" [value]="value" (input)="write($any($event.target).value)" />`
})
export class ColourScaleComponent {
@Input() api?: InspectorApi;
get value(): string {
return typeof this.api?.value === "string" ? this.api.value : "#000000";
}
write(next: string): void {
this.api?.setValue(next);
}
}
/** Call this from a component or service that has a ViewContainerRef. */
export function colourScaleInspector(): InspectorComponent {
return angularInspector(ColourScaleComponent, inject(ViewContainerRef));
}A note on bundling
The editor is built once, in React, behind a framework-neutral mount API. Angular hosts therefore include React inside the editor bundle (about 45 KB gzipped). Nothing React-specific leaks into your application code, templates or the saved JSON. The rendering engine, blocks and PDF output contain no framework at all.
The package itself is a real Angular library: ng-packagr compiles it in partial mode, so what ships is a FESM bundle carrying Ivy declarations your own Angular linker expands at build time. It works in AOT production builds with nothing to configure, and needs no allowedCommonJsDependencies entry of its own.
Licensing
The designer, the preview and the viewer never check a licence: only the PDF does. If your Angular app exports PDFs in the browser, configure the key once in its bootstrap. The browser has no environment variables, so the value comes from an Angular environment file:
import { BroadPaper } from "@broadpaper/forme";
import { environment } from "./environments/environment";
BroadPaper.configure({ license: environment.broadpaperLicense });Without one, PDFs carry the evaluation mark and the console says so once per page load. A free 60-day evaluation key from broadpaper.com/eval removes the mark while you evaluate. It needs BroadPaper 1.0.0 or later. Production use needs a paid licence, configured the same way. A key is a signed certificate rather than a credential, so it is safe in a browser bundle. See Licensing.