Saltar al contenido
Introducción al diseño maestro-detalle con Ignite UI for Angular cuadrícula

Introducción al diseño maestro-detalle con Ignite UI for Angular cuadrícula

El diseño maestro-detalle es un patrón de interfaz de usuario probado que proporciona una forma sencilla, escalable y fácil de usar de presentar datos relacionados sin sobrecargar la pantalla. Obtén más información en este artículo.

12min read

Al crear aplicaciones empresariales (como CRM, ERP y paneles de administración), es común trabajar con conjuntos de datos relacionales en los que un registro está vinculado a varios registros relacionados. Por ejemplo, un pedido puede tener varios artículos, un cliente puede tener varias transacciones o un departamento puede tener numerosos empleados.

El diseño maestro-detalle es un patrón de interfaz de usuario probado para estos escenarios. Proporciona una forma sencilla, escalable y fácil de usar de presentar datos relacionados sin abrumar la pantalla.

En este artículo, lo guiaremos a través de la creación de una interfaz maestro-detalle limpia y eficiente usando Ignite UI for Angular. Exploraremos su poderoso componente Grid y su soporte integrado para plantillas anidadas usando igxGridDetail, lo que lo ayuda a mostrar datos jerárquicos con una configuración mínima y un alto rendimiento.

¿Qué es un diseño maestro-detalle?

Un diseño maestro-detalle (también conocido como filas principales-secundarias o expandibles) es un patrón de diseño en el que una lista de nivel superior (maestra) permite a los usuarios expandir elementos para revelar información relacionada (detalle). Los ejemplos comunes incluyen:

  • Órdenes (maestro) con posiciones de pedido (detalle)
  • Usuarios (maestro) con registros de perfil (detalle)
  • Departamentos (maestro) con Empleados (detalle)

Este patrón mejora la facilidad de uso al reducir el desorden de la pantalla al tiempo que proporciona acceso a datos más profundos cuando es necesario.

Configuración del proyecto Angular

Antes de sumergirnos en la implementación de maestro-detalle, configuremos un espacio de trabajo de Angular limpio para nuestra demostración.

1. Crear un nuevo proyecto de Angular 

Si aún no tiene un espacio de trabajo Angular listo, cree uno mediante la CLI de Angular. Abra este proyecto en su IDE o editor preferido para comenzar.

ng new master-detail-grid 

2. Add Ignite UI for Angular 

Ignite UI for Angular ofrece potentes componentes de interfaz de usuario, incluido el igxGrid que usaremos para el diseño maestro-detalle. A continuación, le indicamos cómo agregarlo.

ng add igniteui-angular

3. Generar un componente de demostración dedicado 

Para mantener su ejemplo maestro-detalle modular, genere un componente Angular dedicado.

ng generate component pages/grid-demo

4. Configurar el enrutamiento para la demostración 

Para simplificar la navegación, reemplace su app.routes.ts existente (o su módulo de enrutamiento) con una configuración mínima que redirija a su componente de demostración de forma predeterminada:

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

Configuración de los datos

Antes de crear el diseño maestro-detalle, es esencial preparar sus datos. Esto garantiza que los componentes de la cuadrícula tengan contenido significativo para mostrar y puedan obtener datos detallados relacionados de manera eficiente a medida que los usuarios interactúan con la interfaz de usuario.

Puede utilizar cualquier fuente de datos con Ignite UI for Angular:

  • Datos estáticos/locales: de un archivo JSON en su carpeta de activos.
  • Datos de API remota: obtenidos a través de HttpClient de Angular desde un servicio de backend.
  • Datos simulados: para el desarrollo, generados en el propio servicio.

En este ejemplo, nos integraremos con una API personalizada de Northwind Swagger. Los conceptos siguen siendo los mismos independientemente de la estructura de la fuente de datos.

1. Define Your Models 

Para aplicar la seguridad y la claridad de tipos, defina interfaces de TypeScript que representen las formas de datos en un archivo de 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 

Centralice toda la lógica de obtención de datos en un servicio Angular, por ejemplo, northwind-swagger.service.ts. Este es un servicio de ejemplo que interactúa con una API de Northwind personalizada, que usaremos en este ejemplo, e incluye un control básico de errores:

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); 
    }; 
  } 
} 

Con sus modelos y servicio preparados, los componentes de la cuadrícula ahora pueden vincularse directamente a matrices de pedidos por cliente seleccionado. A partir de aquí, pasaremos al diseño de la cuadrícula maestra y agregaremos progresivamente vistas de detalles, plantillas personalizadas y optimizaciones de rendimiento.

Diseño de la cuadrícula maestra

Ahora que nuestro servicio de datos está listo, es hora de crear la interfaz de usuario maestro-detalle paso a paso. Comenzaremos seleccionando un cliente, mostrando sus pedidos en una cuadrícula maestra y luego mejorando la cuadrícula con una plantilla de detalles personalizable.

1. Agregar un combo a clientes seleccionados 

Para permitir que los usuarios elijan un cliente, usaremos el componente igx-simple-combo que carga de forma asincrónica los datos del cliente desde el servicio.

  • Cuando un usuario selecciona un cliente, actualizamos una variable local localCustomerId en el componente.
  • Esto desencadena la obtención de los pedidos correspondientes para ese 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. Agregar la cuadrícula maestra para pedidos 

Una vez que se selecciona un cliente, mostramos sus pedidos utilizando una cuadrícula igx. El origen de datos de la cuadrícula es northwindSwaggerOrderWithDetailsDto, que se actualiza cada vez que cambia 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. Agregar una plantilla de detalles personalizada 

Para que la cuadrícula maestra sea más útil, agregamos una plantilla de detalles para cada pedido. Esto podría contener detalles del producto, totales o cualquier información relacionada. En nuestro caso, lo usaremos para mostrar algunas de las columnas de la cuadrícula para la dirección y, además, los detalles del orden en una cuadrícula.

Para configurar igxGrid para que se muestre en modo maestro-detalle, debe especificar una plantilla dentro de la cuadrícula, marcada con la directiva igxGridDetail:

<ng-template igxGridDetail> </ng-template>

Puede personalizar esta plantilla de detalles con cualquier componente que se ajuste a las necesidades de su negocio: texto, grupos de entrada, cuadrículas anidadas o gráficos. Aquí usaremos textos y una cuadrícula.

<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 

El contexto del modelo son los datos del registro maestro, de modo que los valores del registro maestro se pueden visualizar en el modelo detallado.

  • El contexto es el registro maestro al que pertenece el detalle.
  • Se declara una variable de contexto de plantilla utilizando la sintaxis let-, que permite acceder a los datos del registro maestro dentro de la plantilla de detalles.
  • En nuestro ejemplo, usamos let-rowData para nombrar esta variable de contexto.

Esto significa que dentro de la plantilla detallada, puede acceder a cualquier propiedad del registro maestro a través de rowData. Por ejemplo, rowData.shipAddress.country y rowData.orderDetails acceden al ID del pedido desde el registro de la cuadrícula maestra.

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

Carga dinámica simple

En muchos casos, no desea cargar todos los datos detallados por adelantado. En su lugar, puede obtenerlo a petición cuando se expande una fila.

A veces, los datos detallados provienen de un punto final completamente diferente y requieren un parámetro del registro maestro (por ejemplo, el ID de un pedido). En tales casos, podemos cargar los detalles a pedido cuando se expande la fila.

En este enfoque, cada vez que se expande una fila, solicitamos los datos detallados de la API utilizando el ID del registro maestro.

<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) 
    );   
}

Si bien es sencillo, este enfoque desencadena una nueva llamada a la API constantemente después de que se haya hecho visible. Esto podría ralentizar la aplicación e incluso hacer que se bloquee.

Consejos de optimización del rendimiento

Obtener repetidamente los mismos datos detallados es costoso tanto en rendimiento como en uso de la red. Una solución simple y efectiva es almacenar en caché los datos después de cargarlos por primera 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)!; 
  }

¿Cómo funciona?

  • En la primera expansión de una fila, getOrderDetails() llama a la API y almacena el observable resultante en orderDetailsCache.
  • Se llamará a este método constantemente, pero debido a que el método devuelve el observable almacenado en caché, evita solicitudes duplicadas.
  • El operador shareReplay(1) garantiza que, incluso si los suscriptores se adjuntan después de que se haya completado la llamada a la API, reciban los datos almacenados en caché inmediatamente sin activar una nueva solicitud.

Esto es fácil de implementar y reduce significativamente el número de solicitudes.

Envolver...

La creación de vistas maestro-detalle en Angular no tiene por qué ser compleja ni requerir un alto rendimiento. Con Ignite UI for Angular, obtiene una solución potente y elegante que requiere una configuración mínima pero que ofrece total flexibilidad y personalización para adaptarse a las necesidades únicas de su aplicación.

Ya sea que esté creando paneles de administración, sistemas de administración o interfaces basadas en datos, la cuadrícula de Angular de Ignite UI le brinda todo lo que necesita para desarrollar aplicaciones rápidas, receptivas y fáciles de mantener.

Puede consultar nuestros ejemplos de cuadrícula de Ignite UI for Angular y explorar todos los componentes y características utilizados. La aplicación Administración de flotas muestra el uso de Master-Detail Grid, para que pueda ver cómo se presentan y administran los datos en escenarios del mundo real.

Solicitar una demostración