ReoGrid Web は React と Vue の公式ラッパーを提供していますが、Angular 用のラッパーはありません — そして必要ありません。 グリッドのコアは、任意の DOM 要素にマウントできるフレームワーク非依存のファクトリです。Angular ではコンポーネントから直接呼び出します。これはラッパーより、むしろきれいかもしれません。ライフサイクルを自分で握れて、API との間に余計な抽象が挟まりません。
この記事では、マウント・投入・編集・Angular への同期・Excel ファイルの読み込みという統合の全体を、1つの standalone コンポーネントで示します。
インストール
npm install @reogrid/lite
@reogrid/lite は無料ティア(100 行 × 26 列、算術数式)です。SUM のような組み込み関数、並べ替え/フィルター、ウィンドウ枠の固定、xlsx エクスポートが必要になったら @reogrid/pro に差し替えてください。API は同一なので、以下のコードは一切変わりません。
最小の standalone コンポーネント
コアのエントリポイントは createReogrid(target) で、target は CSS セレクタ または HTMLElement です。Angular では @ViewChild で要素を取得し、ngAfterViewInit でマウントします。後片付けは ngOnDestroy で行います。インスタンスは destroy() を公開しています。
import {
Component,
ElementRef,
ViewChild,
AfterViewInit,
OnDestroy,
} from '@angular/core';
import { createReogrid, type ReogridInstance } from '@reogrid/lite';
@Component({
selector: 'app-grid',
standalone: true,
template: `<div #host style="width: 100%; height: 480px;"></div>`,
})
export class GridComponent implements AfterViewInit, OnDestroy {
@ViewChild('host', { static: true }) host!: ElementRef<HTMLDivElement>;
private grid?: ReogridInstance;
ngAfterViewInit() {
this.grid = createReogrid(this.host.nativeElement);
const { worksheet } = this.grid;
worksheet.cell('A1').setValue('Product').setStyle({ bold: true, backgroundColor: '#eff6ff' });
worksheet.cell('B1').setValue('Price').setStyle({ bold: true, backgroundColor: '#eff6ff' });
worksheet.cell('A2').setValue('Widget');
worksheet.cell('B2').setValue('9.99');
worksheet.column(0).width = 160;
}
ngOnDestroy() {
this.grid?.destroy();
}
}
これで、編集可能な Excel ライクのグリッドが動きます。セルをダブルクリックすれば、本物のスプレッドシートと同じように編集できます。
なぜ
static: true? ホストの<div>は常にテンプレートに存在する(*ngIfの裏ではない)ため、ngAfterViewInitで利用できます。構造ディレクティブの裏にグリッドを置く場合は{ static: false }を使い、要素が存在してからマウントしてください。
ライブ合計付きの編集可能な請求書
スプレッドシートの真価は、セルが互いに参照し合うときに発揮されます。数式は = で始まる入力そのものです ― setValue('=B2*C2') でも setCellInput(row, col, '=B2*C2') でも数式として評価されます。setCellInput は 0 始まりの (行, 列) で指定できる形で、ループ内で便利です。
ngAfterViewInit() {
this.grid = createReogrid(this.host.nativeElement);
const { worksheet } = this.grid;
['Item', 'Qty', 'Unit Price', 'Subtotal'].forEach((h, c) =>
worksheet.cell(0, c).setValue(h).setStyle({ bold: true, backgroundColor: '#eff6ff' }),
);
const items = [
{ item: 'Design review', qty: 4, price: 150 },
{ item: 'Prototype build', qty: 8, price: 120 },
{ item: 'User testing', qty: 3, price: 140 },
];
items.forEach((it, i) => {
const row = i + 1;
worksheet.cell(row, 0).setValue(it.item);
worksheet.cell(row, 1).setValue(String(it.qty));
worksheet.cell(row, 2).setValue(String(it.price));
worksheet.setCellInput(row, 3, `=B${row + 1}*C${row + 1}`);
});
const totalRow = items.length + 1;
worksheet.cell(totalRow, 2).setValue('Total').setStyle({ bold: true });
worksheet.setCellInput(totalRow, 3, `=SUM(D2:D${items.length + 1})`);
worksheet.range(`C2:D${totalRow + 1}`).setFormat('$#,##0.00');
}
数量や単価を編集すると、小計と総合計が即座に再計算されます。(SUM は Pro の関数です。Lite では合計を =D2+D3+D4 と書いてください。)
グリッドの編集を Angular に同期する
ReoGrid は命令的なキャンバスで、Angular の変更検知には参加しません。正しいパターンは 一方向の購読 です。セルデータはグリッドが所有し、必要な部分だけをコンポーネントのフィールドや signal にミラーします。
import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy, NgZone, signal } from '@angular/core';
import { createReogrid, type ReogridInstance } from '@reogrid/lite';
@Component({
selector: 'app-grid',
standalone: true,
template: `
<div style="padding: 8px; font-size: 18px;">Total: <strong>{{ total() }}</strong></div>
<div #host style="width: 100%; height: 420px;"></div>
`,
})
export class GridComponent implements AfterViewInit, OnDestroy {
@ViewChild('host', { static: true }) host!: ElementRef<HTMLDivElement>;
private grid?: ReogridInstance;
readonly total = signal('$0.00');
constructor(private zone: NgZone) {}
ngAfterViewInit() {
this.grid = createReogrid(this.host.nativeElement);
const { worksheet } = this.grid;
// ...上記と同様に投入...(総合計の数式は 行4・列3 = D5 に入る)
// ReoGrid のイベント購読は専用メソッドです(汎用の .on() はありません)。
// 戻り値は購読解除関数なので、破棄時に呼べます。
worksheet.onCellValueChange(() => {
// *計算済み* の表示値を読みます。cell().value は生の入力値を返すため、
// 数式セルでは「=SUM(D2:D4)」という数式文字列になってしまいます。
// getDisplayText()(または getCellNumericValue())を使ってください。
const grand = worksheet.getDisplayText(4, 3); // D5 = 合計行
this.zone.run(() => this.total.set(grand ?? '$0.00'));
});
}
ngOnDestroy() {
this.grid?.destroy();
}
}
知っておくべき点がいくつかあります。
- zoneless や
OnPushの構成では、グリッドのイベントコールバックが Angular の変更検知をトリガーしないことがあります。state の更新をNgZone.run()で包む(または signal を使う)のが安全なパターンで、ビューが確実に更新されます。 - 計算済みの値を読むには
worksheet.getDisplayText(row, col)(書式適用後のテキスト)かworksheet.getCellNumericValue(row, col)(数値)を使います。cell(...).valueゲッターは生の入力値を返すため、数式セルでは結果ではなく数式文字列になります。 - 変更のたびに Angular の state からグリッドを書き戻す制御コンポーネント化はしないでください。キャンバスレンダラーと衝突し、フィードバックループを生みます。セルデータの真実の源はグリッドであり、Angular は派生値・集計値を保持します。
Excel ファイルを読み込む
xlsx の読み込みは Lite・Pro の両方で動作します。ファイル入力を繋ぎ、File を loadFromFile に渡します。
async onFile(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (file && this.grid) {
await this.grid.worksheet.loadFromFile(file);
}
}
<input type="file" accept=".xlsx" (change)="onFile($event)" />
loadFromUrl('/data/report.xlsx') や loadFromBuffer(arrayBuffer) も使えます。xlsx への書き出し(saveAsXlsx)は Pro の機能です。
Angular での Lite と Pro
Angular の統合はティア間で何も変わりません。import の形もコンポーネントのコードも同じです。Lite は 100 × 26 が上限で、組み込み関数と xlsx エクスポートが無効です。SUM/VLOOKUP、並べ替え&フィルター、ウィンドウ枠の固定、条件付き書式、xlsx エクスポートが必要な実アプリでは、import を @reogrid/pro に切り替え、ライセンスキーを渡します。
this.grid = createReogrid(this.host.nativeElement, { licenseKey: 'YOUR_LICENSE_KEY' });
まとめ
キャンバススプレッドシートと Angular のライフサイクルフックは自然に噛み合います。ngAfterViewInit でマウントし、ngOnDestroy で destroy()、そしてグリッドを自身のデータの所有者として扱う。ラッパーも、Shadow DOM の癖も、キャンバスと戦う仮想 DOM の差分計算もなし — ただ速い、Excel ライクなグリッドが Angular アプリに載るだけです。
グリッドの機能はライブデモで確認できます。あるいははじめにガイドへどうぞ。Excel ファイルを閲覧したいだけなら、無料オンライン XLSX ビューアをお試しください。