309
votes

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 suggestion? Thanks

22
Looks correct. Maybe something with the template, but it isn't included in your question.Günter Zöchbauer
Agreed with Günter. I created a plunkr with your code and simple associated templates and it works. See this link: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview. We need the templates ;-)Thierry Templier
ft wouldn't be set in the constructor, but in a click event handler it would be set already.Günter Zöchbauer
You're using loadAsRoot, which has a known issue with change detection. Just to make sure try using loadNextToLocation or loadIntoLocation.Eric Martinez
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 acceptedAndrea Ialenti

22 Answers

464
votes

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
    }
}

Hope that helps.

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

185
votes

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. Hope this helps.

73
votes

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.

43
votes

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.

27
votes

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.

16
votes

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.

10
votes

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";
    }
  }
....
7
votes

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;
4
votes

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

2
votes

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.

2
votes

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

}
2
votes

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)

1
votes

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
votes

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.

1
votes

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
  })
1
votes

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>
1
votes

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.

1
votes

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
  }
}
1
votes

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]
0
votes

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.

0
votes

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?

-2
votes

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