In my Angular 19 application, I have a component with a vertically scrollable window that hides content outside the visible area. The window extends beyond the top and bottom edges, and the content can be centered by pressing a button. There is no horizontal scrolling.
The initial centering of the content is achieved using an effect in the constructor:
element = viewChild.required<ElementRef>('myWindow');
#scrollTo = computed(() => {
const nativeElement = this.element().nativeElement;
const scrollHeight: number = nativeElement.scrollHeight;
const offsetHeight: number = nativeElement.offsetHeight;
return 4 + (scrollHeight - offsetHeight) / 2;
});
constructor() {
effect(() =>
this.element().nativeElement.scrollTo(0, this.#scrollTo())
);
}
Most of the time, the scrollHeight
is 700px and the offsetHeight
is 300px, and everything works correctly. However, about one out of ten to fifteen refreshes in Chrome, both heights are the same at 38px, causing the centering to fail.
Hard-coding the scroll-to value does not solve the problem, and it's not a feasible solution either.
I suspect this issue may be due to a race condition between Chrome's layout calculation and the scrollTo
signal calculation. Any ideas on how to fix this calculation or behavior without introducing a delay in the component constructor?
Here's a StackBlitz demonstration of the problem; you can modify the window component's constructor to test different scenarios.
UPDATES
I've reported an Angular bug as it seems to be a framework issue. A workaround is required until an Angular fix is provided.
As mentioned by Matthieu Riegler, the correct effect for DOM interactions is renderAfterEffect
. Unfortunately, this approach currently fails without a timeout or by disabling Hot Module Replacement (HMR):
constructor() {
renderAfterEffect(() =>
setTimeout(() =>
this.element().nativeElement.scrollTo(0, this.#scrollTo()),
200
)
);
}
As pointed out by Naren Murali in the second part of their answer, adding a timeout delay can circumvent the problem:
constructor() {
effect(() =>
setTimeout(() =>
this.element().nativeElement.scrollTo(0, this.#scrollTo()),
200
)
);
}
However, according to Matthieu Riegler, the correct workaround should involve using renderAfterEffect
.