React

React 19 and React 18. Install the packages, copy the fonts into public/, and wrap the element once.

npm install ./vendor/rd-designer-1.0.0.tgz ./vendor/rd-core-1.0.0.tgz ./vendor/rd-fonts-1.0.0.tgz
npm install ./vendor/rd-fonts-cjk-1.0.0.tgz   # optional: Chinese, Japanese, Korean

A wrapper component#

Wrap the element once in a small component. Setting objects through a ref works in React 18 and 19. React 18 would otherwise turn object props on custom elements into the string "[object Object]".

import { useEffect, useRef } from 'react';

import '@rd/designer';   // registers <report-designer>
import type { ReportDesigner as ReportDesignerElement, Template, TemplateChangeDetail } from '@rd/designer';

declare module 'react' {
    namespace JSX {
        interface IntrinsicElements {
            'report-designer': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
                'font-base-url'?: string;
                theme?: 'light' | 'dark';
            };
        }
    }
}

export interface ReportDesignerProps {
    template: Template | null;
    schema?: object | null;
    lang?: string;
    fontBaseUrl?: string;
    onTemplateChange?: (template: Template) => void;
    onSave?: (template: Template) => void;
}

export function ReportDesigner({ template, schema = null, lang = '', fontBaseUrl = '/rd-fonts/', onTemplateChange, onSave }: ReportDesignerProps) {
    const ref = useRef<ReportDesignerElement>(null);

    // Objects are DOM properties. Setting a template equal to the current design is a no-op, so echoing edits back is safe.
    useEffect(() => { ref.current!.template = template; }, [template]);
    useEffect(() => { ref.current!.schema = schema; }, [schema]);
    useEffect(() => { ref.current!.lang = lang; }, [lang]);

    useEffect(() => {
        const el = ref.current!;
        const change = (e: Event) => onTemplateChange?.((e as CustomEvent<TemplateChangeDetail>).detail.template);
        const save = (e: Event) => onSave?.((e as CustomEvent<TemplateChangeDetail>).detail.template);
        el.addEventListener('template-change', change);
        el.addEventListener('save', save);
        return () => {
            el.removeEventListener('template-change', change);
            el.removeEventListener('save', save);
        };
    }, [onTemplateChange, onSave]);

    return <report-designer ref={ref} font-base-url={fontBaseUrl} style={{ display: 'block', height: '100%' }} />;
}

Use it in a page#

Use it like any controlled component:

import { useCallback, useEffect, useState } from 'react';

import type { Template } from '@rd/designer';

import { ReportDesigner } from './ReportDesigner';

export function TemplateEditorPage({ templateId }: { templateId: string }) {
    const [template, setTemplate] = useState<Template | null>(null);
    const [dirty, setDirty] = useState(false);

    useEffect(() => {
        loadTemplate(templateId).then(setTemplate);
    }, [templateId]);

    const onTemplateChange = useCallback((t: Template) => {
        setTemplate(t);
        setDirty(true);
    }, []);

    const onSave = useCallback(async (t: Template) => {
        await saveTemplate(templateId, t);
        setDirty(false);
    }, [templateId]);

    return (
        <div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
            <header>{dirty ? 'Unsaved changes' : 'All changes saved'}</header>
            <div style={{ flex: 1, minHeight: 0 }}>
                <ReportDesigner template={template} schema={bolSchema} onTemplateChange={onTemplateChange} onSave={onSave} />
            </div>
        </div>
    );
}

Copy the fonts#

With Vite, copy them before building (or in postinstall), and keep font-base-url="/rd-fonts/":

mkdir -p public/rd-fonts
cp -R node_modules/@rd/fonts/fonts/. public/rd-fonts/
cp -R node_modules/@rd/fonts-cjk/fonts/. public/rd-fonts/   # if installed

Tip: Lazy-load the editor page (const TemplateEditorPage = lazy(() => import('./TemplateEditorPage'))) so the 0.8 MB designer isn't in your main bundle.