対象読者
- 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は、タイトルを「請求書」に変更して保存する操作の画面です。
このように、開発者でなくてもブラウザ上で帳票レイアウトを修正し、保存できる仕組みが実現しました。

