Templates and gallery
The designer edits a template in memory. Your application owns storage: loading, saving, permissions and versioning.
Load → edit → save#
import type { ReportDesigner, Template, TemplateChangeDetail } from '@rd/designer';
const designer = document.querySelector('report-designer') as ReportDesigner;
let dirty = false;
// 1. Load. This does not fire template-change, and it resets undo history.
designer.template = await loadTemplate('bol');
// 2. Track edits (debounced 300 ms after the last change).
designer.addEventListener('template-change', () => {
dirty = true;
});
// 3. Save when the user asks (Ctrl/Cmd+S or the Save button)...
designer.addEventListener('save', async (e) => {
await saveTemplate('bol', (e as CustomEvent<TemplateChangeDetail>).detail.template);
dirty = false;
});
// ...and warn before leaving with unsaved changes.
window.addEventListener('beforeunload', (e) => {
if (dirty) {
e.preventDefault();
}
});
// Your own Save button outside the designer:
document.getElementById('save-button')!.addEventListener('click', async () => {
const current: Template = designer.getTemplate();
await saveTemplate('bol', current);
dirty = false;
});
template-change vs save.
template-changereports every edit (debounced). Use it for dirty flags, autosave drafts or live previews elsewhere in your UI.savemeans the user wants to save now. Use it for the real save. A finaltemplate-changeis dispatched immediately before everysave, so a listener that only watchestemplate-changenever misses the last edit.- Loading a template (the property,
setTemplate()or thetemplateoption ofmountDesigner) fires neither event.
Two-way binding is safe. If you feed every template-change back into the template property (as framework bindings do), the designer sees a template
equal to its current design and ignores it, so the cursor, selection and undo history are kept.
Storing templates#
A template is a self-contained JSON document, typically 10–200 KB. It grows beyond that with embedded images or uploaded fonts (stored as base64).
CREATE TABLE report_template (
id BIGINT PRIMARY KEY,
company_code VARCHAR(32) NOT NULL, -- tenant, if you are multi-tenant
doc_type VARCHAR(64) NOT NULL, -- e.g. BILL_OF_LADING, PICK_LIST
name VARCHAR(200) NOT NULL,
version INT NOT NULL,
status VARCHAR(16) NOT NULL, -- DRAFT, PUBLISHED, RETIRED
template_json JSON NOT NULL, -- or LONGTEXT / CLOB
created_by VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL,
UNIQUE (company_code, doc_type, name, version)
);
Recommendations:
- Validate on save with
validateTemplate()from@rd/core(Node), orPOST /api/v1/validateon the render service, and reject templates with errors. - Size limit. Reject templates over a few MB. Large ones usually carry huge embedded images. The render service's request limit is
RD_BODY_LIMIT_MB(default 20 MB). - Don't store data in templates. Preview text is sample data typed by authors. Remind them not to paste real customer data into it.
Versioning#
- Immutable versions. Save every publish as a new row (
version + 1), and render with a specific published version. Old shipments can then be reprinted exactly as they were. - Draft vs published. Autosave drafts from
template-change, and publish onsaveor with an explicit "Publish" action in your UI. - Contract versions. Store the data-contract (schema) version with each template version. When you rename a data field, either migrate the bindings, or keep sending the old field until all published templates are updated.
- Template format version. Every template has
"version": 1, the template format version, which is managed by the product. Newer releases read older templates (Upgrading).
Read-only viewing#
Show a published template without allowing edits:
<report-designer read-only font-base-url="/rd-fonts/"></report-designer>
Generating previews for a template list#
Thumbnails for your own template picker can be rendered in the browser in placeholder mode, without data:
import type { ReportDesigner } from '@rd/designer';
const designer = document.querySelector('report-designer') as ReportDesigner;
const { pdf } = await designer.renderPdf();
const url = URL.createObjectURL(new Blob([pdf.slice()], { type: 'application/pdf' }));
window.open(url);
Or render them on the server with real sample data through the render API (Render API).
Gallery entries#
The New from template gallery lists 20 built-in supply-chain templates. Add your organization's approved templates with the gallery property, and hide the
built-ins with builtin-gallery="false" if you want only yours.
import type { GalleryEntry, ReportDesigner, Template } from '@rd/designer';
const designer = document.querySelector('report-designer') as ReportDesigner;
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`${url}: ${res.status}`);
}
return res.json() as Promise<T>;
}
const entries: GalleryEntry[] = [
{
id: 'acme-bol',
name: 'ACME Bill of Lading',
category: 'Shipping', // built-in categories: Shipping, Orders, Procurement, Inbound, Warehouse, Inventory, Labels
description: 'Corporate BOL approved by Logistics, 2026 edition',
thumbnail: '/assets/templates/acme-bol.png', // URL or data: URI, about 300 px wide
tags: ['VICS', 'BOL'], // matched by the gallery search
locale: 'en-US',
nameI18n: { de: 'ACME Frachtbrief', es: 'Conocimiento de embarque ACME' },
descriptionI18n: { de: 'Von der Logistik freigegebener Frachtbrief' },
// Called only when the user picks the entry.
load: async () => ({
template: await fetchJson<Template>('/api/templates/acme-bol/published'),
schema: await fetchJson<object>('/api/schemas/bill-of-lading'),
}),
},
];
designer.gallery = entries;
- Picking an entry replaces the design (after a confirmation if there are unsaved edits) and fires
template-change. - The entry's
schemais applied unless you have set the element'sschemaproperty yourself, in which case your schema wins. nameI18n/descriptionI18nare keyed by UI language (de,pt-BR, …), falling back toname/description.- If
load()rejects, the user sees "Could not load the template …" with your error message.