Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 @ViewChild annotation returns undefined

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?

like image 943
Andrea Ialenti Avatar asked Jan 22 '16 12:01

Andrea Ialenti


People also ask

Why is ViewChild undefined?

The problem can be caused by the *ngIf or other directive. The solution is to use the @ViewChildren instead of @ViewChild and subscribe the changes subscription that is executed when the component is ready. For example, if in the parent component ParentComponent you want to access the child component MyComponent .

What does @ViewChild does in angular?

The @ViewChild decorator allows us to inject into a component class references to elements used inside its template, that's what we should use it for. Using @ViewChild we can easily inject components, directives or plain DOM elements.

What does ViewChild return?

ViewChild returns the first matching element and ViewChildren returns all the matching elements as a QueryList of items. We can use these references to manipulate element properties in the component.

How do I use ContentChild in angular 6?

To use ContentChild , we need to import it first from the @angular/core . import { Component, ContentChild, ContentChildren, ElementRef, Renderer2, ViewChild } from '@angular/core'; Then use it to query the header from the projected content.


4 Answers

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.

like image 160
kenecaswell Avatar answered Oct 23 '22 21:10

kenecaswell


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.

like image 41
Ashg Avatar answered Oct 23 '22 21:10

Ashg


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.

like image 22
parliament Avatar answered Oct 23 '22 21:10

parliament


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.

like image 26
Paul LeBeau Avatar answered Oct 23 '22 22:10

Paul LeBeau