diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html index c3f1866c83..366398e444 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html @@ -7,6 +7,11 @@ (activate)="tableActivate.emit($event)" (select)="onSelect($event)" [selected]="selected" + (scroll)="onScroll($event)" + [scrollbarV]="infiniteScroll" + [style.height]="getTableHeight()" + [loadingIndicator]="infiniteScroll && isLoading" + [footerHeight]="infiniteScroll ? false : 50" > @if(selectable) { diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts index 2a7bd925c4..98132bc5f6 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts @@ -10,16 +10,16 @@ import { Input, LOCALE_ID, OnChanges, + OnDestroy, Output, signal, SimpleChanges, TemplateRef, TrackByFunction, - ViewChild, } from '@angular/core'; import { AsyncPipe, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; -import { Observable, filter, map } from 'rxjs'; +import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { NgxDatatableModule, SelectionType } from '@swimlane/ngx-datatable'; @@ -75,7 +75,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; templateUrl: './extensible-table.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ExtensibleTableComponent implements OnChanges, AfterViewInit { +export class ExtensibleTableComponent implements OnChanges, AfterViewInit, OnDestroy { readonly #injector = inject(Injector); readonly getInjected = this.#injector.get.bind(this.#injector); protected readonly cdr = inject(ChangeDetectorRef); @@ -113,11 +113,17 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn this._selectionType = typeof value === 'string' ? SelectionType[value] : value; } _selectionType: SelectionType = SelectionType.multiClick; - - + @Input() selected: any[] = []; @Output() selectionChange = new EventEmitter(); + // Infinite scroll configuration + @Input() infiniteScroll = false; + @Input() isLoading = false; + @Input() scrollThreshold = 10; + @Output() loadMore = new EventEmitter(); + @Input() tableHeight: number; + hasAtLeastOnePermittedAction: boolean; readonly propList: EntityPropList; @@ -129,6 +135,12 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn // Signal for actions column width private readonly _actionsColumnWidth = signal(DEFAULT_ACTIONS_COLUMN_WIDTH); + // Infinite scroll: debounced load more subject + private readonly loadMoreSubject = new Subject(); + private readonly loadMoreSubscription = this.loadMoreSubject + .pipe(debounceTime(100), distinctUntilChanged()) + .subscribe(() => this.triggerLoadMore()); + readonly columnWidths = computed(() => { const actionsColumn = this._actionsColumnWidth(); const widths = [actionsColumn]; @@ -216,7 +228,6 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn return record; }); - } isVisibleActions(rowData: any): boolean { @@ -247,10 +258,50 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn this.selectionChange.emit(selected); } + onScroll(scrollEvent: Event): void { + if (!this.shouldHandleScroll()) { + return; + } + + const target = scrollEvent.target as HTMLElement; + if (!target) { + return; + } + + if (this.isNearScrollBottom(target)) { + this.loadMoreSubject.next(); + } + } + + private shouldHandleScroll(): boolean { + return this.infiniteScroll && !this.isLoading; + } + + private isNearScrollBottom(element: HTMLElement): boolean { + const { offsetHeight, scrollTop, scrollHeight } = element; + return offsetHeight + scrollTop >= scrollHeight - this.scrollThreshold; + } + + private triggerLoadMore(): void { + this.loadMore.emit(); + } + + getTableHeight() { + if (!this.infiniteScroll) return 'auto'; + + return this.tableHeight ? `${this.tableHeight}px` : 'auto'; + } + ngAfterViewInit(): void { - this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => { + if (!this.infiniteScroll) { + this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => { this.data = []; this.cdr.markForCheck(); }); + } + } + + ngOnDestroy(): void { + this.loadMoreSubscription.unsubscribe(); } }