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-change reports every edit (debounced). Use it for dirty flags, autosave drafts or live previews elsewhere in your UI.
  • save means the user wants to save now. Use it for the real save. A final template-change is dispatched immediately before every save, so a listener that only watches template-change never misses the last edit.
  • Loading a template (the property, setTemplate() or the template option of mountDesigner) 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), or POST /api/v1/validate on 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 on save or 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).

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 schema is applied unless you have set the element's schema property yourself, in which case your schema wins.
  • nameI18n / descriptionI18n are keyed by UI language (de, pt-BR, …), falling back to name / description.
  • If load() rejects, the user sees "Could not load the template …" with your error message.