26

What I'm Doing
I have a component that hides/shows using *ngIf based on a simple Boolean. When the component becomes visible I want to apply focus to a child element within its template.

The Problem
If I flip the Boolean value the component shows correctly but if I then try and get a reference to the child element using this._elRef.nativeElement.querySelector("div#test") it just comes back null. If I wait a few seconds the same code will return the reference to the element as I expected.

Speculation
I'm assuming that after flipping the Boolean angular goes through a whole rendering cycle to reveal the newly visible component and that this has not finished by the time I apply the querySelector() in the next line.

What I'd Like To Know
So what I'm wondering is, how can I be sure that my ngIf has taken effect and the elements are their in the DOM to be selected?
Is there such a thing as a callback for ngIf or can I force the view to update and get a callback from that?

I hope this makes sense. It has been a long day (long week) and I'm super tired.
Thanks all

If it helps, I'm using Angular2 v2.0.0-beta.15

popClingwrap
  • 3,919
  • 5
  • 26
  • 44

2 Answers2

38

If you flip the boolean value to true and in the next line of code you try to get a reference to the component or DOM element controlled by NgIf... well, that component or DOM element doesn't exist yet. Angular doesn't run in parallel with your code. Your JavaScript callback has to finish, then Angular (change detection) runs, which will notice the boolean value change and create the component or DOM element and insert it into the DOM.

To fix your issue, call setTimeout(callbackFn, 0) after you flip the boolean value. This adds your callbackFn to the JavaScript message queue. This will ensure that Angular (change detection) runs before your callback function. Hence, when your callbackFn executes, the element you want to focus should now exist. Using setTimeout(..., 0) ensures that your callbackFn gets called in the next turn of the JavaScript event loop.

This technique of using setTimeout(..., 0) is used in the LifeCycle hooks dev guide when discussing the AfterView* hooks.

Here are a few other examples, if you need more details:

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • How robust is the `setTimeout()` solution? It has solved my issue using a delay of 0 but this feels kind of hacky. Is the timeout not completely separate to the rest of the code and therefore could there not be times when view updates require longer to render and the delay would need to be increased? – popClingwrap May 22 '16 at 12:16
  • 1
    @popClingwrap, please see my answer edits. I tried to add some more details. Using `setTimeout()` is not hacky at all, and it is robust. It is the normal JavaScript way to defer execution of some code. In this case, we need to defer the focus until after Angular creates the DOM element. There shouldn't be any "longer to render" cases. It should always be one turn of the event loop to render whatever DOM changes that Angular change detection makes. – Mark Rajcok May 23 '16 at 17:20
  • 1
    That is some interesting stuff that I didn't know before regarding the actual guts of Javascript. Thank you for the added detail it really helped and has given a whole load of new stuff to read up on and become confused by :) – popClingwrap May 29 '16 at 17:25
  • i ran into this problem today....took me a while to figure out what was happening and took about as long to find this thread that provided the solution. Code executing faster than the DOM was rendering, thus causing an error because the DOM element did not exist yet. This setTimeout did the trick....and more importantly, I have a deeper understanding of setTimeout. Thanks! – rolinger Nov 15 '21 at 11:05
16

This question is fairly old, and the current solution may not have been available at that time.

The setTimeout() method is perfectly viable, but has a significant downside. If you just set a class to position an element, like I did, you get a jumpy result, since the code is executed after the angular loop.

Using ChangeDetectorRef produces a result that does not jump.

So instead of this:

class Foo {
  public isDisplayed = false;

  constructor(@Inject(ElementRef) private elementRef: ElementRef) {
  }

  public someMethod(): void {
     this.isDisplayed = true;
     setTimeout(() => {
         const child = this.elementRef.nativeElement.querySelector('.child-element');
         // ...
     });
  }
}

You could do this:

class Foo {
  public isDisplayed = false;

  constructor(@Inject(ElementRef) private elementRef: ElementRef,
              @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef) {
  }

  public someMethod(): void {
     this.isDisplayed = true;
     this.changeDetectorRef.detectChanges();
     const child = this.elementRef.nativeElement.querySelector('.child-element');
     // ...
  }
}