Close
Angular React Web Components Blazor Angular
Open Source

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 (in px) 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.

For the drop-down to work with a virtualized list of items, value and index inputs must be passed to all items.

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-scroll element must have a fixed height, 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 the isHeader property instead
  • The items accessor will return only the list of non-header drop-down items that are currently in the virtualized view.
  • dropdown.selectedItem is of type { value: any, index: number }
  • The object emitted by selectionChanging changes to const emittedEvent: { newSelection: { value: any, index: number }, oldSelection: { value: any, index: number }, cancel: boolean, }
  • dropdown.setSelectedItem should 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.

API References