Angular

Angular 17.1 or later, standalone components or NgModules. Three steps: install the packages, copy the fonts with an asset glob, and wrap the element in a typed component.

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 typed wrapper component#

A typed wrapper component with @Inputs and @Outputs. templateChange pairs with the template input, so [(template)] two-way binding works:

import { CUSTOM_ELEMENTS_SCHEMA, Component, EventEmitter, Input, Output } from '@angular/core';

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

@Component({
    selector: 'app-report-designer',
    standalone: true,
    // Lets Angular accept the <report-designer> tag and bind its properties and events.
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    template: `
        <report-designer
            [template]="template"
            [schema]="schema"
            [lang]="lang"
            [attr.font-base-url]="fontBaseUrl"
            (template-change)="onTemplateChange($event)"
            (save)="onSave($event)"
        ></report-designer>
    `,
    styles: `
        :host, report-designer { display: block; height: 100%; }
    `,
})
export class ReportDesignerComponent {
    @Input() template: Template | null = null;
    @Input() schema: object | null = null;
    @Input() lang = '';
    @Input() fontBaseUrl = '/rd-fonts/';
    @Output() templateChange = new EventEmitter<Template>();
    @Output() saved = new EventEmitter<Template>();

    onTemplateChange(e: Event): void {
        this.template = (e as CustomEvent<TemplateChangeDetail>).detail.template;
        this.templateChange.emit(this.template);
    }

    onSave(e: Event): void {
        this.saved.emit((e as CustomEvent<TemplateChangeDetail>).detail.template);
    }
}

Use it in a page#

import { Component, OnInit, input, signal } from '@angular/core';

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

import { ReportDesignerComponent } from './report-designer.component';

@Component({
    selector: 'app-template-editor',
    standalone: true,
    imports: [ReportDesignerComponent],
    template: `
        <header>{{ dirty() ? 'Unsaved changes' : 'All changes saved' }}</header>
        <app-report-designer
            [(template)]="template"
            [schema]="schema"
            (templateChange)="dirty.set(true)"
            (saved)="save($event)"
        ></app-report-designer>
    `,
    styles: `:host { display: flex; flex-direction: column; height: 100vh; } app-report-designer { flex: 1; min-height: 0; }`,
})
export class TemplateEditorComponent implements OnInit {
    readonly templateId = input.required<string>();
    readonly dirty = signal(false);
    template: Template | null = null;
    schema: object = bolSchema;

    async ngOnInit(): Promise<void> {
        this.template = await loadTemplate(this.templateId());
    }

    async save(t: Template): Promise<void> {
        await saveTemplate(this.templateId(), t);
        this.dirty.set(false);
    }
}

Copy the fonts#

Copy both font packages into one served folder with angular.json asset globs, so no copy script is needed (drop the second glob if you didn't install @rd/fonts-cjk):

"assets": [
    { "glob": "**/*", "input": "public" },
    { "glob": "**/*", "input": "node_modules/@rd/fonts/fonts", "output": "rd-fonts" },
    { "glob": "**/*", "input": "node_modules/@rd/fonts-cjk/fonts", "output": "rd-fonts" }
],
"allowedCommonJsDependencies": ["pako"]

Raise the initial bundle budget in angular.json, or lazy-load the editor route (loadComponent: () => import('./template-editor.component').then(m => m.TemplateEditorComponent)). pako is a CommonJS dependency of the PDF engine. Listing it silences the build warning.

Angular with NgModules#

For applications that still use NgModules, declare a non-standalone component and add CUSTOM_ELEMENTS_SCHEMA to the module:

import { CUSTOM_ELEMENTS_SCHEMA, Component, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

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

@Component({
    selector: 'app-legacy-editor',
    standalone: false,
    template: `<report-designer [template]="template" font-base-url="/rd-fonts/" (save)="onSave($event)"></report-designer>`,
    styles: `report-designer { display: block; height: 100vh; }`,
})
export class LegacyEditorComponent {
    template: Template | null = null;

    onSave(e: Event): void {
        void saveTemplate('bol', (e as CustomEvent<TemplateChangeDetail>).detail.template);
    }
}

@NgModule({
    declarations: [LegacyEditorComponent],
    imports: [BrowserModule],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    bootstrap: [LegacyEditorComponent],
})
export class LegacyEditorModule {}