skip to Main Content

Embedded in a large Angular 19 application, I have a component with a vertically scroll-able window where the content outside the window is hidden, i.e., the window content extends beyond the top and bottom of the fixed window and the content can be centered with a button press. There is no horizontal scrolling.

Initial centering of the content is done with an effect in the constructor:

element = viewChild.required<ElementRef>('my-container');

#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 scrollHeight is 700px and offsetHeight is 300px and things work properly; however, about one in ten or fifteen refreshes (in Chrome) the scrollHeight and offsetHeight are the same, in this case 38px which cause the centering to fail.

As you might guess, hard-coding the scroll-to value does not fix the problem (and it would not be a viable solution, either).

I’m guessing this is race-condition between the Chrome’s layout calculation and the scrollTo signal calculation? Any ideas how to fix this calculation and/or behavior, without introducing a delay in the component constructor?


As Naren Murali noted in the second part of his answer, a delay will work-around the problem; with the following I didn’t see a failed centering in fifty+ refreshes:

constructor() {
  effect(() => 
    setTimeout(() =>
      this.element().nativeElement.scrollTo(0, this.#scrollTo()),
      200
    )
  );
}

Angular bug report:

https://github.com/angular/angular/issues/59426

2

Answers


  1. You should use the ngAfterViewInit hook which triggers after the view(HTML) is initialized.

    We do not need an effect here, just a direct call will suffice.

    If it does not work, try wrapping in a setTimeout and execute.

    ngAfterViewInit() {
        this.element().nativeElement.scrollTo(0, this.#scrollTo());
    }
    

    Wrap your code in a setTimeout so that the rendering might complete.

    constructor() {
        effect(() => {
            setTimeout(() => {
                this.element().nativeElement.scrollTo(0, this.#scrollTo());
            });
        }); 
    }
    
    Login or Signup to reply.
  2. This effect should actually be an afterRenderEffect() :

    constructor() {
      afterRenderEffect(() => 
        this.element().nativeElement.scrollTo(0, this.#scrollTo())
      );
    }
    

    effect is known to run before the sync process, while afterRenderEffect() runs after the app has been rendered. This API is specifically recommended for this kind of cases where you want to read & alter the DOM.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search