Virtual Drop Down
The Ignite UI for Angular Drop Down component can host the Virtual Scroll component in order to display a very large list of items for its selection. Only the items in the drop-down’s viewport are rendered, while selection and keyboard navigation work over the whole list.
Angular Virtual Drop Down Example
Usage
First Steps
Import the drop-down together with the VirtualScroll and the IgxVirtualItemDirective, which marks the template of a list item:
// drop-down-virtual.component.ts
import { Component } from '@angular/core';
import { IgxButtonDirective, IgxToggleActionDirective } from 'igniteui-angular/directives';
import { IgxDropDownComponent, IgxDropDownItemComponent, IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';
import { IgxVirtualItemDirective, IgxVirtualScrollComponent } from 'igniteui-angular/virtual-scroll';
// import { IgxVirtualItemDirective, IgxVirtualScrollComponent } from '@infragistics/igniteui-angular'; for licensed package
@Component({
selector: 'app-drop-down-virtual',
templateUrl: './drop-down-virtual.component.html',
styleUrls: ['./drop-down-virtual.component.scss'],
imports: [
IgxButtonDirective, IgxToggleActionDirective, IgxDropDownItemNavigationDirective,
IgxDropDownComponent, IgxDropDownItemComponent,
IgxVirtualScrollComponent, IgxVirtualItemDirective
]
})
export class DropDownVirtualComponent { }
Template Configuration
Next, place an igx-virtual-scroll inside the drop-down and render each item with an ng-template marked with igxVirtualItem. The drop-down detects the projected virtual scroll and uses it for scrolling, keyboard navigation, and selection:
<!-- drop-down-virtual.component.html -->
<button igxButton [igxToggleAction]="dropdown" [igxDropDownItemNavigation]="dropdown">
Item Series
</button>
<igx-drop-down #dropdown>
<igx-virtual-scroll class="drop-down-virtual-wrapper" [data]="items" [estimatedItemSize]="itemHeight">
<ng-template igxVirtualItem let-item let-index="index">
<igx-drop-down-item [value]="item" [isHeader]="item.header" [disabled]="item.disabled" [index]="index">
{{ item.name }}
</igx-drop-down-item>
</ng-template>
</igx-virtual-scroll>
</igx-drop-down>
<div>Selected Model: <span>{{ dropdown.selectedItem?.value.name }}</span></div>
The inputs of igx-virtual-scroll used here are:
data- the whole list of items. It is compared by reference, so assign a new array to change it.estimatedItemSize- the height of an item (inpx) before it is rendered and measured. Set it to the real height of the drop-down items, so that the scrollbar and keyboard navigation are accurate from the start.
In order to assure uniqueness of the items, pass item inside of the value input and index inside of the index input of the igx-drop-down-item.
To preserve selection while scrolling, the drop-down item needs to have a reference to the data items it is bound to.
It is strongly advised for each item to have an unique value passed to the [value] input. Otherwise, it might lead to unexpected results (incorrect selection).
When the drop-down uses virtualized items, the type of dropdown.selectedItem becomes { value: any, index: number }, where value is a reference to the data item passed inside of the [value] input and index is the item’s index in the data set
Component Definition
Inside of the component, declare a moderately large list of items (containing both headers and disabled items), which will be displayed in the drop-down, and the height of an item:
// drop-down-virtual.component.ts
export class DropDownVirtualComponent {
public items: DataItem[];
public itemHeight = 40;
constructor() {
const itemsCollection: DataItem[] = [];
for (let i = 0; i < 50; i++) {
const series = (i * 10).toString();
itemsCollection.push({
id: series,
name: `${series} Series`,
header: true,
disabled: false
});
for (let j = 0; j < 10; j++) {
itemsCollection.push({
id: `${series}_${j}`,
name: `Series ${series}, ${i * 10 + j} Model`,
header: false,
disabled: j % 9 === 0
});
}
}
this.items = itemsCollection;
}
}
Styles
The igx-virtual-scroll element is the scroll container of the list, so it needs a fixed height. No wrapping element or overflow rule is needed:
// drop-down-virtual.component.scss
.drop-down-virtual-wrapper {
height: 240px;
width: 180px;
}
Remote Data
The igx-drop-down also supports loading pages of remote data. Bind the virtual scroll’s dataWindow input to the loaded page, and load the next page from the range that the stateChange output reports. The list is as long as the whole remote collection, so the scrollbar spans every item, while only the loaded page is in memory.
Template
The template differs from the previous example only in the data binding: dataWindow takes the place of data, and stateChange requests the pages. The index template variable is the index of the item in the whole remote collection:
<igx-drop-down #remoteDropDown>
<igx-virtual-scroll
class="drop-down-virtual-wrapper"
[dataWindow]="page()"
[estimatedItemSize]="itemHeight"
(stateChange)="onStateChange($event)">
<ng-template igxVirtualItem let-item let-index="index">
<igx-drop-down-item [value]="item.ProductName" [index]="index">
{{ item.ProductName }}
</igx-drop-down-item>
</ng-template>
</igx-virtual-scroll>
</igx-drop-down>
Loading pages
First, define a remote service that returns a page of the collection together with the total number of records:
// remote.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable()
export class RemoteService {
private http = inject(HttpClient);
// Assuming that the API service is RESTful and can take the following:
// skip: start index of the data that we fetch
// count: number of records we fetch
public getPage(skip: number, count: number): Observable<{ value: any[]; '@odata.count': number }> {
return this.http.get<{ value: any[]; '@odata.count': number }>(
`https://dummy.db/dummyEndpoint?$skip=${skip}&$top=${count}&$count=true`
);
}
}
In the component, keep the loaded page in a signal of type VirtualDataWindow. Load the first page on initialization, and a new one whenever stateChange reports a range that the loaded page does not cover. Cancel the previous request, so a slow response cannot replace a newer page:
// drop-down-remote.component.ts
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core';
import { VirtualDataWindow, VirtualScrollState } from 'igniteui-angular/virtual-scroll';
import { Subscription } from 'rxjs';
/** Extra records requested on each side of the range the viewport wants. */
const BUFFER = 10;
@Component({
providers: [RemoteService],
selector: 'app-drop-down-remote',
templateUrl: './drop-down-remote.component.html',
styleUrls: ['./drop-down-remote.component.scss'],
imports: [/* the same imports as in the local example */]
})
export class DropDownRemoteComponent implements OnInit, OnDestroy {
private remoteService = inject(RemoteService);
public itemHeight = 40;
public readonly page = signal<VirtualDataWindow<any>>({ items: [], startIndex: 0, totalCount: 0 });
private pending?: Subscription;
public ngOnInit() {
this.loadPage(0, 2 * BUFFER);
}
public onStateChange(state: VirtualScrollState) {
const page = this.page();
if (state.startIndex >= page.startIndex && state.endIndex < page.startIndex + page.items.length) {
return; // The loaded page already covers the range.
}
const startIndex = Math.max(0, state.startIndex - BUFFER);
this.loadPage(startIndex, state.endIndex - startIndex + 1 + BUFFER);
}
public ngOnDestroy() {
this.pending?.unsubscribe();
}
private loadPage(startIndex: number, count: number) {
this.pending?.unsubscribe();
this.pending = this.remoteService.getPage(startIndex, count).subscribe(data => {
this.page.set({ items: data.value, startIndex, totalCount: data['@odata.count'] });
});
}
}
When the drop-down navigates to an item that is not loaded yet, for example with the End key, the virtual scroll scrolls to it, stateChange reports the new range, and the page that contains the item is loaded.
Remote Virtualization - Demo
The result of the above configuration is a drop-down that dynamically loads the data it should display, depending on the scrollbar’s state:
Notes and Limitations
Using the drop-down with a virtualized list of items enforces some limitations. Please, be aware of the following when trying to set up a drop-down list with igx-virtual-scroll:
- The
igx-virtual-scrollelement must have a fixedheight, because it is the scroll container of the list. <igx-drop-down-item-group>cannot be used for grouping items when the list is virtualized. Use theisHeaderproperty instead- The
itemsaccessor will return only the list of non-header drop-down items that are currently in the virtualized view. dropdown.selectedItemis of type{ value: any, index: number }- The object emitted by
selectionChangingchanges toconst emittedEvent: { newSelection: { value: any, index: number }, oldSelection: { value: any, index: number }, cancel: boolean, } dropdown.setSelectedItemshould be called with the item’s index in the data set- setting the drop-down item’s
[selected]input will not mark the item in the drop-down selection
The drop-down also works with a projected *igxFor directive, as in earlier versions. The directive is deprecated in favor of the Virtual Scroll, so use igx-virtual-scroll for new drop-downs.