diff --git a/.github/pr-screenshots/picklist-940-lazy-load.png b/.github/pr-screenshots/picklist-940-lazy-load.png
new file mode 100644
index 0000000000..13435111b8
Binary files /dev/null and b/.github/pr-screenshots/picklist-940-lazy-load.png differ
diff --git a/apps/docs/doc/picklist/lazy-doc.ts b/apps/docs/doc/picklist/lazy-doc.ts
new file mode 100644
index 0000000000..8ed371df43
--- /dev/null
+++ b/apps/docs/doc/picklist/lazy-doc.ts
@@ -0,0 +1,68 @@
+import { AppCode } from '@/components/doc/app.code';
+import { AppDocSectionText } from '@/components/doc/app.docsectiontext';
+import { Component, signal } from '@angular/core';
+import { ScrollerLazyLoadEvent } from '@openng/optimus-ui/scroller';
+import { PickListModule } from '@openng/optimus-ui/picklist';
+
+@Component({
+ selector: 'lazy-doc',
+ standalone: true,
+ imports: [PickListModule, AppCode, AppDocSectionText],
+ template: `
+
+
+ When dealing with huge datasets, new chunks of data can be loaded on demand instead of all at once. Enable sourceVirtualScroll along with sourceLazy to render the source list with a Scroller and
+ fetch data as the user scrolls, using the onSourceLazyLoad event to request the next chunk.
+
+
+
+
+
+ {{ item?.name }}
+
+
+
+
+ `
+})
+export class LazyDoc {
+ totalSourceCount = 10000;
+
+ sourceProducts = signal(Array.from({ length: this.totalSourceCount }, () => ({}) as any));
+
+ targetProducts = signal([]);
+
+ loadLazyTimeout: any = null;
+
+ onSourceLazyLoad(event: ScrollerLazyLoadEvent) {
+ if (this.loadLazyTimeout) {
+ clearTimeout(this.loadLazyTimeout);
+ }
+
+ // imitate the delay of a backend fetch for the next chunk of source items
+ this.loadLazyTimeout = setTimeout(
+ () => {
+ const { first, last } = event;
+ const products = [...this.sourceProducts()];
+
+ for (let i = first; i < (last ?? first); i++) {
+ products[i] = { id: i, name: `Product #${i}` };
+ }
+
+ this.sourceProducts.set(products);
+ },
+ Math.random() * 1000 + 250
+ );
+ }
+}
diff --git a/apps/docs/pages/picklist/index.ts b/apps/docs/pages/picklist/index.ts
index e6e27231cd..777ebe2362 100644
--- a/apps/docs/pages/picklist/index.ts
+++ b/apps/docs/pages/picklist/index.ts
@@ -2,6 +2,7 @@ import { AccessibilityDoc } from '@/doc/picklist/accessibility-doc';
import { BasicDoc } from '@/doc/picklist/basic-doc';
import { FilterDoc } from '@/doc/picklist/filter-doc';
import { ImportDoc } from '@/doc/picklist/import-doc';
+import { LazyDoc } from '@/doc/picklist/lazy-doc';
import { PTComponent } from '@/doc/picklist/pt/PTComponent';
import { TemplateDoc } from '@/doc/picklist/template-doc';
import { Component } from '@angular/core';
@@ -35,6 +36,11 @@ export class PickListDemo {
label: 'Template',
component: TemplateDoc
},
+ {
+ id: 'lazy',
+ label: 'Lazy Load',
+ component: LazyDoc
+ },
{
id: 'accessibility',
label: 'Accessibility',
diff --git a/packages/optimus-ui/src/picklist/picklist.spec.ts b/packages/optimus-ui/src/picklist/picklist.spec.ts
index abc3490bf6..c056a5a3a3 100644
--- a/packages/optimus-ui/src/picklist/picklist.spec.ts
+++ b/packages/optimus-ui/src/picklist/picklist.spec.ts
@@ -15,6 +15,7 @@ import {
PickListTargetReorderEvent,
PickListTargetSelectEvent
} from '@openng/optimus-ui/types/picklist';
+import type { ScrollerLazyLoadEvent } from '@openng/optimus-ui/types/scroller';
import { PickList } from './picklist';
@Component({
@@ -36,6 +37,12 @@ import { PickList } from './picklist';
[sourceStyle]="sourceStyle"
[targetStyle]="targetStyle"
[dataKey]="dataKey"
+ [sourceLazy]="sourceLazy"
+ [targetLazy]="targetLazy"
+ [sourceVirtualScroll]="sourceVirtualScroll"
+ [targetVirtualScroll]="targetVirtualScroll"
+ [sourceVirtualScrollItemSize]="sourceVirtualScrollItemSize"
+ [targetVirtualScrollItemSize]="targetVirtualScrollItemSize"
(onMoveToTarget)="onMoveToTarget($event)"
(onMoveToSource)="onMoveToSource($event)"
(onMoveAllToTarget)="onMoveAllToTarget($event)"
@@ -44,6 +51,8 @@ import { PickList } from './picklist';
(onTargetSelect)="onTargetSelect($event)"
(onSourceReorder)="onSourceReorder($event)"
(onTargetReorder)="onTargetReorder($event)"
+ (onSourceLazyLoad)="onSourceLazyLoad($event)"
+ (onTargetLazyLoad)="onTargetLazyLoad($event)"
>
{{ item.name }}
@@ -83,6 +92,12 @@ class TestPickListComponent {
sourceStyle: any = null as any;
targetStyle: any = null as any;
dataKey: string | undefined;
+ sourceLazy: boolean = false;
+ targetLazy: boolean = false;
+ sourceVirtualScroll: boolean = false;
+ targetVirtualScroll: boolean = false;
+ sourceVirtualScrollItemSize: number | undefined;
+ targetVirtualScrollItemSize: number | undefined;
// Event handlers
onMoveToTarget(event: PickListMoveToTargetEvent) {
@@ -117,6 +132,14 @@ class TestPickListComponent {
this.targetReorderEvent = event;
}
+ onSourceLazyLoad(event: ScrollerLazyLoadEvent) {
+ this.sourceLazyLoadEvent = event;
+ }
+
+ onTargetLazyLoad(event: ScrollerLazyLoadEvent) {
+ this.targetLazyLoadEvent = event;
+ }
+
// Event tracking
moveToTargetEvent: PickListMoveToTargetEvent | null = null as any;
moveToSourceEvent: PickListMoveToSourceEvent | null = null as any;
@@ -126,6 +149,8 @@ class TestPickListComponent {
targetSelectEvent: PickListTargetSelectEvent | null = null as any;
sourceReorderEvent: PickListSourceReorderEvent | null = null as any;
targetReorderEvent: PickListTargetReorderEvent | null = null as any;
+ sourceLazyLoadEvent: ScrollerLazyLoadEvent | null = null as any;
+ targetLazyLoadEvent: ScrollerLazyLoadEvent | null = null as any;
}
describe('PickList', () => {
@@ -167,6 +192,69 @@ describe('PickList', () => {
});
});
+ describe('Lazy Loading & VirtualScroll', () => {
+ it('should default lazy and virtualScroll to false for both lists', () => {
+ expect(picklistComponent.sourceLazy).toBe(false);
+ expect(picklistComponent.targetLazy).toBe(false);
+ expect(picklistComponent.sourceVirtualScroll).toBe(false);
+ expect(picklistComponent.targetVirtualScroll).toBe(false);
+ });
+
+ it('should forward sourceLazy and sourceVirtualScroll to the source listbox', () => {
+ component.sourceLazy = true;
+ component.sourceVirtualScroll = true;
+ component.sourceVirtualScrollItemSize = 32;
+ fixture.detectChanges();
+
+ const sourceListbox = fixture.debugElement.queryAll(By.css('p-listbox'))[0].componentInstance;
+ expect(sourceListbox.lazy).toBe(true);
+ expect(sourceListbox.virtualScroll).toBe(true);
+ expect(sourceListbox.virtualScrollItemSize).toBe(32);
+ });
+
+ it('should forward targetLazy and targetVirtualScroll to the target listbox', () => {
+ component.targetLazy = true;
+ component.targetVirtualScroll = true;
+ component.targetVirtualScrollItemSize = 48;
+ fixture.detectChanges();
+
+ const targetListbox = fixture.debugElement.queryAll(By.css('p-listbox'))[1].componentInstance;
+ expect(targetListbox.lazy).toBe(true);
+ expect(targetListbox.virtualScroll).toBe(true);
+ expect(targetListbox.virtualScrollItemSize).toBe(48);
+ });
+
+ it('should keep source and target lazy/virtualScroll configuration independent', () => {
+ component.sourceLazy = true;
+ component.sourceVirtualScroll = true;
+ component.targetLazy = false;
+ component.targetVirtualScroll = false;
+ fixture.detectChanges();
+
+ const listboxes = fixture.debugElement.queryAll(By.css('p-listbox'));
+ expect(listboxes[0].componentInstance.lazy).toBe(true);
+ expect(listboxes[0].componentInstance.virtualScroll).toBe(true);
+ expect(listboxes[1].componentInstance.lazy).toBe(false);
+ expect(listboxes[1].componentInstance.virtualScroll).toBe(false);
+ });
+
+ it('should emit onSourceLazyLoad when the source listbox emits onLazyLoad', () => {
+ const event: ScrollerLazyLoadEvent = { first: 0, last: 50 } as ScrollerLazyLoadEvent;
+
+ picklistComponent.onSourceLazyLoad.emit(event);
+
+ expect(component.sourceLazyLoadEvent).toBe(event);
+ });
+
+ it('should emit onTargetLazyLoad when the target listbox emits onLazyLoad', () => {
+ const event: ScrollerLazyLoadEvent = { first: 10, last: 60 } as ScrollerLazyLoadEvent;
+
+ picklistComponent.onTargetLazyLoad.emit(event);
+
+ expect(component.targetLazyLoadEvent).toBe(event);
+ });
+ });
+
describe('Drag & Drop Functionality', () => {
it('should have CDK drag drop enabled when dragdrop is true', () => {
// Check that dragdrop is enabled on component
diff --git a/packages/optimus-ui/src/picklist/picklist.ts b/packages/optimus-ui/src/picklist/picklist.ts
index 5cc5935a90..6c6ea66f03 100755
--- a/packages/optimus-ui/src/picklist/picklist.ts
+++ b/packages/optimus-ui/src/picklist/picklist.ts
@@ -22,13 +22,14 @@ import {
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { find, findIndexInList, isEmpty, setAttribute, uuid } from '@openng/optimus-ui-utils';
-import { FilterService, PrimeTemplate, SharedModule } from '@openng/optimus-ui/api';
+import { FilterService, PrimeTemplate, ScrollerOptions, SharedModule } from '@openng/optimus-ui/api';
import { BaseComponent, PARENT_INSTANCE } from '@openng/optimus-ui/basecomponent';
import { Bind, BindModule } from '@openng/optimus-ui/bind';
import { ButtonModule, ButtonProps } from '@openng/optimus-ui/button';
import { AngleDoubleDownIcon, AngleDoubleLeftIcon, AngleDoubleRightIcon, AngleDoubleUpIcon, AngleDownIcon, AngleLeftIcon, AngleRightIcon, AngleUpIcon } from '@openng/optimus-ui/icons';
import { Listbox, ListboxChangeEvent } from '@openng/optimus-ui/listbox';
import { Ripple } from '@openng/optimus-ui/ripple';
+import { ScrollerLazyLoadEvent } from '@openng/optimus-ui/scroller';
import { Nullable, VoidListener } from '@openng/optimus-ui/ts-helpers';
import {
PickListFilterOptions,
@@ -169,6 +170,11 @@ const PICKLIST_INSTANCE = new InjectionToken('PICKLIST_INSTANCE');
[filterPlaceHolder]="sourceFilterPlaceholder"
[dragdrop]="dragdrop"
[dropListData]="source()"
+ [lazy]="sourceLazy"
+ [virtualScroll]="sourceVirtualScroll"
+ [virtualScrollItemSize]="sourceVirtualScrollItemSize"
+ [virtualScrollOptions]="sourceVirtualScrollOptions"
+ (onLazyLoad)="onSourceLazyLoad.emit($event)"
(onDrop)="onDrop($event, SOURCE_LIST)"
(onFilter)="onFilter($event.originalEvent, SOURCE_LIST)"
[pt]="ptm('pcListbox')"
@@ -312,6 +318,11 @@ const PICKLIST_INSTANCE = new InjectionToken('PICKLIST_INSTANCE');
[filterPlaceHolder]="targetFilterPlaceholder"
[dragdrop]="dragdrop"
[dropListData]="target()"
+ [lazy]="targetLazy"
+ [virtualScroll]="targetVirtualScroll"
+ [virtualScrollItemSize]="targetVirtualScrollItemSize"
+ [virtualScrollOptions]="targetVirtualScrollOptions"
+ (onLazyLoad)="onTargetLazyLoad.emit($event)"
(onDrop)="onDrop($event, TARGET_LIST)"
(onFilter)="onFilter($event.originalEvent, TARGET_LIST)"
[pt]="ptm('pcListbox')"
@@ -611,6 +622,47 @@ export class PickList extends BaseComponent {
*/
@Input({ transform: booleanAttribute }) disabled: boolean;
+ /**
+ * Defines if data of source list is loaded and interacted with in a lazy manner.
+ * @group Props
+ */
+ @Input({ transform: booleanAttribute }) sourceLazy: boolean = false;
+ /**
+ * Defines if data of target list is loaded and interacted with in a lazy manner.
+ * @group Props
+ */
+ @Input({ transform: booleanAttribute }) targetLazy: boolean = false;
+ /**
+ * Whether to use the virtual scroller feature for the source list to render the items lazily.
+ * @group Props
+ */
+ @Input({ transform: booleanAttribute }) sourceVirtualScroll: boolean | undefined;
+ /**
+ * Whether to use the virtual scroller feature for the target list to render the items lazily.
+ * @group Props
+ */
+ @Input({ transform: booleanAttribute }) targetVirtualScroll: boolean | undefined;
+ /**
+ * Height of an item in the source list for VirtualScrolling.
+ * @group Props
+ */
+ @Input({ transform: numberAttribute }) sourceVirtualScrollItemSize: number | undefined;
+ /**
+ * Height of an item in the target list for VirtualScrolling.
+ * @group Props
+ */
+ @Input({ transform: numberAttribute }) targetVirtualScrollItemSize: number | undefined;
+ /**
+ * Whether to use the scroller feature for the source list. The properties of scroller component can be used like an object in it.
+ * @group Props
+ */
+ @Input() sourceVirtualScrollOptions: ScrollerOptions | undefined;
+ /**
+ * Whether to use the scroller feature for the target list. The properties of scroller component can be used like an object in it.
+ * @group Props
+ */
+ @Input() targetVirtualScrollOptions: ScrollerOptions | undefined;
+
/**
* Name of the disabled field of a target option or function to determine disabled state.
* @group Props
@@ -781,6 +833,20 @@ export class PickList extends BaseComponent {
*/
@Output() onTargetFilter: EventEmitter = new EventEmitter();
+ /**
+ * Callback to invoke on lazy load of the source list, requires the virtualScroll to be enabled.
+ * @param {ScrollerLazyLoadEvent} event - Scroller lazy load event.
+ * @group Emits
+ */
+ @Output() onSourceLazyLoad: EventEmitter = new EventEmitter();
+
+ /**
+ * Callback to invoke on lazy load of the target list, requires the virtualScroll to be enabled.
+ * @param {ScrollerLazyLoadEvent} event - Scroller lazy load event.
+ * @group Emits
+ */
+ @Output() onTargetLazyLoad: EventEmitter = new EventEmitter();
+
/**
* Callback to invoke when the list is focused
* @param {Event} event - Browser event.