Angular 2 life cycle hook after all children are initialized?

Angular

Angular Problem Overview


I am looking for a concept to implement the following case:

I have a parent search component that has some components as view children / content children for displaying facets and search results. I now want to trigger a search when the application has finished loading so the user does not see an empty page.

My problem now is that I cannot find a lifecycle hook that fits my needs. The facets / search results subscribe to the search results in their respective ngOnInit. So I need a hook that gets called after all child components had finished initializing.

I have tried the following hooks on the parent component

  • ngAfterContentInit: this gets called before ngOnInit is called on the children
  • ngAfterViewInit: this one works but after the search results return the view of the children gets updated which leads to an error since actions that manipulate the view are not allowed in ngAfterViewInit

Any Idea how to tackle this problem? To me it seems I do not grasp something fundamental here.

Cheers

Tom

Angular Solutions


Solution 1 - Angular

I ended up using the ngAfterViewInit() hook in the following way:

ngAfterViewInit(){
  //stuff that doesn't do view changes
  setTimeout(_=> this.methodThatKicksOffAnotherRoundOfChanges());
}

This should be safe to use compared to other invocations of setTimeout where an actual time is set, since it should start instantly (and just affect the threading/context behaviour)

Solution 2 - Angular

If you need your Parent Component to await custom asynchronous behaviour across multiple child components then here's a good solution I cooked up.

Any child component's that initialise asynchronously extend this:

import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';

export class AsynchronouslyInitialisedComponent {
  loadedState: Subject<boolean> = new Subject<boolean>();
  loadedState$ = this.loadedState.asObservable();
  constructor() {
  }
  protected componentLoaded() {
    this.loadedState.next(true);
  }
}

Example Child component:

import {Component} from '@angular/core';
import {AsynchronouslyInitialisedComponent} from './../base_component';

@Component({
  moduleId: module.id,
  selector: 'child'
})
export class Child extends AsynchronouslyInitialisedComponent {
  constructor() {
    super();
  }
  ngOnInit() {
    //Some async behavoir:
    setTimeout(() => {
      this.componentLoaded();
    }, 10000);
  }
}

Now all your Parent component needs to do is the following:

import {Component, ViewEncapsulation, ViewChild};
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/zip';

@Component({
  moduleId: module.id,
  selector: 'parent'
})
export class Parent { 

  @ViewChild('child') child; //Just use <child #child></child> in your template

  constructor() {
  }
  ngOnInit() {
    Observable
    .zip(this.child.loadedState$, this.otherChild.loadedState$) //Add as many as you want here... 
    .subscribe(pair => {
      console.log('All child components loaded');
    });
  }
}

Solution 3 - Angular

If you also have content children (transcluded using <ng-content>) then ngAfterContentInit() is the right lifecycle callback.

https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#aftercontent

ngAfterContentInit() {
  doSomething();
}

Solution 4 - Angular

Make the child components emit an event on their Init and listen for it where you need it

Solution 5 - Angular

The OP seems to think it doesn't suit his needs, or is not elegant, but it seems that Vale Steve's answer is the most logical approach here.

In my case I needed to wait for 2 child components to initialize, which control left and right panels in my app. I have both child components declaring their initialization to a shared service:

  constructor(
      public _navigate: RouteNavigationService // service shared between all child components and parent
  ){}

  ngOnInit() {
    this._navigate.setChildInit(this.panel);
  }

Shared service (RouteNavigationService):

  private _leftChildInit  = new Subject();
  private _rightChildInit = new Subject();

  leftChildInit$          = this._leftChildInit.asObservable();
  rightChildInit$         = this._rightChildInit.asObservable();

  setChildInit(panel){
    switch(panel){
      case 'leftPanel':
        console.log('left panel initialized!');
        this._leftChildInit.next(true);
        break;
      case 'rightPanel':
        console.log('right panel initialized!');
        this._rightChildInit.next(true);
        break;
    }
  }

Then in my parent component I use the zip method to combine multiple Observables together (You could add additional child components here as well) and wait for them all to finish:

  childInitSub: Subscription;

  constructor(
      public _navigate: RouteNavigationService
  ) {
    this.childInitSub = Observable.zip( // WAIT FOR BOTH LEFT & RIGHT PANELS' ngOnInit() TO FIRE
        this._navigate.leftChildInit$,
        this._navigate.rightChildInit$).subscribe(data =>
        // Both child components have initialized... let's go!
    );
  }

The OP states in a comment

> "I don't like this, since everytime I add a new component I need to > wait for an additional onInit event and have to implement it"

But realistically all you'd have to do is make another Subject in your service for the new child component ngOnInit, and add an extra line in the parent component zip method.

Note that the accepted answer here, using ngAfterViewInit() isn't doing quite the same thing as above. In my case, using ngAfterViewInit() was producing an ExpressionChangedAfterItHasBeenCheckedError which is beyond the scope of this question, but serves to demonstrate that this approach is not actually doing the same thing.

In contrast, the above method is quite literally only triggered as a direct result of all your child components' ngOnInit() events having fired.

Solution 6 - Angular

You can try this. It Worked for me

async ngOnInit() {
    // method to call
   await this.myMethod(); 
  }
}

Solution 7 - Angular

In dev mode (default) the change detection mechanism performs an additional check of the components tree to see that you don't manipulate UI in the hooks. That's why you're getting errors.

If you're sure that your changes of the UI in ngAfterViewInit are valid, invoke enableProdMode() right before bootstrap(). This tells Angular, "Don't do the second pass during change detection".

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTomView Question on Stackoverflow
Solution 1 - AngularTomView Answer on Stackoverflow
Solution 2 - AngularwilliamsandonzView Answer on Stackoverflow
Solution 3 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 4 - AngularValexView Answer on Stackoverflow
Solution 5 - AngularInigoView Answer on Stackoverflow
Solution 6 - AngularThomasView Answer on Stackoverflow
Solution 7 - AngularYakov FainView Answer on Stackoverflow