
Introdução ao layout mestre-detalhe usando Ignite UI for Angular grade
O layout mestre-detalhe é um padrão de interface do usuário comprovado que fornece uma maneira direta, escalonável e fácil de apresentar dados relacionados sem sobrecarregar a tela. Saiba mais neste artigo.
Ao criar aplicativos corporativos (como CRMs, ERPs e painéis de administração), é comum trabalhar com conjuntos de dados relacionais em que um registro está vinculado a vários registros relacionados. Por exemplo, um pedido pode ter vários itens, um cliente pode ter várias transações ou um departamento pode ter vários funcionários.
O Layout Mestre-Detalhes é um padrão de interface do usuário comprovado para esses cenários. Ele fornece uma maneira direta, escalável e fácil de apresentar dados relacionados sem sobrecarregar a tela.
Neste artigo, vamos orientá-lo na criação de uma interface mestre-detalhe limpa e eficiente usando Ignite UI for Angular. Exploraremos seu poderoso componente Grid e seu suporte integrado para modelos aninhados usando igxGridDetail, ajudando você a exibir dados hierárquicos com configuração mínima e alto desempenho.
O que é um layout mestre-detalhe?
Um layout mestre-detalhe (também conhecido como linha-pai ou expansível) é um padrão de design em que uma lista de nível superior (mestre) permite que os usuários expandam itens para revelar informações relacionadas (detalhe). Exemplos comuns incluem:
- Ordens (mestre) com Itens da ordem (detalhe)
- Usuários (mestre) com registros de perfil (detalhe)
- Departamentos (mestre) com Funcionários (detalhe)
Esse padrão melhora a usabilidade reduzindo a desordem na tela e fornecendo acesso a dados mais profundos quando necessário.
Configurando o projeto Angular
Antes de mergulhar na implementação mestre-detalhe, vamos configurar um espaço de trabalho Angular limpo para nossa demonstração.
1. Crie um novo projeto Angular
Se você ainda não tiver um espaço de trabalho Angular pronto, crie um usando o Angular CLI. Abra este projeto em seu IDE ou editor preferido para começar.
ng new master-detail-grid
2. Add Ignite UI for Angular
Ignite UI for Angular oferece componentes de interface do usuário poderosos, incluindo o igxGrid que usaremos para o layout de detalhes mestre. Veja como adicioná-lo.
ng add igniteui-angular
3. Gere um componente de demonstração dedicado
Para manter seu exemplo de detalhe mestre modular, gere um componente Angular dedicado.
ng generate component pages/grid-demo
4. Configure o roteamento para a demonstração
Para simplificar a navegação, substitua o app.routes.ts existente (ou o módulo de roteamento) por uma configuração mínima que redirecione para o componente de demonstração por padrão:
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' } ];
Configurando os dados
Antes de criarmos o layout mestre-detalhe, é essencial preparar seus dados. Isso garante que seus componentes de grade tenham conteúdo significativo para exibir e possam buscar com eficiência dados detalhados relacionados à medida que os usuários interagem com a interface do usuário.
Você pode usar qualquer fonte de dados com Ignite UI for Angular:
- Dados estáticos/locais – de um arquivo JSON na pasta de ativos.
- Dados de API remota – buscados por meio do HttpClient do Angular de um serviço de back-end.
- Dados simulados – para desenvolvimento, gerados no próprio serviço.
Neste exemplo, vamos integrar com uma API personalizada do Northwind Swagger. Os conceitos permanecem os mesmos, independentemente da estrutura da fonte de dados.
1. Define Your Models
Para impor a segurança e a clareza de tipo, defina interfaces TypeScript que representam as formas de dados em um arquivo models.ts dedicado.
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
Centralize toda a lógica de busca de dados em um serviço Angular, por exemplo, northwind-swagger.service.ts. Aqui está um serviço de exemplo que interage com uma API Northwind personalizada, que usaremos neste exemplo, e inclui tratamento básico de erros:
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); }; } }
Com seus modelos e serviços preparados, os componentes da grade agora podem se vincular diretamente a matrizes de pedidos por cliente selecionado. A partir daqui, passaremos a projetar a grade mestre e adicionaremos progressivamente exibições de detalhes, modelos personalizados e otimizações de desempenho.
Projetando a grade mestra
Agora que nosso serviço de dados está pronto, é hora de criar a interface do usuário mestre-detalhe passo a passo. Começaremos selecionando um cliente, exibindo seus pedidos em uma grade mestra e, em seguida, aprimorando a grade com um modelo de detalhes personalizável.
1. Adicionando um combo para clientes selecionados
Para permitir que os usuários escolham um cliente, usaremos o componente igx-simple-combo que carrega de forma assíncrona os dados do cliente do serviço.
- Quando um usuário seleciona um cliente, atualizamos uma variável local localCustomerId no componente.
- Isso aciona a busca dos pedidos correspondentes para esse cliente.
<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. Adicionando a grade mestra para pedidos
Depois que um cliente é selecionado, exibimos seus pedidos usando uma grade igx. A fonte de dados da grade é northwindSwaggerOrderWithDetailsDto, atualizada sempre que localCustomerId é alterado.
<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. Adicionando um modelo de detalhe personalizado
Para tornar a grade mestra mais útil, adicionamos um modelo de detalhe para cada pedido. Isso pode conter detalhes do produto, totais ou qualquer informação relacionada. No nosso caso, vamos usá-lo para exibir algumas das colunas da grade para endereço e, adicionalmente, detalhes do pedido em uma grade.
Para configurar o igxGrid para ser exibido no modo master-detail, você precisa especificar um modelo dentro da grade, marcado com a diretiva igxGridDetail:
<ng-template igxGridDetail> </ng-template>
Você pode personalizar este modelo de detalhes com qualquer componente que atenda às suas necessidades de negócios: texto, grupos de entrada, grades aninhadas ou gráficos. Aqui usaremos textos e uma grade.
<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
O contexto do modelo são os dados do registro mestre, de modo que os valores do registro mestre possam ser exibidos no modelo detalhado.
- O contexto é o registro mestre ao qual o detalhe pertence.
- Declara-se uma variável de contexto do modelo usando a sintaxe let-, que permite acessar os dados do registro mestre dentro do modelo detalhado.
- Em nosso exemplo, usamos let-rowData para nomear essa variável de contexto.
Isso significa que, dentro do modelo de detalhe, você pode acessar qualquer propriedade do registro mestre por meio de rowData. Por exemplo, rowData.shipAddress.country e rowData.orderDetails acessam o ID do pedido a partir do registro da grade mestra.
<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>
Carregamento Dinâmico Simples
Em muitos casos, você não deseja carregar todos os dados detalhados antecipadamente. Em vez disso, você pode buscá-lo sob demanda quando uma linha é expandida.
Às vezes, os dados detalhados vêm de um ponto final completamente diferente e requerem um parâmetro do registro mestre (por exemplo, o ID de uma ordem). Nesses casos, podemos carregar os detalhes sob demanda quando a linha é expandida.
Nessa abordagem, toda vez que uma linha é expandida, solicitamos os dados detalhados da API usando o ID do registro mestre.
<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) ); }
Embora simples, essa abordagem dispara uma nova chamada de API constantemente depois que ela se torna visível. Isso pode tornar o aplicativo lento e até mesmo fazer com que ele trave.
Dicas de otimização de desempenho
Buscar repetidamente os mesmos dados detalhados é caro tanto no desempenho quanto no uso da rede. Uma solução simples e eficaz é armazenar os dados em cache depois que eles forem carregados pela primeira vez.
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)!; }
Como funciona?
- Na primeira expansão de uma linha, getOrderDetails() chama a API e armazena o observável resultante em orderDetailsCache.
- Esse método será chamado constantemente, mas como o método retorna o observável armazenado em cache, ele impede solicitações duplicadas.
- O operador shareReplay(1) garante que, mesmo que os assinantes se conectem após a conclusão da chamada à API, eles recebam os dados armazenados em cache imediatamente sem acionar uma nova solicitação.
Isso é fácil de implementar e reduz significativamente o número de solicitações.
Embrulhar...
A criação de exibições de detalhes mestre no Angular não precisa ser complexa ou com uso intensivo de desempenho. Com o Ignite UI for Angular, você obtém uma solução poderosa e elegante que requer configuração mínima, mas oferece total flexibilidade e personalização para atender às necessidades exclusivas de sua aplicação.
Esteja você criando painéis de administração, sistemas de gerenciamento ou interfaces orientadas por dados, a grade de Angular do Ignite UI oferece tudo o que você precisa para desenvolver aplicativos rápidos, responsivos e de fácil manutenção.
Você pode conferir nossos exemplos do Ignite UI for Angular Grid e explorar todos os componentes e recursos usados. O aplicativo Gerenciamento de Frota demonstra o uso do Master-Detail Grid, para que você possa ver como os dados são apresentados e gerenciados em cenários do mundo real.