Menú desplegable virtual
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/directives';
// 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
Como puede ver, la plantilla es casi idéntica a la del ejemplo anterior. En este escenario de datos remotos, el código subyacente hará la mayor parte del trabajo pesado.
Primero, necesitamos definir un servicio remoto para recuperar datos:
// remote.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { IForOfState } from 'igniteui-angular/directives';
// 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 every time the loaded chunk changes. We use pipe(takeUntil(this.destroy$)) so we can easily unsubscribe from the emitter on component destroy.
Remote Virtualization - Demo
El resultado de la configuración anterior es un menú desplegable que carga dinámicamente los datos que debe mostrar, según el estado de la barra de desplazamiento:
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: hiddenandheightequal tocontainerSizeinpx <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
selectionchanges 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