Virtual Drop Down
The Ignite UI for Angular Drop Down can fully integrate usage of the IgxForOf directive in order to display a very large list of items for its selection
Configuration
In order to configure the drop down to display a list of virtual items, you need to fulfil some prerequisites
First, we need to import the IgxForOfModule
in the module of the component that will be declaring the our drop down.
Module Import
// app.module.ts
import { IgxForOfModule } from 'igniteui-angular';
@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
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 class="selection">Selected Model: <span class="selection__name">{{ 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 our component's constructor
, we'll declare a moderately large list of items (containing both headers and disabled items) which we'll be displaying in our drop-down. We also need to declare itemHeight
and itemsMaxHeight
(used in the template) inside of our drop-down-virtual.component.ts
file:
// 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 (but very important) part of the configuration happens inside of our component's style sheet, drop-down-virtual.component.scss
.The wrapping div (drop-down-virtual-wrapper
) needs to have overflow: hidden
set, to prevent the appearance of two scroll bars (one from the igxFor
and one from the container itself):
.drop-down-virtual-wrapper {
overflow: hidden;
height: 320px;
width: 240px;
}
Here, we can also pass the style for height
(but we already did so in the template) - the value of itemsMaxHeight
, but in px
.
Virtual Drop Down Demo
You can view the configured example below:
Remote Data
The igx-drop-down
supports loading chunks of remote data using igxFor
directive. The configuration is similar to the one using igxFor
with local items and the main difference is handling the loading of different data chunks.
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 { 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 in should display, depending on the scrollbar's state. You can view the demo and play around with the configuration below:
Notes and Limitations
Using the drop down with a virtualized list of items enforce some limitations. Please be aware of the following when trying to set up a drop-down list using *igxFor
:
- The
<igx-drop-down-item>
s 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>
s 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-headerigx-drop-down-item
s that are currently in the virtualized view. dropdown.selectedItem
is of type{ value: any, index: number }
- The object emitted by
selecting
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