56

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

isherwood
  • 58,414
  • 16
  • 114
  • 157
Tom
  • 3,807
  • 4
  • 33
  • 58

8 Answers8

54

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)

Tom
  • 3,807
  • 4
  • 33
  • 58
  • 2
    why should I use setTimeout here? what makes it different from direct call of desired method? – miro Oct 04 '16 at 20:40
  • 4
    What you are asking here is basically the original question. As I said in the in explanation we need to fork a child thread here. In my original question I had a problem with 'ngAfterViewInit', because angular doesn't let you update view children in 'ngAfterViewInit'. That's why you need to leave the main thread here, if you invoke directly you have exactly the problem I described in the question. Threading out does the manipulationon another thread, thus angular will not explode. (setTimeout as a way of threading in js). Does directly invoking this now work with the release version? – Tom Oct 10 '16 at 08:28
  • 1
    @Tom this seems to be also the "official" proposed solution in Angular docs themselves. Just search for `tick_then` both in [Lifecycle Hooks](https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html) Guide and in its corresponding [full source code](https://github.com/angular/angular.io/blob/8cab0ef00797036d2281ab3b3eec9a7707326c46/public/docs/_examples/lifecycle-hooks/ts/app/logger.service.ts#L25) – superjos Dec 26 '16 at 20:57
  • Hi, `setTimeout` doesn't really helps. On my project, it works on my localhost machine (I do not know why) but while after put up into production live server, it run too early before the cycle finish still. – claudchan Jun 16 '17 at 02:54
  • 3
    This actually worked for me. Any idea on how to handle this less hacky, e.g. is there already something implemented by the Angular Team? – Nicky Jun 21 '17 at 00:32
  • 1
    @Nicky would love to see a less hacky variant. It still feels wrong. But at the time this (I think) was from an official angular 2 example. However I haven't worked with a2 since, so maybe something changed in that regard. A2 was always quite fast moving. – Tom Jun 21 '17 at 08:05
  • 1
    updated doc ref to `tick_then`: https://angular.io/guide/lifecycle-hooks#abide-by-the-unidirectional-data-flow-rule – TmTron May 10 '19 at 12:00
  • When ngAfterViewInit lifecycle method is hit, it is too late to update the view's display. It has to wait one turn before it can display the seconds. Hence the setTimeout. Read more here: https://angular.io/guide/component-interaction – Ε Г И І И О Oct 03 '19 at 05:04
  • @JanneHarju also answars that use the words "always" and "never" tend to be always wrong due to the nature of generally applicable statements. I'm open for constructive feedback but this is just meaningless. – Tom Apr 17 '20 at 09:45
  • It is just when you are hiding the real problems with bad solution. Ofcause there is some place where it is ok to use settimeout. When you call something inside settimeout it is outside angular zone and angular can behave weird. – Janne Harju Apr 17 '20 at 11:18
  • @Tom after all these years, did you manage to find a less hacky solution? I've stumbled upon the same problem as you and would love to move on to something cleaner looking. – Philipp Doerner Aug 02 '21 at 18:34
  • @PhilippDoerner Hey, no. But I didn't work with angular for over 3 years now. So I'm more or less out of that game :) – Tom Aug 06 '21 at 09:57
8

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');
    });
  }
}
williamsandonz
  • 15,864
  • 23
  • 100
  • 186
5

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();
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
2

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

Valex
  • 1,149
  • 1
  • 8
  • 14
  • That is acutally my current implementation, every child component emits an event and I collect those, once the final event arrives I trigger the action. But 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, for my taste that's not generic enough for something, that should be generic. – Tom Aug 04 '16 at 11:01
  • yeaeh, it's not the most elegant solution. Have you tried AfterViewChecked hook? – Valex Aug 04 '16 at 11:04
  • I tried, AfterViewChecked gets fired everytime Angular checks the view if it needs updates. So it gets fired very often, and after a few cycles it also runs into the view update error. – Tom Aug 04 '16 at 12:09
  • 1
    I was wondering around and i found this https://angular.io/docs/ts/latest/api/core/index/ViewChildren-var.html , this one fires after the ngAfterViewInit and as far as i can see allow to manipulation – Valex Aug 05 '16 at 07:42
  • 1
    Which one fires after ngAfterViewInit? On the doc page they explain that view children are available inside ngAfterViewInit. Did you link the wrong API page or am I blind right now :) Also still looking for a clean solutions. Will update this thread if I find something. – Tom Aug 08 '16 at 09:40
  • If you tested this you would see it doesn't work as the child OnInt is called after the parent triggers any events so the parent event is never caught by the child event binding. – Wancieho Aug 05 '19 at 09:09
2

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.

Inigo
  • 8,110
  • 18
  • 62
  • 110
  • this only seems to work, when you don't deal with dynamic data. in my case, i have serveral child-components that are shown based on the current date. which basically means: changing database-data and changing amount of components to be observed. in it's current form, this answer does not solve this particular issue. – Argee Nov 01 '18 at 08:26
0

because html is still singlethreaded :)

<child-component  [forms]="forms"></child-component>
<ng-container>{{load(forms)}}</ng-container>


export class ChildComponent{

    @Input() forms!: any;
  
    formContact = this.fb.group({... })
  
    ngOnInit(): void {
      this.forms.formContact = this.formContact;
    }  

}

export class ParentComponent{

    forms: any = {}
    formsReady
    
    load(forms:any) {
        if(this.formsReady) return;
        this.formsReady = true;
        //forms are ready here    
    }
}
dsl400
  • 322
  • 3
  • 14
-1

You can try this. It Worked for me

async ngOnInit() {
    // method to call
   await this.myMethod(); 
  }
}
ChrisMM
  • 8,448
  • 13
  • 29
  • 48
Thomas
  • 9
  • 3
-2

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".

Yakov Fain
  • 11,972
  • 5
  • 33
  • 38
  • But that would mean the application would be dysfunctional in development mode and I would loose all benefits of the sanity checking that development mode has. – Tom Aug 04 '16 at 12:00
  • You can run in dev mode, but use setTimeout() from the lifecycle hooks to invoke the code that updates the UI. – Yakov Fain Aug 05 '16 at 16:44
  • Thats what Im doing now. Timeout with explicit time, so it starts instantly and just affects the threading behavior. – Tom Aug 09 '16 at 09:18