From 677eb6dd082cd016e560fc3c9abf733f26dd5f58 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 19 Aug 2026 19:41:22 +0800 Subject: [PATCH 1/2] Keep the extensible table header aligned with its rows after a list request The scrolling element is removed while there are no rows, which resets the horizontal scroll position without the table noticing --- .../extensible-table.component.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) 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 30d1cf9796..8d485251cf 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 @@ -1,4 +1,5 @@ import { + afterNextRender, AfterViewInit, ChangeDetectionStrategy, Component, @@ -186,6 +187,10 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr }, ); + private horizontalScrollOffset = 0; + + private hasPendingHorizontalScrollOffset = false; + hasAtLeastOnePermittedAction: boolean; readonly propList: EntityPropList; @@ -232,6 +237,7 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr } this._data.set(dataValue.map((record, index) => this.prepareRecord(record, index))); + this.restoreHorizontalScrollOffset(); }); } @@ -365,11 +371,55 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr this.list() ?.requestStatus$?.pipe(filter(status => status === 'loading')) .subscribe(() => { + this.rememberHorizontalScrollOffset(); this._data.set([]); }); } } + private getBodyElement(): HTMLElement | null { + if (!this.isBrowser) { + return null; + } + + return this.table()?.element?.querySelector('datatable-body') ?? null; + } + + // ngx-datatable drops its scrolling element while there are no rows, the header offset goes stale + private rememberHorizontalScrollOffset(): void { + const body = this.getBodyElement(); + this.hasPendingHorizontalScrollOffset = true; + + // Not scrollable while a previous request is still in flight, keep the offset taken back then + if (body && body.scrollWidth > body.clientWidth) { + this.horizontalScrollOffset = body.scrollLeft; + } + } + + private restoreHorizontalScrollOffset(): void { + if (!this.hasPendingHorizontalScrollOffset) { + return; + } + + this.hasPendingHorizontalScrollOffset = false; + + afterNextRender( + () => { + const body = this.getBodyElement(); + + if (!body) { + return; + } + + body.scrollLeft = this.horizontalScrollOffset; + + // The header only follows a scroll event, scrolling to the same position does not raise one + body.dispatchEvent(new Event('scroll')); + }, + { injector: this.#injector }, + ); + } + ngOnDestroy(): void { this.loadMoreSubscription.unsubscribe(); } From 6d819603f77a0f71486841d170750d41c4b6b454 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 28 Aug 2026 14:24:17 +0800 Subject: [PATCH 2/2] Restore the extensible table scroll offset when a list request fails Also unsubscribe the request status stream with the component --- .../extensible-table.component.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) 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 8d485251cf..0bf15dff88 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 @@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, Component, computed, + DestroyRef, inject, Injector, LOCALE_ID, @@ -18,9 +19,10 @@ import { contentChild, viewChild, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; -import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; +import { Observable, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { @@ -93,6 +95,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; }) export class ExtensibleTableComponent implements AfterViewInit, OnDestroy { readonly #injector = inject(Injector); + readonly #destroyRef = inject(DestroyRef); readonly getInjected = this.#injector.get.bind(this.#injector); protected readonly locale = inject(LOCALE_ID); protected readonly config = inject(ConfigStateService); @@ -369,10 +372,18 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr ngAfterViewInit(): void { if (!this.infiniteScroll()) { this.list() - ?.requestStatus$?.pipe(filter(status => status === 'loading')) - .subscribe(() => { - this.rememberHorizontalScrollOffset(); - this._data.set([]); + ?.requestStatus$?.pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe(status => { + if (status === 'loading') { + this.rememberHorizontalScrollOffset(); + this._data.set([]); + return; + } + + // A failed request never reaches the data input, restore from here instead + if (status === 'error') { + this.restoreHorizontalScrollOffset(); + } }); } } @@ -385,7 +396,8 @@ export class ExtensibleTableComponent implements AfterViewInit, OnDestr return this.table()?.element?.querySelector('datatable-body') ?? null; } - // ngx-datatable drops its scrolling element while there are no rows, the header offset goes stale + // The element carrying the column width is gone while loading without rows, so the browser + // resets the horizontal scroll position and the header keeps the offset it had before private rememberHorizontalScrollOffset(): void { const body = this.getBodyElement(); this.hasPendingHorizontalScrollOffset = true;