対象読者
- Next.jsで業務システムを開発している方
- 帳票機能の組み込みを検討している方
- ActiveReportsJSやWijmoに興味がある方
前提環境
筆者の検証環境は以下の通りです。
- macOS Tahoe 26.3.1
- Node.js 25.2.1
- npm 11.6.2
- Next.js 16.1.3
- React 19.2.3
- Wijmo 5.20252.44
- ActiveReportsJS 6.0.1
- Firebase 12.8.0
はじめに
前回までに、ActiveReportsJS Viewerで帳票を表示し、Wijmoコンポーネントからデータを連携させる仕組みを構築しました。しかし、帳票のレイアウトは開発時に作り込んだものが使われており、変更が必要になるたびに開発者への修正依頼が発生する状態でした。
「請求書のタイトルを変えたい」「列の幅を調整したい」といったレイアウトの微調整のたびに開発サイクルを回すのは、業務の現場では非効率です。ActiveReportsJS Designerをアプリケーションに組み込むことで、現場担当者がブラウザ上で帳票レイアウトを直接編集・保存できるようになります。
図1は、今回構築するDesigner画面の完成イメージです。
今回は以下の内容を解説します。
- ActiveReportsJS Designerの組み込みと保存機能の実装
- レポートパーツによる共通部品の作成と再利用
- マスターレポートによる帳票テンプレートの管理
- PDF/Excel/HTMLへのプログラマティックなエクスポート
ActiveReportsJS Designerの組み込み
ActiveReportsJS Designerは、ブラウザ上で帳票レイアウトを視覚的に編集できるコンポーネントです。前回までに組み込んだViewerと同様に、Next.jsアプリケーションに埋め込むことができます。
Designerコンポーネントの実装
Viewerの組み込みと同様に、Designerもブラウザ環境でのみ動作するため、SSR無効化と動的インポートが必要です。ここでは、より柔軟に初期化処理を制御できるよう、@mescius/activereportsjs/reportdesignerから直接インスタンス化する方法を採用します(リスト1)。
"use client";
import { useEffect, useRef, useState } from "react";
import "@mescius/activereportsjs/styles/ar-js-ui.css";
import "@mescius/activereportsjs/styles/ar-js-designer.css";
export default function ReportDesigner({ reportUri, masterReportId }) {
const hostRef = useRef(null);
const designerRef = useRef(null);
useEffect(() => {
const initDesigner = async () => {
if (!hostRef.current) return;
// 日本語ロケール
await import("@mescius/activereportsjs-i18n");
await import("@mescius/activereportsjs-i18n/dist/designer/ja-locale.js");
// Designer を動的インポートして初期化
const ArDesigner = await import(
"@mescius/activereportsjs/reportdesigner"
);
designerRef.current = new ArDesigner.Designer(hostRef.current);
// レポートを開く
if (reportUri) {
await designerRef.current.setReport({ id: reportUri });
} else {
await designerRef.current.createReport({ reportType: "CPL" });
}
};
initDesigner();
return () => {
designerRef.current?.destroy?.();
designerRef.current = null;
};
}, [reportUri, masterReportId]);
return <div ref={hostRef} style={{ width: "100%", height: "700px" }} />;
}
Viewerの場合と同じくuseEffect内で動的インポートを行いますが、CSSファイルが異なる点に注意してください。Viewerではar-js-viewer.cssを使いましたが、Designerではar-js-designer.cssを使用します。ar-js-ui.cssは共通です。
日本語化には、Viewer用の@mescius/activereportsjs-i18nに加えて、Designer固有のdesigner/ja-locale.jsも必要です。setReport({ id: reportUri })で既存のレポート定義ファイルを開き、createReport({ reportType: "CPL" })で新規レポートを作成します。
保存機能の実装
Designerで編集したレポートを保存するには、setActionHandlersでonSave/onSaveAsコールバックを設定します(リスト2)。
// 保存ハンドラーを設定
designer.setActionHandlers({
// 上書き保存:現在のレポートIDでサーバーに送信
onSave: async (info) => {
const res = await fetch("/api/reports", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: info.id, // レポートのファイルパス
definition: info.definition, // レポート定義JSON
}),
});
if (!res.ok) throw new Error("保存に失敗しました");
// 保存成功時は表示名を返す
return { displayName: info.displayName };
},
// 名前を付けて保存:ユーザーに新しいファイル名を入力してもらう
onSaveAs: async (info) => {
const name = prompt("ファイル名を入力してください");
if (!name) return undefined; // キャンセル時は何もしない
const res = await fetch("/api/reports", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: /reports/${name},
definition: info.definition,
}),
});
if (!res.ok) throw new Error("保存に失敗しました");
// 新しいIDと表示名を返してDesignerに反映
return { id: /reports/${name}, displayName: name };
},
});
onSaveは上書き保存、onSaveAsは名前を付けて保存に対応します。info.definitionにはレポート定義のJSONオブジェクトが渡されるため、これをサーバー側のAPIに送信してファイルとして保存します。
レポート保存用API Route
保存先としてNext.jsのAPI Routeを用意します(リスト3)。
import { NextRequest, NextResponse } from "next/server";
import { writeFile } from "fs/promises";
import { join } from "path";
export async function POST(request: NextRequest) {
// リクエストボディからレポートIDと定義を取得
const { id, definition } = await request.json();
// 必須パラメータのバリデーション
if (!id || !definition) {
return NextResponse.json(
{ error: "id and definition are required" },
{ status: 400 }
);
}
// ファイル名のサニタイズ(パストラバーサル防止)
const filename = id.replace(/^\/reports\//, "").replace(/[^a-zA-Z0-9._-]/g, "");
const filePath = join(process.cwd(), "public", "reports", filename);
// 読みやすいようにインデント付きでJSON文字列化
const json = JSON.stringify(definition, null, 2);
// ファイルとして保存
await writeFile(filePath, json, "utf-8");
return NextResponse.json({ success: true, path: /reports/${filename} });
}
受け取ったレポート定義JSONをpublic/reports/ディレクトリにファイルとして保存します。ファイル名のサニタイズでは、まずreplace(/^\/reports\//, "")でパスプレフィックスを除去してファイル名だけを取り出し、次にreplace(/[^a-zA-Z0-9._-]/g, "")で英数字・ドット・ハイフン・アンダースコア以外の文字を除去しています。これにより../などを使ったパストラバーサル攻撃を防いでいます。
JSON.stringify(definition, null, 2)の第2引数nullはreplacer関数を省略する指定で全プロパティがそのまま出力されます。第3引数2はインデント幅で、保存されたJSONファイルを人間が読みやすい形式にします。
本番環境ではデータベースやクラウドストレージへの保存が適切ですが、開発・デモ用途としてはこのシンプルな構成で十分です。
Designerページの構成
Designer用のページは、前回のViewer組み込みと同じく、Server Component + Client Component + dynamic({ ssr: false })のパターンで構成します(リスト4)。
// page.tsx(Server Component)― レイアウトとSuspense境界を定義
import { Suspense } from "react";
import DesignerClient from "./DesignerClient";
export default function DesignerPage() {
return (
<div>
<h1>帳票デザイナー</h1>
<Suspense>
<DesignerClient />
</Suspense>
</div>
);
}
// DesignerClient.tsx(Client Component)― SSR無効でDesignerを読み込む
"use client";
import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
// Designerはブラウザ専用のため、SSRを無効化して動的インポート
const ReportDesigner = dynamic(
() => import("@/components/reports/ReportDesigner"),
{ ssr: false }
);
export default function DesignerClient() {
// URLクエリパラメータから編集対象のレポートURIを取得
const searchParams = useSearchParams();
const reportUri = searchParams.get("report") || undefined;
return <ReportDesigner reportUri={reportUri} />;
}
useSearchParamsでURLクエリパラメータを取得し、?report=/reports/order-detail.rdlx-jsonのように既存レポートを指定してDesignerで開くことができます。
帳票の編集と保存を試す
Designerの組み込みが完了したところで、実際に帳票を編集してみましょう。前回作成した受発注明細レポート(order-detail.rdlx-json)をDesignerで開き、タイトルを「受発注明細書」から「請求書」に変更して、別名で保存します。
Designerでは、テキストボックスをダブルクリックするとテキストを直接編集できます。編集後、ツールバーの保存ボタンから「名前を付けて保存」を選ぶと、ファイル名を入力するダイアログが表示されます。ここではinvoice.rdlx-jsonとして保存しました。
図2は、タイトルを「請求書」に変更して保存する操作の画面です。
このように、開発者でなくてもブラウザ上で帳票レイアウトを修正し、保存できる仕組みが実現しました。
レポートパーツで共通部品を作成する
帳票の種類が増えると、会社名・住所・ロゴといった共通要素を帳票ごとに作り直す問題が出てきます。ActiveReportsJSのレポートパーツ機能を使うと、これらの共通要素をライブラリ化して再利用できます。
レポートパーツの作成手順
レポートパーツは、Designer上で作成します。共通ヘッダー(会社名・住所・電話番号)を例に、作成手順を説明します。
- Designerで新規レポートを作成する
- TextBoxを配置して、会社名(16pt)、住所(9pt)、電話番号(9pt)を縦に並べる
- 配置した要素をすべて選択し、右クリックメニューから「レポートパーツの作成」を選ぶ
- 右ペインの「レポートパーツ」タブで表示名を設定する
- 「名前を付けて保存」でライブラリファイルとして保存する
図3は、レポートパーツを作成した後のDesigner画面です。右ペインに「レポートパーツ」タブが表示され、パーツの表示名を設定できます。
ライブラリの読み込み
保存したレポートパーツライブラリは、Designerの初期化時にsetReportPartsLibrariesで読み込みます(リスト5)。
// レポートパーツライブラリを設定(ファイルが存在する場合のみ)
// HEADリクエストでライブラリファイルの存在を確認
const libRes = await fetch("/reports/parts-library.rdlx-json",
{ method: "HEAD" });
// ファイルが存在する場合のみライブラリとして登録
if (libRes.ok) {
await designer.setReportPartsLibraries([
{
id: "/reports/parts-library.rdlx-json",
name: "CommonParts",
displayName: "共通パーツ",
},
]);
}
ライブラリを設定すると、Designerの左パネルの「ライブラリ」タブから、登録されたパーツを帳票上にドラッグ&ドロップで配置できるようになります。共通ヘッダーを複数の帳票で統一したい場合に便利です。
図4は、Designerの「ライブラリ」タブを開いた状態です。「共通パーツ」の下に登録されたパーツが一覧表示され、帳票上にドラッグ&ドロップで配置できます。
マスターレポートで帳票テンプレートを管理する
レポートパーツが「部品の再利用」を実現する機能だとすると、マスターレポートは「テンプレートの共通化」を実現する機能です。マスターレポートを使うと、共通レイアウトをベースに新規帳票を効率的に作成できます。
マスターレポートへの変換
既存のレポートをマスターレポートに変換するには、Designerの「レポート」タブから「マスターレポートに変換」を選びます。変換後、ツールバーに「エリアの追加」ボタンが表示され、レポートエリア(コンテンツプレースホルダ)を定義できるようになります。
図5は、受発注明細レポートをマスターレポートに変換した画面です。
マスターレポートの保存と利用
マスターレポートを保存する際は、拡張子を.rdlx-json-masterにする必要があります。通常の.rdlx-jsonで保存すると、派生レポート作成時にDecodingErrorが発生します。
マスターレポートをベースに新規レポートを作成するには、createReportにmasterReportIdを指定します(リスト6)。
// マスターレポートをベースにした新規レポートを作成
await designer.createReport({
reportType: "CPL",
masterReportId: "/reports/invoice-template.rdlx-json-master",
});
派生レポートでは、マスターレポートのレイアウトが読み取り専用で表示され、レポートエリア内のみ編集可能になります。請求書・納品書・注文書のように共通レイアウトを持つ帳票群を管理する際に有効です。
図6は、マスターレポートをベースに作成した派生レポートをDesignerで開いた状態です。右側のプロパティパネルに「マスターレポート」フィールドが表示され、マスター部分がグレーアウトして読み取り専用になっていることが確認できます。
PDF/Excel/HTMLエクスポート
ActiveReportsJSでは、Viewer UIを使わずにプログラマティックに帳票をエクスポートできます。PDF・Excel・HTMLの3形式に対応しており、それぞれ専用モジュールを動的インポートして使用します(リスト7)。
const handleExport = async (format: "pdf" | "xlsx" | "html", filename: string) => {
// ActiveReportsJSのコアモジュールを動的インポート
const Core = await import("@mescius/activereportsjs/core");
const pageReport = new Core.PageReport();
// データバインディングが必要な場合はレポート定義を加工して読み込む
if (data) {
// レポート定義JSONを取得
const response = await fetch(reportUri);
const reportDef = await response.json();
// データソースの接続文字列にJSONデータを直接埋め込む
if (reportDef.DataSources && reportDef.DataSources.length > 0) {
reportDef.DataSources[0].ConnectionProperties.ConnectString =
"jsondata=" + JSON.stringify(data);
}
// 加工済みの定義オブジェクトを読み込む
await pageReport.load(reportDef);
} else {
// データ不要の場合はURIから直接読み込む
await pageReport.load(reportUri);
}
// レポートを実行してページドキュメントを生成
const pageDocument = await pageReport.run();
// 指定された形式でエクスポートしてダウンロード
if (format === "pdf") {
const PdfExport = await import("@mescius/activereportsjs/pdfexport");
const result = await PdfExport.exportDocument(pageDocument, {
info: { title: filename },
});
result.download(${filename}.pdf);
} else if (format === "xlsx") {
const XlsxExport = await import("@mescius/activereportsjs/xlsxexport");
const result = await XlsxExport.exportDocument(pageDocument);
result.download(${filename}.xlsx);
} else {
const HtmlExport = await import("@mescius/activereportsjs/htmlexport");
const result = await HtmlExport.exportDocument(pageDocument);
result.download(${filename}.html);
}
};
エクスポートの流れは、PageReportでレポートを読み込み → run()で実行 → 各形式のexportDocument()でエクスポート → download()でダウンロード、という4ステップです。
if (data)ブロックでは、プログラマティック実行時にデータを帳票に注入する処理を行っています。Viewerで表示する場合はデータソース設定に基づいてデータが自動的に取得されますが、PageReportで直接実行する場合は、レポート定義JSONを取得したうえでデータソースのConnectStringに"jsondata=" + JSON.stringify(data)を設定し、実行時データを埋め込む必要があります。
図7は、エクスポートボタンを配置した画面です。
前節でDesignerを使って編集・保存した請求書レポートも、このエクスポート機能を通じてPDFやExcelとして出力できます。Designerでレイアウトを調整し、エクスポートで配布用ファイルを生成する、という一連の業務フローがアプリケーション内で完結します。
まとめ
本記事では、ActiveReportsJS Designerの組み込みを中心に、帳票開発を効率化する機能を解説しました。
-
Designer組み込み:
@mescius/activereportsjs/reportdesignerから直接インスタンス化し、setActionHandlersで保存機能を実装 -
レポートパーツ: 共通のヘッダー・フッターをライブラリ化し、
setReportPartsLibrariesで複数帳票から再利用 -
マスターレポート: 共通レイアウトをテンプレート化し、
.rdlx-json-masterとして管理 -
エクスポート:
PageReport→run()→exportDocument()でPDF/Excel/HTMLをプログラマティックに生成
全3回のシリーズを通じて、Firebase + Next.js + Wijmo + ActiveReportsJSによる業務システムの構築手順を解説してきました。
最終的に構築したシステムでは、開発者がWijmoとActiveReportsJSで業務UIと帳票の基盤を作り、現場担当者がDesignerで帳票レイアウトを自分で調整できる、という役割分担が実現しています。本番運用に向けては、レポート定義の永続化先(データベースやクラウドストレージ)の検討や、Designerの編集権限管理などが追加で必要になるでしょう。
サンプルコードは本記事の添付ファイルからダウンロードできます。ぜひ手元で動かして、帳票開発の効率化を体験してみてください。

