'Angular: Wait until all child components have rendered before running code

I have a directive that's at the top level of the app and queries elements via document.querySelector and hands them to fromEvent. The problem is, that when the code in the directive's ngAfterViewInit runs, the DOM isn't fully rendered yet so the element query returns null.

I could run the code in setTimeout with some arbitrary amount of time, but that doesn't seem like a sustainable, and flexible solution.

Any ideas for how this can be elegantly handled?

Right now, my code looks like some variation of this:

ngAfterViewInit() {
  const trackedEl = document.querySelector('#myInput');
  const input$ = fromEvent(trackedEl, 'input')
    .pipe(
      map((inputEvent: any) => inputEvent.target.value)
    )
    .subscribe((v) => {
      console.log(`Got to the observable:: ${v}`, count++);
    })
}


Solution 1:[1]

I think your best approach here would be to use something like a ViewChild.

So assuming your querySelector targets an <input> tag your html would be like (note the #myInput decorator):

<input #myInput ....>

Your .ts file would be:

 @ViewChild('myInput', { static: true })
  input?: ElementRef<HTMLElement>;

Then because we only want to resolve the event when it is subscribed to:

const input$ = defer(() => fromEvent(this.input, 'input').pipe(
      map((inputEvent: any) => inputEvent.target.value)
    )
    .subscribe((v) => {
      console.log(`Got to the observable:: ${v}`, count++);
    });

Something to this affect, but certainly ViewChild being key here.

Solution 2:[2]

ngAfterViewInit lifecycle hook that angular calls after it creates all the component's child views. So you have to implement it on the root AppComponent. Then keep an observable into a service or state and just subscribe to it anywhere you wanna check.

root AppComponent:

export class AppComponent implements AfterViewInit {
  title = "Angular App";
  constructor(private data: DataService){}
  ngAfterViewInit() {
    this.data.rendered.next(true);
  }
}

subscribing it anywhere:

export class HighlightDirective {
  constructor(private data: DataService) {
    this.data.rendered.subscribe(rendered => {
     // all components rendered here, write you code
    });
  }
}

Solution 3:[3]

I think this could be an approach:

ngAfterViewInit() {
  defer(() => fromEvent(document.querySelector('#myInput'), 'input'))
    .pipe(
      subscribeOn(asyncScheduler),
      map((inputEvent: any) => inputEvent.target.value)
    )
    .subscribe((v) => {
      console.log(`Got to the observable:: ${v}`, count++);
    })
}

This approach is still resorting something like setTimeout, but in a more elegant way. This is achieved by using defer + subscribeOn.

By providing asyncScheduler to subscribeOn, we obtain a setTimeout-like functionality. The defer operator has been used because we want to evaluate that function which queries the DOM when source is subscribed, and recall that the subscription time is delayed by the subscribeOn operator.
So, by the time the source is subscribed, the DOM should be ready to be queried.

I know this is just a workaround for setTimeout, but I thought it would look more fancy with some interesting RxJS operators.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 SomeStudent
Solution 2 Klodian
Solution 3 Andrei Gătej