Skip to content
On-Demand Data Loading in Master-Detail Grid Layouts  

On-Demand Data Loading in Master-Detail Grid Layouts  

On-demand data loading is a powerful technique for scaling master-detail interfaces. Instead of fetching every detail upfront, you load related records only when the user needs them. Learn more in this article.

16min read

In our previous article, Getting Started with Master-Detail Layout Using Ignite UI for Angular Grid , we explored how to set up a clean and efficient master-detail interface. This pattern is ideal for presenting related records, such as orders with items or departments with employees, without cluttering the screen. 

But what happens when your dataset is huge? Loading all the details for all records at once is inefficient. That’s where on-demand data loading comes in. Instead of fetching every possible detail upfront, you only load the data when the user expands a row, leading to faster initial renders, smoother interactions, and improved scalability. 

In this article, we’ll dive into why on-demand loading matters, how to implement it in the Ignite UI for Angular Grid component, and best practices to make the most of it. 

Extending the Master-Detail Template 

If you’ve followed our first article, you already know how to set up a master grid with expandable rows and a detail template, and where to display data  from: 

  • The same dataset – For example, orders and their associated items are bundled together 
  • An external dataset – for example, a service that takes customerID as a parameter and calls the API to retrieve customer orders data. 

For large datasets, the second approach (external detail fetching) is where on-demand loading proves most effective. 

Setting Up the Angular Project  

Before we jump into the on-demand loading implementation, let’s prepare an Angular workspace for the demo. Here are the steps. 

  1. Create a new Angular project 

If you don’t already have one, start by generating a new Angular application using the Angular CLI and open the project in your preffered IDE. 

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

We’ll be using the Ignite UI for Angular component library, which provides a rich set of UI components – most importantly the igxGrid, the foundation for our master-detail and on-demand loading examples. Add it to your project with: 

ng add igniteui-angular 
  1. Create a demo component 

To keep everything organized, generate a dedicated Angular component where we’ll build the grid demo: 

ng generate component pages/grid-demo 
  1. Set up routing for the demo 

Finally, adjust your routing configuration so the demo component is the default view. Replace your existing app.routes.ts (or your routing module) with a minimal setup.

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

Add provideHttpClient to the providers in app.config.ts.

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

Setting Up the Data  

Before implementing on-demand loading in the master-detail grid, we need a reliable data layer. Preparing your data models and services ensures the grid displays meaningful content and can fetch related details as users interact with rows. 

Your data for the IgxGrid can come from various sources: 

  • Static/local data – JSON files in the assets folder. 
  • Remote APIs – fetched via Angular’s HttpClient from a backend service. 
  • Mocked data – generated directly in a service during development. 

For this example, we’ll connect to a custom remote Northwind Swagger API, which provides Customers and their related Orders. The concepts remain the same regardless of your data source structure.  

Define Data Models 

To enforce type safety and clarity, define TypeScript interfaces representing your data in a dedicated models.ts file. These models mirror the structure of the API responses and make it easy to bind data to the grid.

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 

Next, centralize all API calls in a dedicated service – for example, northwind-swagger.service.ts. We’ll also add basic error handling to keep the app resilient.

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 

With the models and service in place, the next step is to display the Customers dataset in the grid. The grid’s data property is bound to a northwindSwaggerCustomerDto array, which is populated when the component initializes by fetching data from the service. 

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

On the TypeScript side, add a service subscription in the GridDemoComponent class to retrieve the customers data and store it in the northwindSwaggerCustomerDto property. Then bind the grid data input property to `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();
  }
}
on-demand data loading

Designing the Detail Template & Implementing Load-on-Demand 

When the master grid presents many parent rows (customers), it’s inefficient to fetch every related child list (orders) for every row up front. Instead, load the details only when the user expands a row. This is the essence of on-demand loading: request child data only when it’s needed, then show it in the detail template. 

How It Works? 

  • Put an igxGridDetail template inside the master igx-grid. 
  • In that template, call a method (e.g. getOrders(customerId)) that returns an Observable<OrderDto[]>. 
  • Use the async pipe to subscribe to that observable in the template so Angular renders results once available. 
  • Cache each customer’s observable/result so multiple expansions (or rapid change detection cycles) don’t trigger new HTTP calls. 

How Do We Implement It? 

  1. Add the detail template 

First, we define a placeholder for the detail content by adding an ng-template with the igxGridDetail directive. This marks where the detail view for each expanded row will render: 

<ng-template igxGridDetail let-rowData></ng-template> 
  1. Add a combo bound to orders (loaded on demand) 

Inside the template, we can render any component we want. In this example, we’ll add a combo box to display the orders for the selected customer. The key point here is that the orders are not loaded until the row is expanded. 

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

The first time a row is expanded, the getOrders(customerId) method makes an HTTP call and returns an observable. Because of using the async pipe, Angular subscribes automatically and renders the combo once the data arrives. 

A very important note here is that when we have a ng-template, Angular will keep loading data into this template every frame (change detection). If this data is loaded from a request, this means that for every visible template few requests will be made every second until the application inevitably freezes. This is why data needs to be cached after fetching. 

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)!; 
  }
On-Demand Data Loading with ignite ui

A couple of things are happening here: 

  • We cache the observable in a Map, keyed by customerId. This ensures that once data has been fetched for a row, the same observable is reused instead of triggering new HTTP calls. 
  • take(1) guarantees the observable completes after the first response, which makes subscription management clean and avoids memory leaks. 
  • shareReplay(1) ensures that if multiple subscribers (or change detection cycles) request the same data, only one HTTP request is sent. It also replays the result to late subscribers (e.g., when the row is collapsed and expanded again). 
  • Then we store the data in ordersCache and return it 

By combining caching with shareReplay, we make sure each customer row triggers at most one request, no matter how many times it is expanded or how often Angular runs change detection. 

Displaying the Details for the Selected Order 

So far, we’ve shown how to expand a customer row and fetch the list of orders on demand. Next, we want the user to select a specific order from that list and display its details right under the row. 

  1. Add a selection event to the combo 

We extend the combo box to notify us whenever a new order is selected. This is done by handling the selectionChanging event: 

<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. Track the selected order per customer 

We use a Map<string, OrderDto> to keep track of the selection, keyed by the customerId. This ensures that each expanded row remembers its own selected order: 

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 

Finally, once an order is selected, we can render its details in the expanded template. By calling getSelectedOrder(customerId), we retrieve the saved selection and display its fields: 

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

   This gives us a simple yet functional detail view: when the user expands a customer row, they can pick an order from the combo, and the order details are displayed immediately underneath.

Cache Invalidation & Refresh Strategies 

A cache that never expires can become stale or grow unbounded. You can choose to implement a strategy that fits your app: 

Manual Refresh 

Add a “Refresh” button in the detail template calling refreshOrders(customerId) that replaces the cache entry. 

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

Clear on Collapse 

Clear the cached observable when the row is collapsed. This ensures a fresh request next time the user expands:

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

We’ve set up the master grid, implemented on-demand detail loading, cached results to avoid redundant requests, and even discussed refresh strategies. 

Now let’s look at additional considerations and best practices that will help you scale this pattern to larger datasets and production scenarios. 

Handling Large Detail Data 

When the detail dataset itself is large, fetching and rendering everything at once can hurt performance. Instead, we use one of these strategies: 

  • Client or Server-side paging: Fetch only a slice of the data at a time by passing page and pageSize parameters to your orders API.
  • Load-On-Demand: Fetching only the data that is needed at the moment 

Memory & Lifecycle Considerations 

  • Clear caches in ngOnDestroy() to avoid memory leaks: this.ordersCache.clear(). 
  • If you keep cached data (not just observables), consider limiting total memory. 
  • Avoid storing large, nested object Maps indefinitely. 

Comparison with Preloaded Details 

Approach Best For The Downsides
Preloaded details Small datasets, simple demos, and quick prototypes Slower initial load and higher memory usage
On-demand loading Large/complex datasets and production apps Slightly more code complexity and requires async handling

Conclusion 

On-demand data loading is a natural extension of the Master-Detail Grid Layout. It improves performance, scales seamlessly with large datasets, and ensures a smooth user experience. 

If you’re new to this pattern, check out our Getting Started with Master-Detail Layout Using Ignite UI for Angular Grid article first. Then, apply on-demand loading to your own apps to take your data grids to the next level. 

With Ignite UI for Angular, you can build responsive, data-driven applications that stay fast, even as your data grows. 

Check out the complete Load-on-demand app sample.

Request a Demo