最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Angular: Wait until all child components have rendered before running code - Stack Overflow

programmeradmin12浏览0评论

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++);
    })
}

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++);
    })
}
Share Improve this question edited Jan 21, 2022 at 19:27 Pytth asked Jan 21, 2022 at 19:09 PytthPytth 4,1761 gold badge27 silver badges31 bronze badges 2
  • I bet there's some elegant solution (would love to read it, so I'll watch this too!), but off the top of my head, with my limited experience - I'd probably use a BehaviorSubject as number and each ponent would increase that number by 1 in their own ngAfterViewInit, and I'd subscribe to it from your top level. Then, if the number reached some x amount, I'd run the code. – Misha Mashina Commented Jan 21, 2022 at 19:41
  • 1 @MishaMashina Thanks for the tip! Also, if you are watching the question, give it an upvote for more visibility. Your idea of having each ponent increase the number would work for a small application, but because I am working on a massive enterprise app, that would be untenable here. – Pytth Commented Jan 21, 2022 at 19:43
Add a ment  | 

4 Answers 4

Reset to default 6

I ended up finding the solution I was looking for.

The child elements were not rendered in time likely because they were responding to some outstanding http event that hadn't yet returned a response. After reading through the documentation for change detection and ngZone, it looked like outstanding http events count as "micro tasks."

The below code is what worked for me.

ngOnInit() {
    this.ngZone.onMicrotaskEmpty.pipe(take(1)).subscribe((a) => {
      this.initiateElementEventListeners();
    });
}

ngAfterViewInit lifecycle hook that angular calls after it creates all the ponent'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 ponents rendered here, write you code
    });
  }
}

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.

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.

发布评论

评论列表(0)

  1. 暂无评论