Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions apps/docs/doc/picklist/lazy-doc.ts
Original file line number Diff line number Diff line change
@@ -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: `
<app-docsectiontext>
<p>
When dealing with huge datasets, new chunks of data can be loaded on demand instead of all at once. Enable <i>sourceVirtualScroll</i> along with <i>sourceLazy</i> to render the source list with a <a href="/scroller">Scroller</a> and
fetch data as the user scrolls, using the <i>onSourceLazyLoad</i> event to request the next chunk.
</p>
</app-docsectiontext>
<div class="card">
<p-picklist
[source]="sourceProducts()"
[target]="targetProducts()"
[responsive]="true"
breakpoint="1400px"
[sourceLazy]="true"
[sourceVirtualScroll]="true"
[sourceVirtualScrollItemSize]="41"
[sourceStyle]="{ height: '20rem' }"
[targetStyle]="{ height: '20rem' }"
(onSourceLazyLoad)="onSourceLazyLoad($event)"
>
<ng-template let-item #item>
{{ item?.name }}
</ng-template>
</p-picklist>
</div>
<app-code [extFiles]="['Product']"></app-code>
`
})
export class LazyDoc {
totalSourceCount = 10000;

sourceProducts = signal<any[]>(Array.from({ length: this.totalSourceCount }, () => ({}) as any));

targetProducts = signal<any[]>([]);

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
);
}
}
6 changes: 6 additions & 0 deletions apps/docs/pages/picklist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -35,6 +36,11 @@ export class PickListDemo {
label: 'Template',
component: TemplateDoc
},
{
id: 'lazy',
label: 'Lazy Load',
component: LazyDoc
},
{
id: 'accessibility',
label: 'Accessibility',
Expand Down
88 changes: 88 additions & 0 deletions packages/optimus-ui/src/picklist/picklist.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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)"
Expand All @@ -44,6 +51,8 @@ import { PickList } from './picklist';
(onTargetSelect)="onTargetSelect($event)"
(onSourceReorder)="onSourceReorder($event)"
(onTargetReorder)="onTargetReorder($event)"
(onSourceLazyLoad)="onSourceLazyLoad($event)"
(onTargetLazyLoad)="onTargetLazyLoad($event)"
>
<ng-template pTemplate="item" let-item>
<div class="item-template">{{ item.name }}</div>
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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
Expand Down
68 changes: 67 additions & 1 deletion packages/optimus-ui/src/picklist/picklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -169,6 +170,11 @@ const PICKLIST_INSTANCE = new InjectionToken<PickList>('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')"
Expand Down Expand Up @@ -312,6 +318,11 @@ const PICKLIST_INSTANCE = new InjectionToken<PickList>('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')"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -781,6 +833,20 @@ export class PickList extends BaseComponent {
*/
@Output() onTargetFilter: EventEmitter<PickListTargetFilterEvent> = new EventEmitter<PickListTargetFilterEvent>();

/**
* 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<ScrollerLazyLoadEvent> = new EventEmitter<ScrollerLazyLoadEvent>();

/**
* 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<ScrollerLazyLoadEvent> = new EventEmitter<ScrollerLazyLoadEvent>();

/**
* Callback to invoke when the list is focused
* @param {Event} event - Browser event.
Expand Down