Create dual list from ListView in Angular ListView component
12 Sep 202520 minutes to read
A dual list interface consists of two ListView components that enable users to transfer items between two collections through intuitive controls. This pattern is commonly used for selection scenarios where users need to move items from an available list to a selected list and vice versa. The ListView component provides robust support for implementing dual list functionality with built-in events and data manipulation capabilities.
Use cases
Dual list interfaces are particularly effective for:
- Stock exchanges management: Moving stocks between different country portfolios or market categories
- Job application systems: Transferring skills between available competencies and selected qualifications
- User permission management: Assigning and removing user roles or access rights
- Product catalog organization: Moving products between different categories or collections
- Resource allocation: Distributing resources between available and assigned pools
Integration of Dual List
The dual list implementation uses two ListView components positioned side-by-side with transfer controls between them. An ej2-button component provides the transfer functionality, while a TextBox enables filtering capabilities for improved user experience with large datasets.
The dual list architecture supports:
- Bulk transfer operations: Moving entire datasets from one list to another
- Selective item transfer: Moving individually selected items between lists
- Real-time filtering: Dynamically filtering list content based on user input
- Bidirectional data flow: Supporting transfers in both directions with appropriate validation
In the ListView component, sorting is enabled using the sortOrder property to maintain consistent item ordering across transfers. The select event triggers when users interact with items, enabling dynamic button state management and transfer validation.
Data Transfer Operations
Moving complete dataset from first to second list (»)
The bulk forward transfer moves all items from the source ListView to the destination ListView. When users click the forward button, the system extracts all items from the first list using array slicing operations and concatenates them with the second ListView’s existing data. This button remains active only when the source ListView contains data, preventing unnecessary operations on empty collections.
Moving complete dataset from second to first list («)
The bulk backward transfer operates identically to the forward transfer but moves data from the second ListView back to the first. This operation maintains data integrity by transferring all items while preserving their original structure and sorting order. The button activation depends on the second ListView containing transferable items.
Moving selected items between lists (>) and (<)
Individual item transfers rely on the ListView’s select event to identify user-chosen items. When users select specific items from either list, the corresponding transfer buttons become active, allowing precise control over which items move between collections. This selective approach provides granular control while maintaining the overall list organization.
Filtering Implementation
The filtering functionality uses a TextBox input to capture user queries and dynamically filter ListView content. The implementation leverages the dataManager
to query the underlying data source, applying text-based filters that update the ListView display in real-time. This approach ensures efficient data handling while providing responsive user feedback during filtering operations.
Sorting Behavior
List item sorting in dual list implementations uses the ListView’s sortOrder property to maintain consistent ordering across both components. When sorting is enabled in one ListView, transferred items retain the same sort order in the destination list, ensuring data consistency and predictable user experience throughout the transfer process.
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { ListViewModule } from '@syncfusion/ej2-angular-lists'
import { ButtonModule } from '@syncfusion/ej2-angular-buttons'
import { Component, ViewChild } from "@angular/core";
import { enableRipple } from "@syncfusion/ej2-base";
import { DataManager, Query, ODataV4Adaptor } from "@syncfusion/ej2-data";
import { ListViewComponent } from "@syncfusion/ej2-angular-lists";
import { ButtonComponent } from "@syncfusion/ej2-angular-buttons"
enableRipple(true);
@Component({
imports: [
ListViewModule, ButtonModule
],
standalone: true,
selector: 'my-app',
template: `<div id="text1">
<input #textbox class="e-input" type="text" id="firstInput" placeholder="Filter" title="Type in a name" (keyup)="onFirstKeyUp($event)" />
</div>
<ejs-listview #list1 id='list-1' [dataSource]='firstListData' [fields]='fields' [sortOrder]='Ascending' (select)="onFirstListSelect()"></ejs-listview>
<div id="btn">
<button ejs-button #btn1 id="firstBtn" (click)="firstbtnclick()"> >> </button>
<button ejs-button #btn2 id="secondBtn" [disabled]=true (click)="secondbtnclick()"> > </button>
<button ejs-button #btn3 id="thirdBtn" [disabled]=true (click)="thirdbtnclick()"> < </button>
<button ejs-button #btn4 id="fourthBtn" (click)="fourthbtnclick()"> << </button>
</div>
<div id="text2">
<input #text class="e-input" type="text" id="secondInput" placeholder="Filter" title="Type in a name" (keyup)="onSecondKeyUp($event)" />
</div>
<ejs-listview #list2 id='list-2' [dataSource]='secondListData' [fields]='fields' [sortOrder]='Ascending' (select)="onSecondListSelect()"></ejs-listview>`,
})
export class AppComponent {
public fields?: Object;
public firstListData?: any; secondListData: any;
Ascending: any;
constructor() {
this.firstListData = [
{ text: "Hennessey Venom", id: "list-01" },
{ text: "Bugatti Chiron", id: "list-02" },
{ text: "Bugatti Veyron Super Sport", id: "list-03" },
{ text: "SSC Ultimate Aero", id: "list-04" },
{ text: "Koenigsegg CCR", id: "list-05" },
{ text: "McLaren F1", id: "list-06" }
];
this.secondListData = [
{ text: 'Aston Martin One- 77', id: 'list-07' },
{ text: 'Jaguar XJ220', id: 'list-08' },
{ text: 'McLaren P1', id: 'list-09' },
{ text: 'Ferrari LaFerrari', id: 'list-10' },
];
this.fields = { text: "text", id: "id" };
}
@ViewChild('list1') firstListObj?: ListViewComponent;
@ViewChild('list2') secondListObj?: ListViewComponent;
@ViewChild('btn1') firstBtnObj?: ButtonComponent;
@ViewChild('btn2') secondBtnObj?: ButtonComponent;
@ViewChild('btn3') thirdBtnObj?: ButtonComponent;
@ViewChild('btn4') fourthBtnObj?: ButtonComponent;
@ViewChild('textbox') textboxEle: any;
@ViewChild('text') textEle: any;
ngAfterViewInit() {
this.firstListData = (this.firstListObj as ListViewComponent | any).dataSource.slice();
this.secondListData = (this.secondListObj as ListViewComponent | any).dataSource.slice();
}
//Here, all list items are moved to the second list on clicking move all button
firstbtnclick() {
(this.secondListObj as ListViewComponent).dataSource = Array.prototype.concat.call((this.firstListObj as ListViewComponent).dataSource, (this.secondListObj as ListViewComponent).dataSource);
this.updateFirstListData();
this.firstListObj?.removeMultipleItems((this.firstListObj as ListViewComponent | any).liCollection);
this.firstListData = this.firstListData.concat((this.firstListObj as ListViewComponent).dataSource);
this.secondListData = (this.secondListObj as ListViewComponent | any).dataSource.slice();
(this.firstBtnObj as ButtonComponent).disabled = true;
this.onFirstKeyUp((e: any) => { });;
this.setButtonState();
}
//Here, the selected list items are moved to the second list on clicking move button
secondbtnclick() {
let e = this.firstListObj?.getSelectedItems();
(this.secondListObj as ListViewComponent).dataSource = Array.prototype.concat.call((this.secondListObj as ListViewComponent).dataSource, (e as any).data);
this.firstListObj?.removeItem((e as any).item);
this.firstListData = (this.firstListObj as ListViewComponent).dataSource;
this.secondListData = (this.secondListObj as ListViewComponent | any).dataSource.slice();
this.onFirstKeyUp((e: any) => { });;
(this.secondBtnObj as ButtonComponent).disabled = true;
this.setButtonState();
}
//Here, the selected list items are moved to the first list on clicking move button
thirdbtnclick() {
let e = this.secondListObj?.getSelectedItems();
(this.firstListObj as ListViewComponent).dataSource = Array.prototype.concat.call((this.firstListObj as ListViewComponent).dataSource, (e as any).data);
this.secondListObj?.removeItem((e as any).item);
this.secondListData = (this.secondListObj as ListViewComponent).dataSource;
this.firstListData = (this.firstListObj as ListViewComponent | any).dataSource.slice();
this.onSecondKeyUp((e: any) => { });
(this.thirdBtnObj as ButtonComponent).disabled = true;
this.setButtonState();
}
//Here, all list items are moved to the first list on clicking move all button
fourthbtnclick() {
(this.firstListObj as ListViewComponent).dataSource = Array.prototype.concat.call((this.firstListObj as ListViewComponent).dataSource, (this.secondListObj as ListViewComponent).dataSource);
this.updateSecondListData();
this.secondListObj?.removeMultipleItems((this.secondListObj as ListViewComponent | any).liCollection);
this.secondListData = this.secondListData.concat((this.secondListObj as ListViewComponent).dataSource);
this.firstListData = (this.firstListObj as ListViewComponent | any).dataSource.slice();
this.onSecondKeyUp((e: any) => { });
this.setButtonState();
}
//Here, the ListView data source is updated to the first list
updateFirstListData() {
Array.prototype.forEach.call((this.firstListObj as ListViewComponent | any).liCollection, (list) => {
this.firstListData.forEach((data: any, index: any) => {
if (list.innerText.trim() === data.text) {
delete this.firstListData[index];
}
});
});
this.textboxEle.nativeElement.value = '';
let ds: any[] = [];
this.firstListData.forEach((data: any) => {
ds.push(data);
})
this.firstListData = ds;
}
//Here, the ListView dataSource is updated for the second list
updateSecondListData() {
Array.prototype.forEach.call((this.secondListObj as ListViewComponent | any).liCollection, (list) => {
this.secondListData.forEach((data: any, index: string | number) => {
if (list.innerText.trim() === data.text) {
delete this.secondListData[index];
}
});
});
this.textEle.nativeElement.value = '';
let ds: any = [];
this.secondListData.forEach((data: any) => {
ds.push(data);
})
this.secondListData = ds;
}
onFirstListSelect() {
(this.secondBtnObj as ButtonComponent).disabled = false;
}
onSecondListSelect() {
(this.thirdBtnObj as ButtonComponent).disabled = false;
}
//Here, filtering is handled using the dataManager for the first list
onFirstKeyUp(e: any) {
let value = this.textboxEle.nativeElement.value;
let data = new DataManager(this.firstListData).executeLocal(new Query().where('text', 'startswith', value, true));
if (!value) {
(this.firstListObj as ListViewComponent).dataSource = this.firstListData.slice();
} else {
(this.firstListObj as ListViewComponent | any).dataSource = data;
}
}
//Here, filtering is handled using the dataManager for the second list
onSecondKeyUp(e: any) {
let value = this.textEle.nativeElement.value;
let data = new DataManager(this.secondListData).executeLocal(new Query().where('text', 'startswith', value, true));
if (!value) {
(this.secondListObj as ListViewComponent).dataSource = this.secondListData.slice();
} else {
(this.secondListObj as ListViewComponent | any).dataSource = data;
}
}
//Here, the state of the button is changed
setButtonState() {
if ((this.firstListObj as ListViewComponent | any).dataSource.length) {
(this.firstBtnObj as ButtonComponent).disabled = false;
} else {
(this.firstBtnObj as ButtonComponent).disabled = true;
(this.secondBtnObj as ButtonComponent).disabled = true;
}
if ((this.secondListObj as ListViewComponent | any).dataSource.length) {
(this.fourthBtnObj as ButtonComponent).disabled = false;
} else {
(this.fourthBtnObj as ButtonComponent).disabled = true;
(this.thirdBtnObj as ButtonComponent).disabled = true;
}
}
}
@import 'node_modules/@syncfusion/ej2-base/styles/material.css';
@import 'node_modules/@syncfusion/ej2-angular-base/styles/material.css';
@import 'node_modules/@syncfusion/ej2-angular-lists/styles/material.css';
@import 'node_modules/@syncfusion/ej2-angular-buttons/styles/material.css';
#list-1,
#list-2 {
width: 40%;
height: 430px;
box-shadow: 0 1px 4px #ddd;
border-bottom: 1px solid #ddd;
}
#firstList,
#secondList {
margin-top: 13px;
}
.e-btn {
width: 60px;
height: 60px;
margin-bottom: 15px;
}
#btn {
float: left;
width: 5%;
padding-left: 30px;
margin-top: 67px;
}
#list-1 {
float: left;
}
#list-2 {
float: right;
}
#firstInput {
width: 40%;
}
#secondInput {
margin-left: 102px;
width: 51.4%;
}
#text2 {
margin-top: -23px;
margin-left: 194px;
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));