383

I am trying to learn Angular 2.

I would like to access to a child component from a parent component using the @ViewChild Annotation.

Here some lines of code:

In BodyContent.ts I have:

import { ViewChild, Component, Injectable } from 'angular2/core';
import { FilterTiles } from '../Components/FilterTiles/FilterTiles';

@Component({
    selector: 'ico-body-content',
    templateUrl: 'App/Pages/Filters/BodyContent/BodyContent.html',
    directives: [FilterTiles] 
})
export class BodyContent {
    @ViewChild(FilterTiles) ft: FilterTiles;

    public onClickSidebar(clickedElement: string) {
        console.log(this.ft);
        var startingFilter = {
            title: 'cognomi',
            values: [ 'griffin', 'simpson' ]
        }
        this.ft.tiles.push(startingFilter);
    } 
}

while in FilterTiles.ts:

 import { Component } from 'angular2/core';

 @Component({
     selector: 'ico-filter-tiles',
     templateUrl: 'App/Pages/Filters/Components/FilterTiles/FilterTiles.html'
 })
 export class FilterTiles {
     public tiles = [];

     public constructor(){};
 }

Finally here the templates (as suggested in comments):

BodyContent.html

<div (click)="onClickSidebar()" class="row" style="height:200px; background-color:red;">
    <ico-filter-tiles></ico-filter-tiles>
</div>

FilterTiles.html

<h1>Tiles loaded</h1>
<div *ngFor="#tile of tiles" class="col-md-4">
     ... stuff ...
</div>

FilterTiles.html template is correctly loaded into ico-filter-tiles tag (indeed I am able to see the header).

Note: the BodyContent class is injected inside another template (Body) using DynamicComponetLoader: dcl.loadAsRoot(BodyContent, '#ico-bodyContent', injector):

import { ViewChild, Component, DynamicComponentLoader, Injector } from 'angular2/core';
import { Body } from '../../Layout/Dashboard/Body/Body';
import { BodyContent } from './BodyContent/BodyContent';

@Component({
    selector: 'filters',
    templateUrl: 'App/Pages/Filters/Filters.html',
    directives: [Body, Sidebar, Navbar]
})
export class Filters {

    constructor(dcl: DynamicComponentLoader, injector: Injector) {
       dcl.loadAsRoot(BodyContent, '#ico-bodyContent', injector);
       dcl.loadAsRoot(SidebarContent, '#ico-sidebarContent', injector);
   } 
}

The problem is that when I try to write ft into the console log, I get undefined, and of course I get an exception when I try to push something inside the "tiles" array: 'no property tiles for "undefined"'.

One more thing: FilterTiles component seems to be correctly loaded, since I'm able to see the html template for it.

Any suggestions?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Andrea Ialenti
  • 4,380
  • 2
  • 19
  • 22
  • Looks correct. Maybe something with the template, but it isn't included in your question. – Günter Zöchbauer Jan 22 '16 at 12:54
  • 1
    Agreed with Günter. I created a plunkr with your code and simple associated templates and it works. See this link: https://plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview. We need the templates ;-) – Thierry Templier Jan 22 '16 at 13:01
  • 1
    `ft` wouldn't be set in the constructor, but in a click event handler it would be set already. – Günter Zöchbauer Jan 22 '16 at 13:11
  • 5
    You're using `loadAsRoot`, which has a [known issue](https://github.com/angular/angular/issues/6223) with change detection. Just to make sure try using `loadNextToLocation` or `loadIntoLocation`. – Eric Martinez Jan 22 '16 at 13:26
  • 1
    The problem was `loadAsRoot`. Once I replaced with `loadIntoLocation` the problem was solved. If you make your comment as answer I can mark it as accepted – Andrea Ialenti Jan 22 '16 at 15:31
  • This still happens in Angular7 – Paul Taylor Sep 05 '19 at 15:25
  • The eternal problem. jQuery was unironically better, since adding or removing from the DOM was slow and difficult, you ended up hidding things and everything just worked. – Mister Smith Jun 05 '21 at 23:21
  • Not the problem here, but the usual fixes do not work and your @ViewChild is still undefined *and* you are using transloco: There is a catch: https://github.com/ngneat/transloco/issues/375 – MKK Jan 31 '22 at 16:05

28 Answers28

542

I had a similar issue and thought I'd post in case someone else made the same mistake. First, one thing to consider is AfterViewInit; you need to wait for the view to be initialized before you can access your @ViewChild. However, my @ViewChild was still returning null. The problem was my *ngIf. The *ngIf directive was killing my controls component so I couldn't reference it.

import { Component, ViewChild, OnInit, AfterViewInit } from 'angular2/core';
import { ControlsComponent } from './controls/controls.component';
import { SlideshowComponent } from './slideshow/slideshow.component';

@Component({
  selector: 'app',
  template: `
    <controls *ngIf="controlsOn"></controls>
    <slideshow (mousemove)="onMouseMove()"></slideshow>
  `,
  directives: [SlideshowComponent, ControlsComponent],
})
export class AppComponent {
  @ViewChild(ControlsComponent) controls: ControlsComponent;

  controlsOn: boolean = false;

  ngOnInit() {
    console.log('on init', this.controls);
    // this returns undefined
  }

  ngAfterViewInit() {
    console.log('on after view init', this.controls);
    // this returns null
  }

  onMouseMove(event) {
    this.controls.show();
    // throws an error because controls is null
  }
}

EDIT
As mentioned by @Ashg below, a solution is to use @ViewChildren instead of @ViewChild.

starball
  • 20,030
  • 7
  • 43
  • 238
kenecaswell
  • 7,212
  • 1
  • 24
  • 19
  • 1
    true the ngIf will mess with things. my solution was to bubble up an event from the child component to tell the parent component that it was done initializing. mildly hacky but it works – brando Apr 22 '16 at 18:49
  • 11
    @kenecaswell So did you find the better way to solve the problem. I am also facing the same issue. I have many *ngIf so that element will be in the only after all true, but i need the element reference. Any way to solve this > – monica Sep 23 '16 at 06:59
  • 5
    I found that child component is 'undefined' in ngAfterViewInit() if using ngIf. I tried putting long timeouts but still no effect. However, the child component is available later (ie in response to click events etc). If I don't use ngIf and it is defined as expected in ngAfterViewInit(). There's more on Parent / Child communication here https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child – Matthew Hegarty Sep 30 '16 at 14:52
  • 4
    I used bootstrap `ngClass`+`hidden` class instead of `ngIf`. That worked. Thanks! – Rahmathullah M Apr 02 '17 at 17:47
  • 13
    This doesn't solve the problem, use the solution below using @ViewChildren go get a reference to the child control once it becomes available – Ashg May 16 '17 at 02:48
  • 1
    I had the same problem and after you mentioned the ngIf the penny dropped. The ngIf directive is a structural directive which means it uses the ng-template which means that the element ends up in the content DOM. This means you have to use ContentChild instead of ViewChild to access it. – Tom Maher Dec 10 '17 at 19:09
  • 68
    This just proves the "issue", right? It does not post a solution. – Miguel Ribeiro Feb 09 '18 at 11:36
  • 2
    The problem messed by ngIf can be solved by using viewchildren instead. This post has a solution that worked for me. https://expertcodeblog.wordpress.com/2018/01/12/angular-resolve-error-viewchild-annotation-returns-undefined/ – Terry Lam Aug 22 '18 at 08:21
  • 1
    Good example, but the `AppComponent` must implement `OnInit, AfterViewInit` in this case. \\ `export class AppComponent implements OnInit, AfterViewInit { ...}` \\ Then you can access the child component in `ngAfterViewInit()` method. – Leonid Dashko Nov 30 '18 at 09:37
  • or in my case the problem was bootstrap d-md-none class which essentially hides the element. – Wildhammer Apr 30 '19 at 21:01
  • How many times do I have to make this mistake and see this answer to remember?? always nfIf ha - thanks. – java-addict301 Nov 01 '19 at 15:42
  • 11
    I don't understand why this was the chosen answer. It doesn't provide any solution... – HankScorpio Mar 02 '20 at 21:18
  • My problem was that I called a function in the child in ngOnInit. Moving it to ngAfterViewInit solved it – TheCondorIV Jun 01 '20 at 10:46
  • Please check the answer below using `@ViewChidlren` and `ngAfterViewInit` hook. It is the correct way to do it. – Andrew Pomorski Oct 05 '20 at 07:12
  • This is Perfect Example, but would like to mention one thing that (For beginner of Angular). you need to define child component in template of parent component with appropriate selector. – Sunny Vakil Apr 22 '21 at 02:57
  • A very good in-depth explanation provided by Angular docs on this issue: https://angular.io/guide/static-query-migration#what-does-this-flag-mean-and-why-is-it-necessary – Aakash Goplani Nov 06 '21 at 07:51
  • Can someone post the link to the answer here? – user1034912 Sep 07 '22 at 23:41
246

The issue as previously mentioned is the ngIf which is causing the view to be undefined. The answer is to use ViewChildren instead of ViewChild. I had similar issue where I didn't want a grid to be shown until all the reference data had been loaded.

html:

   <section class="well" *ngIf="LookupData != null">
       <h4 class="ra-well-title">Results</h4>
       <kendo-grid #searchGrid> </kendo-grid>
   </section>

Component Code

import { Component, ViewChildren, OnInit, AfterViewInit, QueryList  } from '@angular/core';
import { GridComponent } from '@progress/kendo-angular-grid';

export class SearchComponent implements OnInit, AfterViewInit
{
    //other code emitted for clarity

    @ViewChildren("searchGrid")
    public Grids: QueryList<GridComponent>

    private SearchGrid: GridComponent

    public ngAfterViewInit(): void
    {
        
        this.Grids.changes.subscribe((comps: QueryList <GridComponent>) =>
        {
            this.SearchGrid = comps.first;
        });

       
    }
}

Here we are using ViewChildren on which you can listen for changes. In this case any children with the reference #searchGrid.

starball
  • 20,030
  • 7
  • 43
  • 238
Ashg
  • 2,526
  • 1
  • 10
  • 5
  • 6
    I would like add that in some cases when you try change eg. `this.SearchGrid` properties you should use syntax like `setTimeout(()=>{ ///your code here }, 1); ` to avoid Exception: Expression has changed after it was checked – rafalkasa Apr 12 '17 at 19:58
  • 3
    How do you do this if you want to place your #searchGrid tag on a normal HTML element instead of an Angular2 element? (For instance,
    and this is inside of an *ngIf block?
    – Vern Jensen May 31 '17 at 03:56
  • 1
    this is the correct answer for my use case! Thanks I need to access an component as it comes available through ngIf= – Frozen_byte Jul 21 '17 at 13:04
  • 1
    this works perfect on ajax responses, now the `*ngIf`works, and after render we can save a ElementRef from the dinamic components. – elporfirio Oct 24 '17 at 16:09
  • 5
    Also don't forget to assign it to a subscription and then unSubscribe from it – tam.teixeira Dec 11 '17 at 02:41
  • 1
    This worked for me, and should be the chosen answer imo – HankScorpio Mar 02 '20 at 21:18
  • Thank you, I've had an issue with that for a long time and this is the only thing that has worked for me. I think this should be the accepted answer as well. – Andrew Pomorski Oct 05 '20 at 07:11
  • if you subscribe in ngAfterViewInit() it will subscribe every time angular detect changes, i think the subscribe should go in ngOnInit(). – Cristian Sepulveda May 22 '22 at 19:00
  • I was accessing the viewchildren directly in ngAfterViewInit but inside the QueryList I noticed _results: array(0) even though the length was 2! Perhaps I was accessing it too early, so I wrapped it in a QueryList.changes.subscribe(..) to defer execution and it works (for now.) I struggled with this for half a day, I feel emotions of gratitude :) – spl Jan 17 '23 at 23:07
85

You could use a setter for @ViewChild()

@ViewChild(FilterTiles) set ft(tiles: FilterTiles) {
    console.log(tiles);
};

If you have an ngIf wrapper, the setter will be called with undefined, and then again with a reference once ngIf allows it to render.

My issue was something else though. I had not included the module containing my "FilterTiles" in my app.modules. The template didn't throw an error but the reference was always undefined.

e_i_pi
  • 4,590
  • 4
  • 27
  • 45
parliament
  • 21,544
  • 38
  • 148
  • 238
  • 8
    This isn't working for me--I get the first undefined, but I don't get the second call w/ the reference. App is an ng2...is this ng4+ feature? – Jay Cummins Jul 20 '18 at 18:55
  • @Jay I believe this is because you have not registered the component with Angular, in this case ```FilterTiles```. I've encountered that issue for that reason before. – parliament Jul 27 '18 at 16:13
  • 3
    Works for Angular 8 using #paginator on html element and annotation like `@ViewChild('paginator', {static: false})` – Qiteq Jan 20 '20 at 08:21
  • 1
    Is this a callback for ViewChild changes? – Yasser Nascimento May 06 '20 at 19:17
  • can you please provider the code for the getter also? –  Aug 02 '20 at 23:45
  • this answer is slightly different, need to set the value https://stackoverflow.com/a/41095677/13889515 –  Aug 02 '20 at 23:57
73

What solved my problem was to make sure static was set to false.

@ViewChild(ClrForm, {static: false}) clrForm;

With static turned off, the @ViewChild reference gets updated by Angular when the *ngIf directive changes.

Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
32

This worked for me.

My component named 'my-component', for example, was displayed using *ngIf="showMe" like so:

<my-component [showMe]="showMe" *ngIf="showMe"></my-component>

So, when the component is initialized the component is not yet displayed until "showMe" is true. Thus, my @ViewChild references were all undefined.

This is where I used @ViewChildren and the QueryList that it returns. See angular article on QueryList and a @ViewChildren usage demo.

You can use the QueryList that @ViewChildren returns and subscribe to any changes to the referenced items using rxjs as seen below. @ViewChild does not have this ability.

import { Component, ViewChildren, ElementRef, OnChanges, QueryList, Input } from '@angular/core';
import 'rxjs/Rx';

@Component({
    selector: 'my-component',
    templateUrl: './my-component.component.html',
    styleUrls: ['./my-component.component.css']
})
export class MyComponent implements OnChanges {

  @ViewChildren('ref') ref: QueryList<any>; // this reference is just pointing to a template reference variable in the component html file (i.e. <div #ref></div> )
  @Input() showMe; // this is passed into my component from the parent as a    

  ngOnChanges () { // ngOnChanges is a component LifeCycle Hook that should run the following code when there is a change to the components view (like when the child elements appear in the DOM for example)
    if(showMe) // this if statement checks to see if the component has appeared becuase ngOnChanges may fire for other reasons
      this.ref.changes.subscribe( // subscribe to any changes to the ref which should change from undefined to an actual value once showMe is switched to true (which triggers *ngIf to show the component)
        (result) => {
          // console.log(result.first['_results'][0].nativeElement);                                         
          console.log(result.first.nativeElement);                                          

          // Do Stuff with referenced element here...   
        } 
      ); // end subscribe
    } // end if
  } // end onChanges 
} // end Class

Hope this helps somebody save some time and frustration.

Joshua Dyck
  • 2,113
  • 20
  • 25
  • 3
    Indeed you solution seems to be the best approach listed so far. NOTE We have to keep in mind that top 73 solution is now DEPRECATED... since directive:[...] declaration is NO longer supported in Angular 4. IOW it WON'T work in Angular 4 scenario – PeteZaria Jul 31 '17 at 14:07
  • 6
    Don't forget to unsubscribe, or use `.take(1).subscribe()`, but excellent answer, thank you so much! – Blair Connolly Feb 08 '18 at 20:36
  • 2
    Excellent solution. I have subscribed to the ref changes in ngAfterViewInit() rather than the ngOnChanges(). But I had to add a setTimeout to get rid of ExpressionChangedAfterChecked error – Josf Feb 09 '18 at 00:18
  • This should be marked as the actual solution. Thanks a lot! – platzhersh Mar 28 '19 at 12:10
20

My solution to this was to replace *ngIf with [hidden]. Downside was all the child components were present in the code DOM. But worked for my requirements.

Liam
  • 27,717
  • 28
  • 128
  • 190
Hima Susan
  • 401
  • 1
  • 4
  • 8
11

In my case, I had an input variable setter using the ViewChild, and the ViewChild was inside of an *ngIf directive, so the setter was trying to access it before the *ngIf rendered (it would work fine without the *ngIf, but would not work if it was always set to true with *ngIf="true").

To solve, I used Rxjs to make sure any reference to the ViewChild waited until the view was initiated. First, create a Subject that completes when after view init.

export class MyComponent implements AfterViewInit {
  private _viewInitWaiter$ = new Subject();

  ngAfterViewInit(): void {
    this._viewInitWaiter$.complete();
  }
}

Then, create a function that takes and executes a lambda after the subject completes.

private _executeAfterViewInit(func: () => any): any {
  this._viewInitWaiter$.subscribe(null, null, () => {
    return func();
  })
}

Finally, make sure references to the ViewChild use this function.

@Input()
set myInput(val: any) {
    this._executeAfterViewInit(() => {
        const viewChildProperty = this.viewChild.someProperty;
        ...
    });
}

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;
nikojpapa
  • 660
  • 1
  • 8
  • 16
10

My workaround was to use [style.display]="getControlsOnStyleDisplay()" instead of *ngIf="controlsOn". The block is there but it is not displayed.

@Component({
selector: 'app',
template:  `
    <controls [style.display]="getControlsOnStyleDisplay()"></controls>
...

export class AppComponent {
  @ViewChild(ControlsComponent) controls:ControlsComponent;

  controlsOn:boolean = false;

  getControlsOnStyleDisplay() {
    if(this.controlsOn) {
      return "block";
    } else {
      return "none";
    }
  }
....
Liam
  • 27,717
  • 28
  • 128
  • 190
Calderas
  • 111
  • 1
  • 4
  • Have a page where a list of items is shown in a table, or the edit item is shown, based on the value of showList variable. Got rid of the annoying console error by using the [style.display]="!showList", combined with *ngIf="!showList". – razvanone Apr 23 '18 at 10:59
5

It must work.

But as Günter Zöchbauer said there must be some other problem in template. I have created kinda Relevant-Plunkr-Answer. Pleas do check browser's console.

boot.ts

@Component({
selector: 'my-app'
, template: `<div> <h1> BodyContent </h1></div>

      <filter></filter>

      <button (click)="onClickSidebar()">Click Me</button>
  `
, directives: [FilterTiles] 
})


export class BodyContent {
    @ViewChild(FilterTiles) ft:FilterTiles;

    public onClickSidebar() {
        console.log(this.ft);

        this.ft.tiles.push("entered");
    } 
}

filterTiles.ts

@Component({
     selector: 'filter',
    template: '<div> <h4>Filter tiles </h4></div>'
 })


 export class FilterTiles {
     public tiles = [];

     public constructor(){};
 }

It works like a charm. Please double check your tags and references.

Thanks...

micronyks
  • 54,797
  • 15
  • 112
  • 146
  • 3
    If the problem is the same as mine, to duplicate you would need to put a *ngIf in the template around .. Apparently if the ngIf returns false, ViewChild doesn't get wired and returns null – Dan Chase May 11 '18 at 18:31
  • This doesn't address the OP's problem with a late(r) initialized/available component/element where the viewChild reference is desired for. – Youp Bernoulli Oct 20 '21 at 06:25
5

For me using ngAfterViewInit instead of ngOnInit fixed the issue :

export class AppComponent implements OnInit {
  @ViewChild('video') video;
  ngOnInit(){
    // <-- in here video is undefined
  }
  public ngAfterViewInit()
  {
    console.log(this.video.nativeElement) // <-- you can access it here
  }
}
yaya
  • 7,675
  • 1
  • 39
  • 38
4

Use [hidden] instead of *ngif because *ngif kills your code when the condition is not satisfied.

<div [hidden]="YourVariable">
   Show Something
</div>
Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
  • This! I never use hidden because it loads it into the DOM, but in this case it is exactly what I need! – brett Jan 18 '23 at 20:58
2

My solution to this was to move the ngIf from outside of the child component to inside of the child component on a div that wrapped the whole section of html. That way it was still getting hidden when it needed to be, but was able to load the component and I could reference it in the parent.

harmonickey
  • 1,299
  • 2
  • 21
  • 35
  • But for that, how did you get to your "visible" variable that was in the parent? – Dan Chase May 11 '18 at 18:32
  • yeah this is the simplest solution for me. So you add [visible]="yourVisibleVar" to your component tag and bind that as an @Input visible:boolean; in your component... then in the template of that component have a *ngIf="visible" in the outermost tag, possible wrap in a parent div. For me was a tab set so just added the *ngIf to that – Gurnard Nov 20 '20 at 09:34
2

This works for me, see the example below.

import {Component, ViewChild, ElementRef} from 'angular2/core';

@Component({
    selector: 'app',
    template:  `
        <a (click)="toggle($event)">Toggle</a>
        <div *ngIf="visible">
          <input #control name="value" [(ngModel)]="value" type="text" />
        </div>
    `,
})

export class AppComponent {

    private elementRef: ElementRef;
    @ViewChild('control') set controlElRef(elementRef: ElementRef) {
      this.elementRef = elementRef;
    }

    visible:boolean;

    toggle($event: Event) {
      this.visible = !this.visible;
      if(this.visible) {
        setTimeout(() => { this.elementRef.nativeElement.focus(); });
      }
    }

}
NiZa
  • 3,806
  • 1
  • 21
  • 29
2

I had a similar issue, where the ViewChild was inside of a switch clause that wasn't loading the viewChild element before it was being referenced. I solved it in a semi-hacky way but wrapping the ViewChild reference in a setTimeout that executed immediately (i.e. 0ms)

Nikola Jankovic
  • 947
  • 1
  • 13
  • 23
2

A kind of generic approach:

You can create a method that will wait until ViewChild will be ready

function waitWhileViewChildIsReady(parent: any, viewChildName: string, refreshRateSec: number = 50, maxWaitTime: number = 3000): Observable<any> {
  return interval(refreshRateSec)
    .pipe(
      takeWhile(() => !isDefined(parent[viewChildName])),
      filter(x => x === undefined),
      takeUntil(timer(maxWaitTime)),
      endWith(parent[viewChildName]),
      flatMap(v => {
        if (!parent[viewChildName]) throw new Error(`ViewChild "${viewChildName}" is never ready`);
        return of(!parent[viewChildName]);
      })
    );
}


function isDefined<T>(value: T | undefined | null): value is T {
  return <T>value !== undefined && <T>value !== null;
}

Usage:

  // Now you can do it in any place of your code
  waitWhileViewChildIsReady(this, 'yourViewChildName').subscribe(() =>{
      // your logic here
  })
S Panfilov
  • 16,641
  • 17
  • 74
  • 96
2

If an *ngIf="show" prevents a ViewChild from being rendered and you need the ViewChild right after your show turns true, it helped me to fire ChangeDetectorRef.detectChanges() immediately after I set show true.

After that the *ngIf creates the component and renders the ViewChild, s.t. you can use it afterwards. Just typed a quick sample code.

@ViewChild(MatSort) sort: MatSort;    

constructor(private cdRef: ChangeDetectorRef) {}

ngOnInit() {
  this.show = false;
  this.someObservable()
    .pipe(
      tap(() => {
        this.show = true;
        this.cdRef.detectChanges();
      })
    )
    .subscribe({
      next: (data) => {
        console.log(sort)
        this.useResult(data);
      }
    });
}

Is this bad, or why has no one proposed it?

dfinki
  • 324
  • 4
  • 13
2

just adding {static: true} to @View Solves my problem.

@ViewChild(FilterTiles, { static : true }) ft: FilterTiles;
1

I fix it just adding SetTimeout after set visible the component

My HTML:

<input #txtBus *ngIf[show]>

My Component JS

@Component({
  selector: "app-topbar",
  templateUrl: "./topbar.component.html",
  styleUrls: ["./topbar.component.scss"]
})
export class TopbarComponent implements OnInit {

  public show:boolean=false;

  @ViewChild("txtBus") private inputBusRef: ElementRef;

  constructor() {

  }

  ngOnInit() {}

  ngOnDestroy(): void {

  }


  showInput() {
    this.show = true;
    setTimeout(()=>{
      this.inputBusRef.nativeElement.focus();
    },500);
  }
}
1

In my case, I knew the child component would always be present, but wanted to alter the state prior to the child initializing to save work.

I choose to test for the child until it appeared and make changes immediately, which saved me a change cycle on the child component.

export class GroupResultsReportComponent implements OnInit {

    @ViewChild(ChildComponent) childComp: ChildComponent;

    ngOnInit(): void {
        this.WhenReady(() => this.childComp, () => { this.childComp.showBar = true; });
    }

    /**
     * Executes the work, once the test returns truthy
     * @param test a function that will return truthy once the work function is able to execute 
     * @param work a function that will execute after the test function returns truthy
     */
    private WhenReady(test: Function, work: Function) {
        if (test()) work();
        else setTimeout(this.WhenReady.bind(window, test, work));
    }
}

Alertnatively, you could add a max number of attempts or add a few ms delay to the setTimeout. setTimeout effectively throws the function to the bottom of the list of pending operations.

N-ate
  • 6,051
  • 2
  • 40
  • 48
  • Using setTimeout triggers a global change detection cycle in angular, which is horrible for performance in larger apps. Probably you don't want to do this. – JohannesB Dec 11 '20 at 10:42
  • SetTimeout does not trigger global change detection. The work it eventually executes does because the child is changed which is exactly what the OP is trying to accomplish. Rather than waiting for the entire rendering to complete and then making changes, this does it immediately. The slave-master relationship is unavoidable if the child shouldn't know about the parent. This saves Dom rendering though. – N-ate Dec 18 '20 at 14:57
  • It does, if you don't know this I propose you read up on [macrotasks & zone.js](https://github.com/angular/angular/blob/master/packages/zone.js/STANDARD-APIS.md). Or if you would rather like a blogpost instead of official documentation: [read this](https://www.mokkapps.de/blog/the-last-guide-for-angular-change-detection-you-will-ever-need/) instead. – JohannesB Dec 18 '20 at 19:49
  • Go to any angular application on the web, open your developer console, run setTimeout(function(){}), now observe there is no effect on the angular engine. SetTimeout is native javascript. – N-ate Dec 18 '20 at 23:41
  • `setTimeout` is not the same in the global context as when run in the zone, see [this image](https://i.imgur.com/Zgr9Hfn.png). Stackblitz: [link](https://stackblitz.com/edit/angular-ivy-83ri4v?file=src%2Fapp%2Fapp.component.ts). In fact zone.js overrides the `setTimeout` function within its scope. So, since all code in angular itself is run from within the zone and random code from your console is run outside, you do not see a log in your case. Sorry to disappoint, but alas `setTimeout` does trigger global change detection every time when used from within an angular component. – JohannesB Dec 19 '20 at 00:14
  • 1
    FYI, this is done somewhere around here: https://github.com/angular/zone.js/blob/master/dist/zone-mix.js#L3118 – JohannesB Dec 19 '20 at 00:24
  • 1
    I see your confusion. You are running the setTimeout via angular. In my code, the angular component only runs at 2 points: 1. When angular first initializes the component. This is where "WhenReady" is started. 2. When the "test" function resolves to truth and the component is updated. – N-ate Dec 19 '20 at 00:25
  • Look at the function they patch it with. They aren't tying setTimeout to component refresh. They're only extending it with additional functionality. see line 2168. – N-ate Dec 19 '20 at 00:27
  • So that still results in: whenever your component is loaded, a full check of the change detection tree is done by angular, *up until root node*. If you have hundreds of components and they all start doing this funky behaviour on init, your performance of your app will degenerate very quickly. That's why I said initially `you probably don't want to do this`. – JohannesB Dec 19 '20 at 00:29
  • See e.g. https://oricalvo.wordpress.com/2016/07/11/performance-killer-settimeout/ – JohannesB Dec 19 '20 at 00:30
  • Clearly, this isn't a pattern that angular is not optimized for. There is no concept of parent components having knowledge of child components while the children are ignorant of the parent. This however is what the OP needed to solve. Others handle it by making the child aware of the parent which can violate good separation of concerns while providing better performance. I've seen others use a shared class to communicate this global state as an intermediary. Each has its drawbacks. If you want to suggest that the OP take a completely different approach then put together an answer. – N-ate Dec 19 '20 at 01:11
1

Here's something that worked for me.

@ViewChild('mapSearch', { read: ElementRef }) mapInput: ElementRef;

ngAfterViewInit() {
  interval(1000).pipe(
        switchMap(() => of(this.mapInput)),
        filter(response => response instanceof ElementRef),
        take(1))
        .subscribe((input: ElementRef) => {
          //do stuff
        });
}

So I basically set a check every second until the *ngIf becomes true and then I do my stuff related to the ElementRef.

Anjil Dhamala
  • 1,544
  • 3
  • 18
  • 37
1

For me the problem was I was referencing the ID on the element.

@ViewChild('survey-form') slides:IonSlides;

<div id="survey-form"></div>

Instead of like this:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>
Jeremy
  • 3,620
  • 9
  • 43
  • 75
1

If you're using Ionic you'll need to use the ionViewDidEnter() lifecycle hook. Ionic runs some additional stuff (mainly animation-related) which typically causes unexpected errors like this, hence the need for something that runs after ngOnInit, ngAfterContentInit, and so on.

Jai
  • 2,768
  • 24
  • 20
1

For Angular: Change *ngIf with display style 'block' or 'none' in HTML.

selector: 'app',
template:  `
    <controls [style.display]="controlsOn ? 'block' : 'none'"></controls>
    <slideshow (mousemove)="onMouseMove()"></slideshow>
`,
directives: [SlideshowComponent, ControlsComponent]
Rajan Kashiyani
  • 891
  • 7
  • 11
1

I had a similar issue in which a ViewChild was inside a conditionally (*ngIf) rendered component. Which would become rendered on the response of an api call. The response came later than when the @ViewChild decorator was executed and so the desired component reference stayed undefined (null). After using {static: false} the @ViewChild decorator wasn't fired again even when the desired component was visible after some (small) amount of time. This was against the 'promise' of Angular (as stated in other answers in this thread)

The reason for this was ChangeDetectionStrategy was set to OnPush . When changing this to ChangeDetectionStrategy.Default all worked as expected.

Conclusion:

  1. ✅ Use { static: false } &
  2. ChangeDetectionStrategy.Default

for @ViewChild components that are conditionally (*ngIf) rendered to get their reference "later on" (when they become rendered)

Youp Bernoulli
  • 5,303
  • 5
  • 39
  • 59
  • I would not recommend this approach. `ChangeDetectionStrategy.OnPush` is very performant. If that was the default strategy used, it must have been thoroughly thought by the authors that wrote code before you. And also `{static: false}` is the default option available. If that was set to `true`, the logic must have been executing within `oninit` and hence that was necessary. This problem could have probably been solved by triggering change detection manually. Helpful article on change detection: https://www.mokkapps.de/blog/the-last-guide-for-angular-change-detection-you-will-ever-need – Aakash Goplani Nov 05 '21 at 12:05
  • Good point @AakashGoplani to manually trigger change detection. Although it can be bloating your component's implementation if there is a lot of async traffic going on which all must trigger change detection whenever some data arrives or state changes. – Youp Bernoulli Nov 05 '21 at 13:26
0

I resolved this issue with the help of change detection along with delayed initialization of view container reference.

HTML setup:

<ng-container *ngIf="renderMode === 'modal'" [ngTemplateOutlet]="renderModal">
</ng-container>
<ng-container *ngIf="renderMode === 'alert'" [ngTemplateOutlet]="renderAlert">
</ng-container>

<ng-template #renderModal>
  <div class="modal">
    <ng-container appSelector></ng-container>
  </div>
</ng-template>

<ng-template #renderAlert>
  <div class="alert">
    <ng-container appSelector></ng-container>
  </div>
</ng-template>

Component:

@ViewChild(SelectorDirective, { static: true }) containerSelector!: SelectorDirective;

constructor(private cdr: ChangeDetectorRef) { }

ngOnInit(): void {
  // step: 1
  this.renderMode = someService.someMethod();
  // step: 2
  this.cdr.markForCheck();
  // step: 3
  const viewContainerRef = this.containerSelector?.viewContainerRef;
  if (viewContainerRef) {
    // logic...
  }
}
  1. Modified the code such that condition on which HTML is dependent on (*ngIf), should update first
  2. Once the condition is updated, manually trigger ChangeDetection
  3. Get the reference from ViewChild after manual cdr trigger and proceed ahead with logic.
Aakash Goplani
  • 1,150
  • 1
  • 19
  • 36
0

Apart from the other answers you can also use the last life-cycle hook:

ngAfterViewChecked() {}

ngAfterViewChecked is called even after ngAfterViewInit

Life-cycle hooks: https://angular.io/guide/lifecycle-hooks#lifecycle-event-sequence

eko
  • 39,722
  • 10
  • 72
  • 98
0

In my case i used @ViewChildren instead of @viewChild;

@ViewChildren(RandomOneComponent) randomChild: RandomOneComponent;

Umer Baba
  • 285
  • 3
  • 4
-2

The solution which worked for me was to add the directive in declarations in app.module.ts

noro5
  • 11
  • 1