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 }を使い、要素が存在してからマウントしてください。
ライブ合計付きの編集可能な請求書
スプレッドシートの真価は、セルが互いに参照し合うときに発揮されます。数式には setCellInput() を使います(setValue('=B2*C2') だと数式ではなくリテラル文字列として保存されてしまいます)。
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;
// ...上記と同様に投入...
worksheet.on('cellValueChange', () => {
// Angular の外で再計算し、zone の中で signal を更新する
const grand = worksheet.cell(`D${/* 合計行 */ 4}`).value;
this.zone.run(() => this.total.set(grand ?? '$0.00'));
});
}
ngOnDestroy() {
this.grid?.destroy();
}
}
知っておくべき点が2つあります。
- グリッドのイベントは Angular の zone の外で発火します。そのため state の更新は
NgZone.run()で包む(または signal +markForCheckを使う)必要があり、そうしないとビューが更新されません。 - 変更のたびに 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 ビューアをお試しください。