グリッドを使用したマスター/詳細レイアウトの入門Ignite UI for Angular
マスター/ディテール レイアウトは、画面を圧倒することなく関連データを表示するための簡単でスケーラブルでユーザーフレンドリーな方法を提供する、実績のある UI パターンです。詳細については、こちらの記事をご覧ください。
エンタープライズアプリケーション (CRM、ERP、管理ダッシュボードなど) を構築する場合、1 つのレコードが複数の関連レコードにリンクされているリレーショナルデータセットを使用するのが一般的です。たとえば、注文にさまざまな品目がある、顧客に複数のトランザクションがある、または部門に多数の従業員がいる場合があります。
マスター/詳細レイアウトは、これらのシナリオで実証済みの UI パターンです。画面を圧倒することなく、関連データを表示するための簡単でスケーラブルでユーザーフレンドリーな方法を提供します。
この記事では、Ignite UI for Angularを使用してクリーンで効率的なマスターディテールインターフェイスを構築する方法を説明します。強力なグリッド コンポーネントと、igxGridDetail を使用したネストされたテンプレートの組み込みサポートを検討し、最小限のセットアップと高いパフォーマンスで階層データを表示できるようにします。
マスター/ディテールレイアウトとは
マスター/詳細レイアウト (親子行または展開可能な行とも呼ばれます) は、トップレベル リスト (マスター) を使用して、ユーザーがアイテムを展開して関連情報 (詳細) を表示できるデザイン パターンです。一般的な例は次のとおりです。
- オーダ明細 (詳細) を含むオーダ (マスタ)
- プロファイルログを持つユーザー(マスター)(詳細)
- 従業員 (詳細) を持つ部門 (マスター)
このパターンは、画面の乱雑さを軽減し、必要に応じてより深いデータへのアクセスを提供することで使いやすさを向上させます。
Angularプロジェクトの設定
マスターディテールの実装に入る前に、デモ用にクリーンなAngularワークスペースを設定しましょう。
1. 新しいAngularプロジェクトを作成する
Angularワークスペースをまだ用意していない場合は、Angular CLIを使用して作成します。このプロジェクトを任意の IDE またはエディターで開いて開始します。
ng new master-detail-grid
2. Add Ignite UI for Angular
Ignite UI for Angularは、マスター/ディテール レイアウトに使用する igxGrid など、強力な UI コンポーネントを提供します。追加方法は次のとおりです。
ng add igniteui-angular
3. 専用のデモコンポーネントを生成する
マスター/詳細の例をモジュール式に保つには、専用のAngularコンポーネントを生成します。
ng generate component pages/grid-demo
4. デモのルーティングを設定する
ナビゲーションを簡素化するには、既存の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' }
];
データの設定
マスター/詳細レイアウトを作成する前に、データを準備することが重要です。これにより、グリッド コンポーネントに意味のあるコンテンツを表示し、ユーザーが UI を操作するときに関連する詳細データを効率的に取得できるようになります。
次のデータソースは、次のとおりに使用できますIgnite UI for Angular
- 静的/ローカルデータ – アセットフォルダ内のJSONファイルから。
- リモートAPIデータ – バックエンドサービスからAngularのHttpClientを介して取得されます。
- モックされたデータ – 開発用で、サービス自体で生成されます。
この例では、カスタム Northwind Swagger API と統合します。概念は、データソース構造に関係なく同じままです。
1. Define Your Models
タイプセーフと明確さを強制するには、専用のmodels.tsファイル内のデータシェイプを表すTypeScriptインターフェイスを定義します。
export interface Customer {
customerId: string;
companyName: string;
contactName: string;
country: string;
}
export interface Order {
orderId: number;
customerId: string;
orderDate: string;
shipAddress: string;
freight: number;
}
export interface OrderDetail {
orderId: number;
productId: number;
quantity: number;
unitPrice: number;
}
2. Create a Data Service
すべてのデータフェッチロジックをAngularサービス(northwind-swagger.service.tsなど)に一元化します。この例で使用するカスタム Northwind API と対話し、基本的なエラー処理を含むサンプル サービスを次に示します。
const API_ENDPOINT = 'https://data-northwind.indigo.design';
@Injectable({
providedIn: 'root'
})
export class NorthwindSwaggerService {
constructor(private http: HttpClient) {}
public getCustomerDto(id: string): Observable<Customer | undefined> {
return this.http.get<Customer | undefined>(`${API_ENDPOINT}/Customers/${id}`)
.pipe(catchError(this.handleError<Customer | undefined>('getCustomerDto', undefined)));
}
public getCustomerDtoList(): Observable<Customer[]> {
return this.http.get<Customer[]>(`${API_ENDPOINT}/Customers`)
.pipe(catchError(this.handleError<Customer[]>('getCustomerDtoList', [])));
}
public getOrderWithDetailsDtoList(id: string): Observable<Order[]> {
return this.http.get<Order[]>(`${API_ENDPOINT}/Customers/${id}/Orders/WithDetails`)
.pipe(catchError(this.handleError<Order[]>('getOrderWithDetailsDtoList', [])));
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(`${operation} failed: ${error.message}`, error);
return of(result as T);
};
}
}
モデルとサービスが準備されたら、グリッドコンポーネントは、選択した顧客ごとの注文の配列に直接バインドできます。ここからは、マスター グリッドの設計に進み、詳細ビュー、カスタム テンプレート、パフォーマンスの最適化を段階的に追加します。
マスターグリッドの設計
データ サービスの準備ができたので、次は主従 UI を段階的に構築します。まず、顧客を選択し、注文をマスター グリッドに表示し、カスタマイズ可能な詳細テンプレートでグリッドを強化します。
1. 一部の顧客にコンボを追加する
ユーザーが顧客を選択できるようにするために、サービスから顧客データを非同期的に読み込む igx-simple-combo コンポーネントを使用します。
- ユーザーが顧客を選択すると、コンポーネント内のローカル変数 localCustomerId が更新されます。
- これにより、その顧客に対応する注文の取得がトリガーされます。
<igx-simple-combo
type="border"
[data]="northwindSwaggerCustomerDto"
displayKey="customerId"
(selectionChanging)="localCustomerId = $event.newValue.customerId"
class="single-select-combo">
</igx-simple-combo>
public northwindSwaggerCustomerDto: CustomerDto[] = [];
private _localCustomerId?: string;
public get localCustomerId(): string | undefined {
return this._localCustomerId;
}
public set localCustomerId(value: string | undefined) {
this._localCustomerId = value;
this.selectedCustomer$.next();
this.northwindSwaggerOrderWithDetailsDto$.next();
}
ngOnInit() {
this.northwindSwaggerService.getCustomerDtoList().pipe(takeUntil(this.destroy$)).subscribe(
data => this.northwindSwaggerCustomerDto = data
);
}

2. 注文のマスターグリッドの追加
顧客が選択されると、igx-gridを使用して注文を表示します。グリッドのデータ ソースは northwindSwaggerOrderWithDetailsDto で、localCustomerId が変更されるたびに更新されます。
<igx-grid [data]="northwindSwaggerOrderWithDetailsDto" primaryKey="orderId" rowSelection="single" [hideRowSelectors]="true" [allowFiltering]="true" filterMode="excelStyleFilter" (rowSelectionChanging)="selectedOrder = $event.newSelection[0]" class="grid">
<igx-column field="orderId" oldDataType="number" header="orderId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="customerId" oldDataType="string" header="customerId" [filterable]="true" [sortable]="true" required="true" [minlength]="1" [selectable]="false"></igx-column>
<igx-column field="employeeId" oldDataType="number" header="employeeId" [filterable]="true" [sortable]="true" [min]="1" [max]="2147483647" [selectable]="false"></igx-column>
<igx-column field="shipperId" oldDataType="number" header="shipperId" [filterable]="true" [sortable]="true" [min]="1" [max]="2147483647" [selectable]="false"></igx-column>
<igx-column field="orderDate" oldDataType="date" header="orderDate" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="requiredDate" oldDataType="date" header="requiredDate" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="shipVia" oldDataType="string" header="shipVia" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="freight" oldDataType="number" header="freight" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="shipName" oldDataType="string" header="shipName" [filterable]="true" [sortable]="true" [maxlength]="100" [selectable]="false"></igx-column>
<igx-column field="completed" oldDataType="boolean" header="completed" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
</igx-grid>
private _selectedCustomer?: CustomerDto;
public get selectedCustomer(): CustomerDto | undefined {
return this._selectedCustomer;
}
public set selectedCustomer(value: CustomerDto | undefined) {
this._selectedCustomer = value;
this.selectedOrder = undefined;
}
public selectedCustomer$: Subject<void> = new Subject<void>();
public northwindSwaggerOrderWithDetailsDto: OrderWithDetailsDto[] = [];
public northwindSwaggerOrderWithDetailsDto$: Subject<void> = new Subject<void>();
public selectedOrder?: OrderWithDetailsDto;
ngOnInit() {
this.northwindSwaggerService.getOrderWithDetailsDtoList(this.localCustomerId ?? '').pipe(takeUntil(this.destroy$)).subscribe(
data => this.northwindSwaggerOrderWithDetailsDto = data
);
this.northwindSwaggerOrderWithDetailsDto$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.northwindSwaggerService.getOrderWithDetailsDtoList(this.localCustomerId ?? '').pipe(take(1)).subscribe(
data => this.northwindSwaggerOrderWithDetailsDto = data
);
});
}

3. カスタム詳細テンプレートの追加
マスターグリッドをより便利にするために、注文ごとに詳細テンプレートを追加します。これには、製品の詳細、合計、または関連情報が含まれる場合があります。この場合、これを使用して、グリッドの住所とさらに注文の詳細の列の一部をグリッドに表示します。
igxGrid をマスター/詳細モードで表示するように構成するには、グリッド内に igxGridDetail ディレクティブでマークされたテンプレートを指定する必要があります。
<ng-template igxGridDetail> </ng-template>
この詳細テンプレートは、ビジネス ニーズに合ったコンポーネント (テキスト、入力グループ、ネストされたグリッド、グラフ) を使用してカスタマイズできます。ここでは、テキストとグリッドを使用します。
<ng-template igxGridDetail let-rowData>
<div class="row-layout group_3">
<div class="row-layout group_4">
<div class="column-layout group_5">
<p class="ig-typography__subtitle-1 text">
Country
</p>
<p class="ig-typography__subtitle-1 text">
Code
</p>
<p class="ig-typography__subtitle-1 text">
City
</p>
<p class="ig-typography__subtitle-1 text">
Street
</p>
<p class="ig-typography__subtitle-1 text">
Phone
</p>
</div>
<div class="column-layout group_6">
<p class="ig-typography__subtitle-1 text">
{{ rowData.shipAddress.country }}
</p>
<p class="ig-typography__subtitle-1 text">
{{ rowData.shipAddress.postalCode }}
</p>
<p class="ig-typography__subtitle-1 text">
{{ rowData.shipAddress.city }}
</p>
<p class="ig-typography__subtitle-1 text">
{{ rowData.shipAddress.street }}
</p>
<p class="ig-typography__subtitle-1 text">
{{ rowData.shipAddress.phone }}
</p>
</div>
</div>
<div class="column-layout group_7">
<p class="text">
Order Details
</p>
<igx-grid primaryKey="orderId" [allowFiltering]="true" filterMode="excelStyleFilter" [data]="rowData.orderDetails" class="grid_1">
<igx-column field="orderId" oldDataType="number" header="orderId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="productId" oldDataType="number" header="productId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="unitPrice" oldDataType="number" header="unitPrice" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="quantity" oldDataType="number" header="quantity" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<igx-column field="discount" oldDataType="number" header="discount" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
</igx-grid>
</div>
</div>
</ng-template>

Handling Data Binding
テンプレートのコンテキストはマスタレコードデータであるため、マスタレコードの値を詳細テンプレートに照会することができます。
- コンテキストは、詳細が属するマスタレコードです。
- テンプレートコンテキスト変数は、let- 構文を使用して宣言し、詳細テンプレート内のマスタレコードのデータにアクセスすることができます。
- この例では、let-rowData を使用してこのコンテキスト変数に名前を付けます。
つまり、詳細テンプレート内では、rowData を使用してマスタレコードの任意のプロパティにアクセスできます。たとえば、rowData.shipAddress.country と rowData.orderDetails は、マスターグリッドのレコードから注文の ID にアクセスします。
<ng-template igxGridDetail let-rowData>
<div class="column-layout group_6">
<p class="ig-typography__subtitle-1 text">
{{ rowData.shipAddress.country }}
</p>
<div class="column-layout group_7">
<igx-grid primaryKey="orderId" [allowFiltering]="true" filterMode="excelStyleFilter" [data]="rowData.orderDetails" class="grid_1">
/igx-grid>
</div>
</div>
</ng-template>
単純な動的荷重
多くの場合、すべての詳細データを前もって読み込む必要はありません。代わりに、行が展開されたときにオンデマンドでフェッチできます。
詳細データがまったく異なるエンドポイントから取得され、マスタレコードのパラメータ (指図の ID など) が必要になる場合があります。このような場合、行が展開されたときにオンデマンドで詳細を読み込むことができます。
この方法では、行が展開されるたびに、マスターレコードの ID を使用して API から詳細データを要求します。
<igx-grid [data]="northwindSwaggerOrderDto" primaryKey="orderId" [allowFiltering]="true" filterMode="excelStyleFilter" class="grid" (rowToggle)="onRowToggle($event)">
<igx-column field="orderId" dataType="number" header="orderId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column>
<ng-template igxGridDetail let-rowData>
<ng-container *ngIf="getOrderDetails(rowData.orderId) | async as details">
@for (item of details; track item) {
<igx-input-group type="border" class="input">
<input type="text" [(ngModel)]="item.orderId" igxInput />
</igx-input-group>
}
</ng-container>
</ng-template>
</igx-grid>
getOrderDetails(orderId: number): Observable<any[]> {
return this.northwindSwaggerService
.getOrderDetailDtoList(orderId)
.pipe(
take(1)
);
}
このアプローチは簡単ですが、新しい API 呼び出しが表示された後も常にトリガーされます。これにより、アプリケーションの速度が低下し、クラッシュする可能性さえあります。
パフォーマンス最適化のヒント
同じ詳細データを繰り返しフェッチすると、パフォーマンスとネットワーク使用量の両方にコストがかかります。シンプルで効果的な解決策は、初めて読み込まれた後にデータをキャッシュすることです。
private orderDetailsCache = new Map<number, Observable<any[]>>();
getOrderDetails(orderId: number): Observable<any[]> {
if (!this.orderDetailsCache.has(orderId)) {
const request$ = this.northwindSwaggerService
.getOrderDetailDtoList(orderId)
.pipe(
take(1),
shareReplay(1),
);
this.orderDetailsCache.set(orderId, request$);
}
return this.orderDetailsCache.get(orderId)!;
}
それはどのように機能しますか?
- 行の最初の展開時に、getOrderDetails()はAPIを呼び出し、結果のオブザーバブルをorderDetailsCacheに格納します。
- このメソッドは絶えず呼び出されますが、キャッシュされたオブザーバブルを返すため、重複したリクエストが防止されます。
- shareReplay(1) 演算子は、API 呼び出しの完了後にサブスクライバーがアタッチした場合でも、新しいリクエストをトリガーすることなく、キャッシュされたデータをすぐに受信できるようにします。
これは実装が簡単で、リクエストの数を大幅に減らします。
まとめ...
Angularでのマスター/詳細ビューの構築は、複雑であったり、パフォーマンスを集中的に消費したりする必要はありません。Ignite UI for Angularを使用すると、最小限のセットアップで済みながら、アプリケーション固有のニーズに合わせて完全な柔軟性とカスタマイズを提供する、強力でエレガントなソリューションが得られます。
管理ダッシュボード、管理システム、データ駆動型インターフェイスのいずれを構築する場合でも、Ignite UI のAngularグリッドは、高速で応答性が高く、保守しやすいアプリケーションを開発するために必要なすべてを提供します。
Ignite UI for Angular Grid のサンプルをチェックして、使用されているすべてのコンポーネントと機能を調べることができます。フリート管理アプリは、マスター/詳細グリッドの使用方法を示しているため、実際のシナリオでデータがどのように表示および管理されるかを確認できます。