ReoGrid ReoGrid Web

AngularでReoGridを使う — ラッパー不要のExcelライクなスプレッドシート

· unvell team
AngularでReoGridを使う — ラッパー不要のExcelライクなスプレッドシート

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 の両方で動作します。ファイル入力を繋ぎ、FileloadFromFile に渡します。

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 でマウントし、ngOnDestroydestroy()、そしてグリッドを自身のデータの所有者として扱う。ラッパーも、Shadow DOM の癖も、キャンバスと戦う仮想 DOM の差分計算もなし — ただ速い、Excel ライクなグリッドが Angular アプリに載るだけです。

グリッドの機能はライブデモで確認できます。あるいははじめにガイドへどうぞ。Excel ファイルを閲覧したいだけなら、無料オンライン XLSX ビューアをお試しください。

ReoGrid Web を試してみる

React/Vue 向けの Canvas ベース Excel 互換スプレッドシートコンポーネント。 Lite は無料 — npm install 一発で始められます。

関連記事

ニュースレター

開発の最新情報をお届けします

新しいリリース・機能追加・お知らせをいち早く受け取るには、
メーリングリストにご登録ください。