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.