Virtual Drop Down

    The Ignite UI for Angular Drop Down component can fully integrate with the IgxForOf directive in order to display a very large list of items for its selection.

    Angular Virtual Drop Down Example

    Usage

    First Steps

    In order to configure the drop-down to display a list of virtual items, you need to fulfill some prerequisites. First, we need to import the IgxForOfModule in the module of the component that will declare our drop-down.

    // app.module.ts
    import { IgxForOfModule } from 'igniteui-angular';
    // import { IgxForOfModule } from '@infragistics/igniteui-angular'; for licensed package
    
    @NgModule({
        imports: [
            ...
            IgxForOfModule
        ]
    })
    export class AppModule {}
    

    Template Configuration

    Next, we need to create the drop-down component's template, looping through the data using *igxFor instead of *ngFor. The *igxFor directive needs some additional configuration in order to properly display all of the items:

    <!-- drop-down-virtual.component.html -->
    <button igxButton [igxToggleAction]="dropdown"
            [igxDropDownItemNavigation]="dropdown">
            Item Series
    </button>
    <igx-drop-down #dropdown>
        <div class="drop-down-virtual-wrapper" style="height: {{ itemsMaxHeight }}px;">
            <igx-drop-down-item
                *igxFor="let item of items; index as index;
                         scrollOrientation: 'vertical';
                         containerSize: itemsMaxHeight;
                         itemSize: itemHeight;"
                [value]="item" [isHeader]="item.header"
                role="option" [disabled]="item.disabled"
                [index]="index">
                {{ item.name }}
            </igx-drop-down-item>
        </div>
    </igx-drop-down>
    <div>Selected Model: <span>{{ dropdown.selectedItem?.value.name }}</span></div>
    

    The additional parameters passed to the *igxFor directive are:

    • index - captures the index of the current item in the data set
    • scrollOrientation - should always be 'vertical'
    • containerSize - the size of the virtualized container (in px). This needs to be enforced on the wrapping <div> as well
    • itemSize - the size of the items that will be displayed (in px)

    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.

    Note

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

    Note

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

    Note

    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's constructor, we'll declare a moderately large list of items (containing both headers and disabled items), which will be displayed in the drop-down. We will also need to declare itemHeight and itemsMaxHeight:

    // drop-drop-virtual.component.ts
    export class DropDownVirtualComponent {
      public items: DataItem[];
      public itemHeight = 48;
      public itemsMaxHeight = 320;
    
      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 last part of the configuration is to set overflow: hidden to the wrapping div in order to prevent the appearance of two scroll bars (one from the igxFor and one from the container itself):

    // drop-drop-virtual.component.scss
    .drop-down-virtual-wrapper {
        overflow: hidden;
    }
    

    Remote Data

    The igx-drop-down supports loading chunks of remote data using the *igxFor structural directive. The configuration is similar to the one with local items, the main difference being how data chunks are loaded.

    Template

    The drop-down template does not need to change much compared to the previous example - we still need to specify a wrapping div, style it accordingly and write out the complete configuration for the *igxFor. Since we'll be getting our data from a remote source, we need to specify that our data will be an observable and pass it through Angular's async pipe:

    <igx-drop-down #remoteDropDown>
        <div class="drop-down-virtual-wrapper">
            <igx-drop-down-item
                *igxFor="let item of rData | async; index as index;
                         scrollOrientation: 'vertical';
                         containerSize: itemsMaxHeight;
                         itemSize: itemHeight;"
                [value]="item.ProductName" role="option"
                [disabled]="item.disabled" [index]="index">
                {{ item.ProductName }}
            </igx-drop-down-item>
        </div>
    </igx-drop-down>
    

    Handling chunk load

    As you can see, the template is almost identical to the one in the previous example. In this remote data scenario, the code behind will do most of the heavy lifting.

    First, we need to define a remote service for fetching data:

    // remote.service.ts
    import { HttpClient } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    import { IForOfState } from 'igniteui-angular';
    // import { IForOfState } from '@infragistics/igniteui-angular'; for licensed package
    import { BehaviorSubject, Observable } from 'rxjs';
    
    @Injectable()
    export class RemoteService {
        public remoteData: Observable<any[]>;
        private _remoteData: BehaviorSubject<any[]>;
    
        constructor(private http: HttpClient) {
            this._remoteData = new BehaviorSubject([]);
            this.remoteData = this._remoteData.asObservable();
        }
    
        public getData(data?: IForOfState, cb?: (any) => void): any {
            // Assuming that the API service is RESTful and can take the following:
            // skip: start index of the data that we fecth
            // count: number of records we fetch
        this.http.get(`https://dummy.db/dummyEndpoint?skip=${data.startIndex}&count=${data.chunkSize}`).subscribe((data) => {
            // emit the values through the _remoteData subject
            this._remoteData.next(data);
        })
    }
    

    The service exposes an Observable under remoteData. We will inject our service and bind to that property in our remote drop-down component:

    // remote-drop-down.component.ts
    @Component({
        providers: [RemoteService],
        selector: 'app-drop-down-remote',
        templateUrl: './drop-down-remote.component.html',
        styleUrls: ['./drop-down-remote.component.scss']
    })
    export class DropDownRemoteComponent implements OnInit, OnDestroy {
        @ViewChild(IgxForOfDirective, { read: IgxForOfDirective })
        public remoteForDir: IgxForOfDirective<any>;
        @ViewChild('remoteDropDown', { read: IgxDropDownComponent })
        public remoteDropDown: IgxDropDownComponent;
        public itemHeight = 48;
        public itemsMaxHeight = 480;
        public prevRequest: Subscription;
        public rData: any;
    
        private destroy$ = new Subject();
        constructor(private remoteService: RemoteService) { }
    
        public ngAfterViewInit() {
            const initialState = { startIndex: 0, chunkSize: Math.ceil(this.itemsMaxHeight / this.itemHeight) }
            this.remoteService.getData(initialState, (data) => {
                this.remoteForDir.totalItemCount = data['@odata.count'];
            });
            // Subscribe to igxForOf.chunkPreload and load new data from service
            this.remoteForDir.chunkPreload.pipe(takeUntil(this.destroy$)).subscribe((data) => {
                this.dataLoading(data);
            });
        }
    
        public dataLoading(evt) {
            if (this.prevRequest) {
                this.prevRequest.unsubscribe();
            }
            this.prevRequest = this.remoteService.getData(
                evt,
                (data) => {
                    this.remoteForDir.totalItemCount = data['@odata.count'];
                });
        }
    
        public ngOnInit() {
            this.rData = this.remoteService.remoteData;
        }
    
        public ngOnDestroy() {
            this.destroy$.next();
            this.destroy$.complete();
        }
    }
    

    Inside of the ngAfterViewInit hook, we call to get data for the initial state and subscribe to the igxForOf directive's chunkPreload emitter. This subscription will be responsible for fetching data everytime the loaded chunk changes. We use pipe(takeUntil(this.destroy$)) so we can easily unsubscribe from the emitter on component destroy.

    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 using *igxFor:

    • The drop-down items that are being looped need to be passed in a wrapping element (e.g. <div>) which has the following css: overflow: hidden and height equal to containerSize in px
    • <igx-drop-down-item-group> cannot be used for grouping items when the list is virtualized. Use the isHeader propery 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 selection 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

    API References