マスター/詳細グリッドレイアウトでのオンデマンドデータロード
オンデマンド・データ・ロードは、マスター/ディテール・インターフェースをスケーリングするための強力な手法です。すべての詳細を事前に取得する代わりに、ユーザが必要とする場合にのみ関連レコードを読み込みます。詳細については、こちらの記事をご覧ください。
前回の記事では、グリッドを使用したマスター/詳細レイアウトの使用Ignite UI for Angular、クリーンで効率的なマスター/詳細インターフェイスを設定する方法を検討しました。このパターンは、品目のある注文や従業員の部門などの関連レコードを、画面を乱雑にすることなく表示するのに最適です。
しかし、データセットが巨大になるとどうなるでしょうか?すべてのレコードのすべての詳細を一度に読み込むのは非効率的です。そこで、オンデマンドのデータ読み込みの出番です。考えられるすべての詳細を事前に取得する代わりに、ユーザーが行を展開したときにのみデータを読み込むことで、初期レンダリングが高速化され、インタラクションがスムーズになり、スケーラビリティが向上します。
この記事では、オンデマンド読み込みが重要な理由、Ignite UI for Angular Grid コンポーネントにオンデマンド ロードを実装する方法、およびオンデマンド ロードを最大限に活用するためのベスト プラクティスについて詳しく説明します。
主従テンプレートの拡張
最初の記事を読んだ方は、展開可能な行と詳細テンプレートを使用してマスターグリッドを設定する方法と、データの表示先をすでにご存知でしょう。
- 同じデータセット – たとえば、注文とそれに関連するアイテムがバンドルされます
- 外部データセット – たとえば、customerID をパラメータとして受け取り、API を呼び出して顧客の注文データを取得するサービス。
大規模なデータセットの場合、2 番目のアプローチ (外部詳細フェッチ) は、オンデマンド読み込みが最も効果的であることが証明される場所です。
Angularプロジェクトの設定
オンデマンド読み込みの実装に入る前に、デモ用のAngularワークスペースを準備しましょう。手順は次のとおりです。
- 新しいAngularプロジェクトを作成する
まだお持ちでない場合は、まずAngular CLI を使用して新しいAngularアプリケーションを生成し、お好みの IDE でプロジェクトを開きます。
ng new load-on-demand-grid cd load-on-demand-grid
- Install Ignite UI for Angular
豊富な UI コンポーネント セットを提供するIgnite UI for Angularコンポーネント ライブラリ(最も重要なのは、マスター詳細とオンデマンドの読み込み例の基盤である igxGrid) を使用します。次の方法でプロジェクトに追加します。
ng add igniteui-angular
- Create a demo component
すべてを整理しておくには、グリッドデモを構築する専用のAngularコンポーネントを生成します。
ng generate component pages/grid-demo
- デモのルーティングを設定する
最後に、デモ コンポーネントが既定のビューになるようにルーティング構成を調整します。既存のapp.routes.ts(またはルーティングモジュール)を最小限のセットアップに置き換えます。
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: '/demo', pathMatch: 'full' },
{ path: 'demo', loadComponent: () => import('./pages/grid-demo/grid-demo.component').then(m => m.GridDemoComponent) },
{ path: '**', redirectTo: '/demo' }
];
app.config.ts のプロバイダーに provideHttpClient を追加します。
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimations(),
provideHttpClient()
]
};
データの設定
マスター/ディテール グリッドにオンデマンド読み込みを実装する前に、信頼性の高いデータ レイヤーが必要です。データモデルとサービスを準備すると、グリッドに意味のあるコンテンツが表示され、ユーザーが行を操作するときに関連する詳細を取得できるようになります。
IgxGrid のデータは、さまざまなソースから取得できます。
- 静的/ローカルデータ – assets フォルダー内の JSON ファイル。
- リモートAPI – バックエンドサービスからAngularのHttpClientを介して取得されます。
- モックされたデータ – 開発中にサービスで直接生成されます。
この例では、顧客とその関連する注文を提供するカスタム リモート Northwind Swagger API に接続します。概念は、データソース構造に関係なく同じままです。
Define Data Models
タイプセーフと明確さを強制するには、専用のmodels.tsファイルでデータを表すTypeScriptインターフェイスを定義します。これらのモデルは API 応答の構造を反映しており、データをグリッドに簡単にバインドできます。
export interface AddressDto {
street?: string;
city?: string;
region?: string;
postalCode?: string;
country?: string;
phone?: string;
}
export interface CustomerDto {
customerId?: string;
companyName: string;
contactName?: string;
contactTitle?: string;
address?: AddressDto;
}
export interface OrderDto {
orderId: number;
customerId: string;
employeeId: number;
shipperId?: number;
orderDate?: Date;
requiredDate?: Date;
shipVia?: string;
freight: number;
shipName?: string;
completed: boolean;
shipAddress?: AddressDto;
}
Create a Data Service
次に、すべての API 呼び出しを専用サービス (northwind-swagger.service.ts など) に一元化します。また、アプリの回復性を維持するための基本的なエラー処理も追加します。
const API_ENDPOINT = 'https://data-northwind.indigo.design';
@Injectable({
providedIn: 'root'
})
export class NorthwindSwaggerService {
constructor(
private http: HttpClient
) { }
public getCustomerDtoList(): Observable<CustomerDto[]> {
return this.http.get<CustomerDto[]>(`${API_ENDPOINT}/Customers`)
.pipe(catchError(this.handleError<CustomerDto[]>('getCustomerDtoList', [])));
}
public getOrderDtoList(id: string): Observable<OrderDto[]> {
return this.http.get<OrderDto[]>(`${API_ENDPOINT}/Customers/${id}/Orders`)
.pipe(catchError(this.handleError<OrderDto[]>('getOrderDtoList', [])));
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(`${operation} failed: ${error.message}`, error);
return of(result as T);
};
}
}
Bind the Grid to Customer Data
モデルとサービスが配置されたら、次のステップは、グリッドに Customers データセットを表示することです。グリッドのデータ プロパティは、サービスからデータをフェッチしてコンポーネントが初期化されるときに設定される northwindSwaggerCustomerDto 配列にバインドされます。
<igx-grid [data]="northwindSwaggerCustomerDto" primaryKey="customerId" [allowFiltering]="true" filterMode="excelStyleFilter" class="grid"> <igx-column field="customerId" dataType="string" header="customerId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> <igx-column field="companyName" dataType="string" header="companyName" [filterable]="true" [sortable]="true" required="true" [maxlength]="100" [selectable]="false"></igx-column> <igx-column field="contactName" dataType="string" header="contactName" [filterable]="true" [sortable]="true" [maxlength]="50" [selectable]="false"></igx-column> </igx-grid>
TypeScript 側で、GridDemoComponent クラスにサービス サブスクリプションを追加して、顧客データを取得し、それを northwindSwaggerCustomerDto プロパティに格納します。次に、グリッド データ入力プロパティを 'northwindSwaggerCustomerDto ' にバインドします。
@Component({
selector: 'app-grid-demo',
standalone: true,
imports: [IgxCheckboxComponent, IgxSimpleComboComponent, IgxGridComponent, IgxColumnComponent, IgxColumnMaxLengthValidatorDirective, NgIf, AsyncPipe, IgxGridDetailTemplateDirective],
templateUrl: './grid-demo.component.html',
styleUrl: './grid-demo.component.css'
})
export class GridDemoComponent implements OnInit, OnDestroy{
private destroy$ = new Subject<void>();
public northwindSwaggerCustomerDto: CustomerDto[] = [];
constructor(private northwindSwaggerService: NorthwindSwaggerService) {}
ngOnInit() {
this.northwindSwaggerService
.getCustomerDtoList()
.pipe(takeUntil(this.destroy$))
.subscribe(data => (this.northwindSwaggerCustomerDto = data));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

詳細テンプレートの設計とオンデマンドロードの実装
マスターグリッドに多くの親行 (顧客) が表示される場合、すべての行のすべての関連する子リスト (注文) を前もってフェッチするのは非効率的です。代わりに、ユーザーが行を展開したときにのみ詳細を読み込みます。これがオンデマンド読み込みの本質であり、必要な場合にのみ子データを要求し、詳細テンプレートに表示します。
使い方?
- igxGridDetailテンプレートをマスターigx-grid内に配置します。
- そのテンプレートで、Observable<OrderDto[]>を返すメソッド(getOrders(customerId)など)を呼び出します。
- 非同期パイプを使用してテンプレート内のオブザーバブルをサブスクライブし、使用可能になった結果Angularレンダリングします。
- 各顧客の監視可能/結果をキャッシュして、複数の拡張 (または迅速な変更検出サイクル) によって新しい HTTP 呼び出しがトリガーされないようにします。
どのように実装すればよいでしょうか?
- 詳細テンプレートを追加する
まず、igxGridDetailディレクティブでng-templateを追加して、詳細コンテンツのプレースホルダーを定義します。これにより、展開された各行の詳細ビューがレンダリングされる場所がマークされます。
<ng-template igxGridDetail let-rowData></ng-template>
- 注文にバインドされたコンボの追加 (オンデマンドで読み込まれる)
テンプレート内では、必要なコンポーネントをレンダリングできます。この例では、選択した顧客の注文を表示するコンボ ボックスを追加します。ここで重要なのは、行が展開されるまで注文がロードされないことです。
<ng-container *ngIf="getOrders(rowData.customerId) | async as orders">
<div class="row-layout group">
<igx-simple-combo
type="border"
[data]="orders"
displayKey="orderId"
class="single-select-combo">
<label igxLabel>Order Id</label>
</igx-simple-combo>
</div>
</ng-container>
行が初めて展開されると、getOrders(customerId) メソッドは HTTP 呼び出しを行い、監視対象値を返します。非同期パイプを使用しているため、Angularは自動的にサブスクライブし、データが到着するとコンボをレンダリングします。
ここで非常に重要な注意点は、ng-templateがある場合、Angularフレームごとにこのテンプレートにデータをロードし続けることです(変更検出)。このデータがリクエストからロードされる場合、これは、アプリケーションが必然的にフリーズするまで、表示されているテンプレートごとに毎秒いくつかのリクエストが行われることを意味します。これが、フェッチ後にデータをキャッシュする必要がある理由です。
private ordersCache = new Map<string, Observable<OrderDto[]>>();
getOrders(customerId: string): Observable<OrderDto[]> {
if (!this.ordersCache.has(customerId)) {
const request$ = this.northwindSwaggerService
.getOrderDtoList(customerId)
.pipe(take(1), shareReplay(1));
this.ordersCache.set(customerId, request$);
}
return this.ordersCache.get(customerId)!;
}

ここではいくつかのことが起こっています。
- observable を customerId でキー設定された Map にキャッシュします。これにより、行のデータがフェッチされると、新しいHTTP呼び出しをトリガーする代わりに、同じオブザーバブルが再利用されます。
- take(1) は、最初の応答の後にオブザーバブルが完了することを保証し、サブスクリプション管理をクリーンにし、メモリリークを回避します。
- shareReplay(1) は、複数のサブスクライバー (または変更検出サイクル) が同じデータを要求した場合、1 つの HTTP 要求のみが送信されるようにします。また、結果を遅いサブスクライバーに再生します (たとえば、行が折りたたまれて再び展開されたとき)。
- 次に、データをordersCacheに格納して返します
キャッシュとshareReplayを組み合わせることで、各顧客行は、何回拡張されたか、変更検出を実行する頻度に関係なく、最大1つのリクエストAngularトリガーされるようにします。
選択した注文の詳細の照会
ここまで、顧客行を展開し、オンデマンドで注文のリストを取得する方法を示しました。次に、ユーザーがそのリストから特定の注文を選択し、その詳細を行のすぐ下に表示してもらいます。
- コンボに選択イベントを追加する
コンボボックスを拡張して、新しい注文が選択されるたびに通知します。これは、selectionChanging イベントを処理することによって行われます。
<igx-simple-combo
type="border"
[data]="orders"
displayKey="orderId"
(selectionChanging)="onOrderSelectionChange(rowData.customerId, $event.newValue)"
class="single-select-combo"
>
<label igxLabel>Order Id</label>
</igx-simple-combo>
- 顧客ごとに選択した注文を追跡する
Map<string OrderDto> を使用して、customerId をキーとする選択を追跡します。これにより、展開された各行は、選択した順序を確実に記憶します。
public selectedOrders = new Map<string, OrderDto>();
onOrderSelectionChange(customerId: string, order: OrderDto) {
this.selectedOrders.set(customerId, order);
}
getSelectedOrder(customerId: string): OrderDto | undefined {
return this.selectedOrders.get(customerId);
}
- Show order details
最後に、注文を選択したら、展開されたテンプレートでその詳細をレンダリングできます。getSelectedOrder(customerId) を呼び出すと、保存された選択範囲が取得され、そのフィールドが表示されます。
<div *ngIf="getSelectedOrder(rowData.customerId) as selectedOrder" class="column-layout group_1">
<h6 class="h6">Order Details</h6>
<div class="row-layout group_2">
<div class="column-layout group_3">
<div class="row-layout group_4"><p class="text">Completed</p></div>
<div class="row-layout group_4"><p class="text">Shipper</p></div>
<div class="row-layout group_4"><p class="text">Order Date</p></div>
<div class="row-layout group_4"><p class="text">Country</p></div>
<div class="row-layout group_4"><p class="text">City</p></div>
<div class="row-layout group_4"><p class="text">Street</p></div>
<div class="row-layout group_5"><p class="text">Postal Code</p></div>
</div>
<div class="column-layout group_6">
<div class="row-layout group_7"><igx-checkbox [checked]="!!selectedOrder?.completed" class="checkbox"></igx-checkbox></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipperId }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.orderDate }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.country }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.city }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.street }}</p></div>
<div class="row-layout group_5"><p class="text_1">{{ selectedOrder?.shipAddress?.postalCode }}</p></div>
</div>
</div>
</div>
</div>
これにより、シンプルかつ機能的な詳細ビューが得られます:ユーザーが顧客行を展開すると、コンボから注文を選択でき、注文の詳細がすぐ下に表示されます。

Cache Invalidation & Refresh Strategies
有効期限が切れないキャッシュは、古くなったり、無制限に成長したりする可能性があります。アプリに適した戦略を実装することを選択できます。
Manual Refresh
キャッシュエントリを置き換える refreshOrders(customerId) を呼び出す詳細テンプレートに [更新] ボタンを追加します。
refreshOrders(customerId: string) {
const request$ = this.northwindSwaggerService.getOrderDtoList(customerId).pipe(take(1), shareReplay(1));
this.ordersCache.set(customerId, request$);
}
折りたたむとクリア
行が折りたたまれたときに、キャッシュされた監視対象をクリアします。これにより、次回ユーザーが展開したときに新しいリクエストが確実に行われます。
<igx-grid (rowToggle)="onRowToggle($event)"...>
onRowToggle(event: IRowToggleEventArgs) {
if (this.ordersCache.size > 0 && this.ordersCache.has(event.rowID)) {
this.ordersCache.delete(event.rowID);
}
}
マスターグリッドを設定し、オンデマンドの詳細読み込みを実装し、冗長なリクエストを回避するために結果をキャッシュし、更新戦略についても説明しました。
次に、このパターンをより大きなデータセットや運用シナリオにスケーリングするのに役立つ追加の考慮事項とベスト プラクティスを見てみましょう。
Handling Large Detail Data
詳細データセット自体が大きい場合、すべてを一度に取得してレンダリングすると、パフォーマンスが低下する可能性があります。代わりに、次のいずれかの戦略を使用します。
- クライアント側またはサーバー側のページング: 注文 API に page パラメータと pageSize パラメータを渡すことで、一度にデータのスライスのみを取得します。
- Load-on-Demand: 現時点で必要なデータのみをフェッチします
メモリとライフサイクルに関する考慮事項
- メモリリークを回避するためにngOnDestroy()のキャッシュをクリアします:this.ordersCache.clear()。
- キャッシュされたデータ (オブザーバブルだけでなく) を保持する場合は、合計メモリを制限することを検討してください。
- Avoid storing large, nested object Maps indefinitely.
プリロードされた詳細との比較
| Approach | ベスト | 欠点 |
|---|---|---|
| Preloaded details | 小さなデータセット、簡単なデモ、クイックプロトタイプ | 初期負荷が遅くなり、メモリ使用量が増加する |
| On-demand loading | 大規模/複雑なデータセットと運用アプリ | コードの複雑さが若干高く、非同期処理が必要 |
結論
オンデマンド データ読み込みは、マスター/ディテール グリッド レイアウトの自然な拡張です。パフォーマンスが向上し、大規模なデータセットでシームレスに拡張され、スムーズなユーザー エクスペリエンスが保証されます。
このパターンを初めて使用する場合は、まず「グリッドを使用したマスター/詳細レイアウトの開始Ignite UI for Angular」の記事を確認してください。次に、独自のアプリにオンデマンド読み込みを適用して、データグリッドを次のレベルに引き上げます。
Ignite UI for Angularを使用すると、データが増加しても高速性を維持する、応答性の高いデータ駆動型アプリケーションを構築できます。
完全なLoad-on-demand アプリのサンプルを確認してください。