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 setscrollOrientation
- should always be'vertical'
containerSize
- the size of the virtualized container (inpx
). This needs to be enforced on the wrapping<div>
as wellitemSize
- the size of the items that will be displayed (inpx
)
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
andheight
equal tocontainerSize
inpx
<igx-drop-down-item-group>
cannot be used for grouping items when the list is virtualized. Use theisHeader
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 toconst 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