Ir para o conteúdo
Carregamento de Dados Sob Demanda em Layouts de Grade de Detalhe Mestre

Carregamento de Dados Sob Demanda em Layouts de Grade de Detalhe Mestre

O carregamento de dados sob demanda é uma técnica poderosa para dimensionar interfaces mestre-detalhe. Em vez de buscar todos os detalhes antecipadamente, você carrega os registros relacionados somente quando o usuário precisa deles. Saiba mais neste artigo.

16min de leitura

No nosso artigo anterior, Começando com o Layout Master-Detail Usando Ignite UI for Angular Grade, exploramos como configurar uma interface mestre-detalhe limpa e eficiente. Esse padrão é ideal para apresentar registros relacionados, como pedidos com itens ou departamentos com funcionários, sem sobrecarregar a tela.

Mas o que acontece quando seu conjunto de dados é enorme? Carregar todos os detalhes de todos os registros de uma vez é ineficiente. É aí que entra a carga de dados sob demanda. Em vez de buscar todos os detalhes possíveis no início, você só carrega os dados quando o usuário expande uma linha, resultando em renderizações iniciais mais rápidas, interações mais suaves e maior escalabilidade.

Neste artigo, vamos explorar por que o carregamento sob demanda é importante, como implementá-lo no componente Ignite UI for Angular Grid e as melhores práticas para aproveitá-lo ao máximo.

Estendendo o Modelo de Detalhe Mestre

Se você acompanhou nosso primeiro artigo, já sabe como configurar uma grade mestra com linhas expansíveis e um modelo de detalhes, e de onde exibir dados:

  • O mesmo conjunto de dados – Por exemplo, pedidos e seus itens associados são agrupados
  • Um conjunto de dados externo – por exemplo, um serviço que aceita o customerID como parâmetro e chama a API para recuperar dados de pedidos de clientes.

Para grandes conjuntos de dados, a segunda abordagem (obtenção externa de detalhes) é onde o carregamento sob demanda se mostra mais eficaz.

Configurando o projeto Angular

Antes de começarmos a implementar o carregamento sob demanda, vamos preparar um Angular workspace para a demonstração. Aqui estão os passos.

  1. Crie um novo projeto Angular 

Se você ainda não tem um, comece gerando uma nova aplicação Angular usando a CLI Angular e abra o projeto no seu IDE preferido.

ng new load-on-demand-grid 
cd load-on-demand-grid 
  1. Install Ignite UI for Angular 

Vamos usar a biblioteca de componentes Ignite UI for Angular, que oferece um conjunto rico de componentes de interface – principalmente o igxGrid, a base para nossos exemplos de master-detail e carregamento sob demanda. Adicione ao seu projeto com:

ng add igniteui-angular 
  1. Create a demo component 

Para manter tudo organizado, gere um componente dedicado Angular onde construiremos a demonstração em grade:

ng generate component pages/grid-demo 
  1. Configurar roteamento para a demonstração 

Por fim, ajuste sua configuração de roteamento para que o componente demo seja a visualização padrão. Substitua seu app.routes.ts existente (ou seu módulo de roteamento) por uma configuração mínima.

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' }  
]; 

Adicione o provideHttpClient aos provedores em app.config.ts.

export const appConfig: ApplicationConfig = { 
    providers: [
	provideZoneChangeDetection({ eventCoalescing: true }),
	provideRouter(routes),
	provideAnimations(),
	provideHttpClient()
    ]
}; 

Configurando os dados

Antes de implementar carregamento sob demanda na grade mestre-detalhe, precisamos de uma camada de dados confiável. Preparar seus modelos de dados e serviços garante que a grade exiba conteúdo significativo e possa buscar detalhes relacionados à medida que os usuários interagem com as linhas.

Seus dados para o IgxGrid podem vir de várias fontes:

  • Dados estáticos/locais – Arquivos JSON na pasta de assets.
  • APIs remotas – obtidas via HttpClient do Angular a partir de um serviço backend.
  • Dados simulados – gerados diretamente em um serviço durante o desenvolvimento.

Para este exemplo, vamos nos conectar a uma API remota personalizada do Northwind Swagger, que fornece aos clientes e seus pedidos relacionados. Os conceitos permanecem os mesmos independentemente da estrutura da sua fonte de dados.

Define Data Models 

Para garantir a segurança e clareza dos tipos, defina interfaces TypeScript que representem seus dados em um arquivo dedicado de models.ts. Esses modelos refletem a estrutura das respostas da API e facilitam o vinculamento de dados à grade.

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 

Em seguida, centralize todas as chamadas de API em um serviço dedicado – por exemplo, northwind-swagger.service.ts. Também adicionaremos o gerenciamento básico de erros para manter o app resiliente.

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 

Com os modelos e o serviço implementados, o próximo passo é exibir o conjunto de dados de Clientes na grade. A propriedade de dados da grade é vinculada a um array northwindSwaggerCustomerDto, que é preenchido quando o componente inicializa ao buscar dados do serviço.

<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>

No lado do TypeScript, adicione uma assinatura de serviço na classe GridDemoComponent para recuperar os dados do cliente e armazená-los na propriedade northwindSwaggerCustomerDto. Depois, vincule a propriedade de entrada de dados da grade para '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();
  }
}
Carregamento de dados Angular On-Demand

Desenhando o Modelo de Detalhes e Implementando o Carregamento sob Demanda

Quando a grade mestre apresenta muitas linhas pais (clientes), é ineficiente buscar todas as listas filhas relacionadas (ordens) para cada linha inicial. Em vez disso, carregue os detalhes apenas quando o usuário expandir uma linha. Essa é a essência do carregamento sob demanda: solicitar dados filhos apenas quando necessário, e depois mostrá-los no modelo de detalhes.

Como funciona?

  • Coloque um template igxGridDetail dentro do mestre igx-grade.
  • Nesse modelo, chame um método (por exemplo, getOrders(customerId)) que retorne um Observable<OrderDto[]>.
  • Use o pipe assíncrono para assinar esse observável no template, assim Angular renderize os resultados assim que estiver disponível.
  • Armazene em cache o observável/resultado de cada cliente para que múltiplas expansões (ou ciclos rápidos de detecção de mudanças) não acionem novas chamadas HTTP.

Como implementamos isso?

  1. Adicionar o modelo de detalhes 

Primeiro, definimos um marcador para o conteúdo detalhado adicionando um template ng com a diretiva igxGridDetail. Isso marca onde a visualização de detalhes de cada linha expandida será renderizada:

<ng-template igxGridDetail let-rowData></ng-template> 
  1. Adicionar um combo vinculado a pedidos (carregados sob demanda) 

Dentro do template, podemos renderizar qualquer componente que quisermos. Neste exemplo, vamos adicionar uma caixa de combo para exibir os pedidos do cliente selecionado. O ponto chave aqui é que as ordens não são carregadas até que a linha seja expandida.

 <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>

Na primeira vez que uma linha é expandida, o método getOrders(customerId) faz uma chamada HTTP e retorna um observável. Por usar o pipe assíncrono, Angular assina automaticamente e renderiza o combo assim que os dados chegam.

Uma observação muito importante aqui é que, quando temos um template ng, Angular continuará carregando dados nesse template a cada frame (detecção de mudança). Se esses dados forem carregados a partir de uma requisição, isso significa que para cada modelo visível, poucas requisições serão feitas a cada segundo até que a aplicação inevitavelmente trave. É por isso que os dados precisam ser armazenados em cache após a busca.

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)!; 
  }
Carregamento de Dados sob demanda com a interface do ignite

Algumas coisas estão acontecendo aqui:

  • Armazenamos o observável em cache em um Mapa, indexado pelo customerId. Isso garante que, uma vez que os dados tenham sido obtidos para uma linha, o mesmo observável seja reutilizado em vez de acionar novas chamadas HTTP.
  • Take(1) garante que o observável se completa após a primeira resposta, o que torna o gerenciamento de assinaturas limpo e evita vazamentos de memória.
  • O shareReplay(1) garante que, se múltiplos assinantes (ou ciclos de detecção de alteração) solicitarem os mesmos dados, apenas uma requisição HTTP seja enviada. Também reproduz o resultado para assinantes atrasados (por exemplo, quando a linha é colapsada e expandida novamente).
  • Depois, armazenamos os dados no ordersCache e retornamos

Ao combinar cache com shareReplay, garantimos que cada linha de cliente acione no máximo uma solicitação, não importa quantas vezes ela seja expandida ou com que frequência Angular execute a detecção de mudanças.

Exibindo os Detalhes da Ordem Selecionada

Até agora, mostramos como expandir uma linha de clientes e buscar a lista de pedidos sob demanda. Em seguida, queremos que o usuário selecione uma ordem específica dessa lista e exiba seus detalhes logo abaixo da linha.

  1. Adicione um evento de seleção ao combo 

Estendemos a caixa de combo para nos notificar sempre que um novo pedido for selecionado. Isso é feito ao lidar com o evento de seleção Mudando:

<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>
  1. Acompanhe o pedido selecionado por cliente 

Usamos uma string<mapa, OrderDto> para acompanhar a seleção, indexada pelo customerId. Isso garante que cada linha expandida se lembre de sua própria ordem selecionada:

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); 
}
  1. Show order details 

Finalmente, uma vez que uma ordem é selecionada, podemos renderizar seus detalhes no template expandido. Ao chamar getSelectedOrder(customerId), recuperamos a seleção salva e exibimos seus campos:

<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>

Isso nos dá uma visão de detalhes simples, porém funcional: quando o usuário expande uma linha de cliente, ele pode escolher um pedido do combo, e os detalhes do pedido são exibidos imediatamente abaixo.

Imagem de demonstração de carregamento de dados sob demanda do Angular

Cache Invalidation & Refresh Strategies 

Um cache que nunca expira pode se tornar obsoleto ou crescer sem limites. Você pode optar por implementar uma estratégia que se encaixe no seu app:

Manual Refresh 

Adicione um botão "Atualizar" no modelo de detalhes chamando refreshOrders(customerId) que substitua a entrada do cache.

refreshOrders(customerId: string) { 
  const request$ = this.northwindSwaggerService.getOrderDtoList(customerId).pipe(take(1), shareReplay(1)); 
  this.ordersCache.set(customerId, request$); 
}

Claro no colapso

Limpe o observável em cache quando a linha for colapsada. Isso garante uma nova solicitação na próxima vez que o usuário expandir:

<igx-grid (rowToggle)="onRowToggle($event)"...> 
onRowToggle(event: IRowToggleEventArgs) { 
    if (this.ordersCache.size > 0 && this.ordersCache.has(event.rowID)) { 
      this.ordersCache.delete(event.rowID); 
    } 
  }

Configuramos a grade master, implementamos carregamento de detalhes sob demanda, armazenamos resultados em cache para evitar requisições redundantes e até discutimos estratégias de atualização.

Agora vamos analisar considerações adicionais e melhores práticas que ajudarão você a escalar esse padrão para conjuntos de dados maiores e cenários de produção.

Handling Large Detail Data 

Quando o próprio conjunto de dados de detalhes é grande, buscar e renderizar tudo ao mesmo tempo pode prejudicar o desempenho. Em vez disso, usamos uma destas estratégias:

  • Paginação do lado do cliente ou do servidor: Obtenha apenas uma fatia dos dados por vez, passando parâmetros de página e tamanho de página para a API dos seus pedidos.
  • Carregamento sob demanda: Buscar apenas os dados necessários no momento

Considerações sobre Memória e Ciclo de Vida

  • Limpe caches em ngOnDestroy() para evitar vazamentos de memória: this.ordersCache.clear().
  • Se você mantém dados em cache (não apenas observáveis), considere limitar a memória total.
  • Avoid storing large, nested object Maps indefinitely. 

Comparação com Detalhes Pré-Carregados

Approach Melhor para Os Pontos Negativos
Preloaded details Pequenos conjuntos de dados, demonstrações simples e protótipos rápidos Carga inicial mais lenta e maior uso de memória
On-demand loading Conjuntos de dados grandes/complexos e aplicativos de produção Um pouco mais de complexidade de código e requer tratamento assíncrono

Conclusão

O carregamento de dados sob demanda é uma extensão natural do Layout de Grade Mestre-Detalhado. Ele melhora o desempenho, escala perfeitamente com grandes conjuntos de dados e garante uma experiência de usuário fluida.

Se você é novo nesse padrão, confira nosso artigo Começando com o Layout Mestre de Detalhes Usando Ignite UI for Angular Grade. Depois, aplique carregamento sob demanda aos seus próprios aplicativos para levar suas grades de dados ao próximo nível.

Com Ignite UI for Angular, você pode construir aplicações responsivas e orientadas por dados que permanecem rápidas, mesmo com o crescimento dos seus dados.

Confira o exemplo completo do aplicativo Load-on-demand.

Solicite uma demonstração