Saltar al contenido
Carga de datos bajo demanda en diseños de cuadrícula maestro-detalle

Carga de datos bajo demanda en diseños de cuadrícula maestro-detalle

La carga de datos bajo demanda es una técnica poderosa para escalar interfaces maestro-detalle. En lugar de obtener todos los detalles por adelantado, carga los registros relacionados solo cuando el usuario los necesita. Obtén más información en este artículo.

Lectura de 16 minutos

En nuestro artículo anterior, Introducción al diseño maestro-detalle usando Ignite UI for Angular cuadrícula, exploramos cómo configurar una interfaz maestro-detalle limpia y eficiente. Este patrón es ideal para presentar registros relacionados, como pedidos con artículos o departamentos con empleados, sin saturar la pantalla.

Pero, ¿qué sucede cuando su conjunto de datos es enorme? Cargar todos los detalles de todos los registros a la vez es ineficiente. Ahí es donde entra en juego la carga de datos bajo demanda. En lugar de obtener todos los detalles posibles por adelantado, solo carga los datos cuando el usuario expande una fila, lo que lleva a renderizaciones iniciales más rápidas, interacciones más fluidas y escalabilidad mejorada.

En este artículo, profundizaremos en por qué es importante la carga bajo demanda, cómo implementarla en el componente Ignite UI for Angular Grid y las mejores prácticas para aprovecharla al máximo.

Ampliación de la plantilla maestro-detalle

Si ha seguido nuestro primer artículo, ya sabe cómo configurar una cuadrícula maestra con filas expandibles y una plantilla de detalles, y desde dónde mostrar los datos:

  • El mismo conjunto de datos: por ejemplo, los pedidos y sus artículos asociados se agrupan
  • Un conjunto de datos externo, por ejemplo, un servicio que toma customerID como parámetro y llama a la API para recuperar datos de pedidos de clientes.

Para conjuntos de datos grandes, el segundo enfoque (obtención de detalles externos) es donde la carga bajo demanda resulta más efectiva.

Configuración del proyecto Angular

Antes de pasar a la implementación de carga a petición, preparemos un espacio de trabajo Angular para la demostración. Estos son los pasos.

  1. Crear un nuevo proyecto de Angular 

Si aún no tiene uno, comience por generar una nueva aplicación de Angular mediante la CLI de Angular y abra el proyecto en su IDE preferido.

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

Usaremos la biblioteca de componentes Ignite UI for Angular, que proporciona un amplio conjunto de componentes de interfaz de usuario, lo más importante es igxGrid, la base para nuestros ejemplos de carga de detalles maestros y bajo demanda. Agrégalo a tu proyecto con:

ng add igniteui-angular 
  1. Create a demo component 

Para mantener todo organizado, genere un componente de Angular dedicado donde construiremos la demostración de la cuadrícula:

ng generate component pages/grid-demo 
  1. Configurar el enrutamiento para la demostración 

Por último, ajuste la configuración de enrutamiento para que el componente de demostración sea la vista predeterminada. Reemplace su app.routes.ts existente (o su módulo de enrutamiento) con una configuración 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' }  
]; 

Agregue provideHttpClient a los proveedores de app.config.ts.

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

Configuración de los datos

Antes de implementar la carga bajo demanda en la cuadrícula maestro-detalle, necesitamos una capa de datos confiable. La preparación de los modelos de datos y los servicios garantiza que la cuadrícula muestre contenido significativo y pueda obtener detalles relacionados a medida que los usuarios interactúan con las filas.

Sus datos para IgxGrid pueden provenir de varias fuentes:

  • Datos estáticos/locales: archivos JSON en la carpeta assets.
  • API remotas: obtenidas a través de HttpClient de Angular desde un servicio de backend.
  • Datos simulados: generados directamente en un servicio durante el desarrollo.

Para este ejemplo, nos conectaremos a una API remota personalizada de Northwind Swagger, que proporciona a los clientes y sus pedidos relacionados. Los conceptos siguen siendo los mismos independientemente de la estructura de la fuente de datos.

Define Data Models 

Para aplicar la seguridad y la claridad de tipos, defina interfaces de TypeScript que representen sus datos en un archivo de models.ts dedicado. Estos modelos reflejan la estructura de las respuestas de la API y facilitan la vinculación de datos a la cuadrícula.

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 

A continuación, centralice todas las llamadas a la API en un servicio dedicado, por ejemplo, northwind-swagger.service.ts. También agregaremos un control de errores básico para mantener la resistencia de la aplicación.

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 

Con los modelos y el servicio en su lugar, el siguiente paso es mostrar el conjunto de datos Customers en la cuadrícula. La propiedad de datos de la cuadrícula está enlazada a una matriz northwindSwaggerCustomerDto, que se rellena cuando el componente se inicializa mediante la captura de datos del servicio.

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

En el lado de TypeScript, agregue una suscripción de servicio en la clase GridDemoComponent para recuperar los datos de los clientes y almacenarlos en la propiedad northwindSwaggerCustomerDto. A continuación, vincule la propiedad de entrada de datos de cuadrícula a '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();
  }
}
Carga de datos bajo demanda de Angular

Diseño de la plantilla de detalles e implementación de carga bajo demanda

Cuando la cuadrícula maestra presenta muchas filas principales (clientes), es ineficaz capturar todas las listas secundarias relacionadas (pedidos) para cada fila al principio. En su lugar, cargue los detalles solo cuando el usuario expanda una fila. Esta es la esencia de la carga bajo demanda: solicite datos secundarios solo cuando sea necesario y luego muéstrelos en la plantilla de detalles.

¿Cómo funciona?

  • Coloque una plantilla igxGridDetail dentro de la cuadrícula igx maestra.
  • En esa plantilla, llame a un método (por ejemplo, getOrders(customerId)) que devuelva un Observable<OrderDto[]>.
  • Use la canalización asincrónica para suscribirse a ese observable en la plantilla para Angular represente los resultados una vez que estén disponibles.
  • Almacene en caché el observable/resultado de cada cliente para que varias expansiones (o ciclos rápidos de detección de cambios) no activen nuevas llamadas HTTP.

¿Cómo lo implementamos?

  1. Añadir la plantilla de detalles 

Primero, definimos un marcador de posición para el contenido detallado agregando una plantilla ng con la directiva igxGridDetail. Esto marca dónde se renderizará la vista de detalle de cada fila expandida:

<ng-template igxGridDetail let-rowData></ng-template> 
  1. Agregar un combo vinculado a los pedidos (cargado bajo demanda) 

Dentro de la plantilla, podemos renderizar cualquier componente que queramos. En este ejemplo, agregaremos un cuadro combinado para mostrar los pedidos del cliente seleccionado. El punto clave aquí es que las órdenes no se cargan hasta que se expande la fila.

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

La primera vez que se expande una fila, el método getOrders(customerId) realiza una llamada HTTP y devuelve un observable. Debido al uso de la canalización asincrónica, Angular se suscribe automáticamente y representa el combo una vez que llegan los datos.

Una nota muy importante aquí es que cuando tenemos una plantilla ng, Angular seguirá cargando datos en esta plantilla en cada fotograma (detección de cambios). Si estos datos se cargan desde una solicitud, esto significa que por cada plantilla visible se realizarán pocas solicitudes cada segundo hasta que la aplicación se congele inevitablemente. Esta es la razón por la que los datos deben almacenarse en caché después de la recuperación.

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)!; 
  }
Carga de datos bajo demanda con ignite ui

Aquí están sucediendo un par de cosas:

  • Almacenamos en caché el observable en un mapa, con la clave customerId. Esto garantiza que una vez que se han obtenido los datos de una fila, se reutiliza el mismo observable en lugar de activar nuevas llamadas HTTP.
  • take(1) garantiza que el observable se completa después de la primera respuesta, lo que hace que la administración de suscripciones sea limpia y evita pérdidas de memoria.
  • shareReplay(1) garantiza que si varios suscriptores (o ciclos de detección de cambios) solicitan los mismos datos, solo se envía una solicitud HTTP. También reproduce el resultado para los suscriptores tardíos (por ejemplo, cuando la fila se contrae y se expande nuevamente).
  • Luego almacenamos los datos en ordersCache y los devolvemos

Al combinar el almacenamiento en caché con shareReplay, nos aseguramos de que cada fila de clientes desencadene como máximo una solicitud, independientemente de cuántas veces se expanda o de la frecuencia con la que Angular ejecute la detección de cambios.

Visualización de los detalles de la orden seleccionada

Hasta ahora, hemos mostrado cómo expandir una fila de clientes y obtener la lista de pedidos bajo demanda. A continuación, queremos que el usuario seleccione un pedido específico de esa lista y muestre sus detalles justo debajo de la fila.

  1. Agregar un evento de selección al combo 

Ampliamos el cuadro combinado para que nos notifique cada vez que se selecciona un nuevo pedido. Esto se hace controlando el evento 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>
  1. Seguimiento del pedido seleccionado por cliente 

Usamos una cadena < mapa, OrderDto> para realizar un seguimiento de la selección, con la clave customerId. Esto garantiza que cada fila expandida recuerde su propio orden seleccionado:

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, una vez que se selecciona un pedido, podemos representar sus detalles en la plantilla expandida. Al llamar a getSelectedOrder(customerId), recuperamos la selección guardada y mostramos sus 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>

Esto nos da una vista detallada simple pero funcional: cuando el usuario expande una fila de clientes, puede elegir un pedido del combo y los detalles del pedido se muestran inmediatamente debajo.

Imagen de demostración de carga de datos bajo demanda de Angular

Cache Invalidation & Refresh Strategies 

Una memoria caché que nunca expira puede quedar obsoleta o quedar ilimitada. Puedes optar por implementar una estrategia que se adapte a tu aplicación:

Manual Refresh 

Agregue un botón "Actualizar" en la plantilla de detalles que llama a refreshOrders(customerId) que reemplaza la entrada de caché.

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

Borrar al colapsar

Borre el observable almacenado en caché cuando se contraiga la fila. Esto asegura una nueva solicitud la próxima vez que el usuario se expanda:

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

Hemos configurado la cuadrícula maestra, implementado la carga de detalles bajo demanda, almacenado en caché los resultados para evitar solicitudes redundantes e incluso discutido estrategias de actualización.

Ahora veamos consideraciones adicionales y procedimientos recomendados que le ayudarán a escalar este patrón a conjuntos de datos y escenarios de producción más grandes.

Handling Large Detail Data 

Cuando el conjunto de datos de detalles en sí es grande, obtener y renderizar todo a la vez puede perjudicar el rendimiento. En su lugar, utilizamos una de estas estrategias:

  • Paginación del lado del cliente o del servidor: obtenga solo una porción de los datos a la vez pasando los parámetros page y pageSize a su API de pedidos.
  • Carga bajo demanda: Obtener solo los datos que se necesitan en este momento

Consideraciones sobre la memoria y el ciclo de vida

  • Borra las cachés en ngOnDestroy() para evitar pérdidas de memoria: this.ordersCache.clear().
  • Si mantiene los datos almacenados en caché (no solo los observables), considere limitar la memoria total.
  • Avoid storing large, nested object Maps indefinitely. 

Comparación con detalles precargados

Approach Mejor para Las desventajas
Preloaded details Conjuntos de datos pequeños, demostraciones simples y prototipos rápidos Carga inicial más lenta y mayor uso de memoria
On-demand loading Conjuntos de datos grandes/complejos y aplicaciones de producción Un poco más de complejidad de código y requiere control asincrónico

Conclusión

La carga de datos bajo demanda es una extensión natural del diseño de cuadrícula maestro-detalle. Mejora el rendimiento, se escala sin problemas con grandes conjuntos de datos y garantiza una experiencia de usuario fluida.

Si es nuevo en este patrón, consulte primero nuestro artículo Introducción al diseño maestro-detalle con Ignite UI for Angular cuadrícula. Luego, aplique la carga bajo demanda a sus propias aplicaciones para llevar sus cuadrículas de datos al siguiente nivel.

Con Ignite UI for Angular, puede crear aplicaciones receptivas y basadas en datos que se mantienen rápidas, incluso a medida que crecen sus datos.

Consulte el ejemplo completo de aplicación de carga a petición.

Solicitar una demostración