ChatGPT解决这个技术问题 Extra ChatGPT

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?

Looks correct. Maybe something with the template, but it isn't included in your question.
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 ;-)
ft wouldn't be set in the constructor, but in a click event handler it would be set already.
You're using loadAsRoot, which has a known issue with change detection. Just to make sure try using loadNextToLocation or loadIntoLocation.
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

D
Dominik

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.


@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 >
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 angular.io/docs/ts/latest/cookbook/…
I used bootstrap ngClass+hidden class instead of ngIf. That worked. Thanks!
This doesn't solve the problem, use the solution below using @ViewChildren go get a reference to the child control once it becomes available
This just proves the "issue", right? It does not post a solution.
L
Liam

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.


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
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?
this is the correct answer for my use case! Thanks I need to access an component as it comes available through ngIf=
this works perfect on ajax responses, now the *ngIfworks, and after render we can save a ElementRef from the dinamic components.
Also don't forget to assign it to a subscription and then unSubscribe from it
e
e_i_pi

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.


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 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.
Works for Angular 8 using #paginator on html element and annotation like @ViewChild('paginator', {static: false})
Is this a callback for ViewChild changes?
can you please provider the code for the getter also?
P
Paul LeBeau

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.


This is almost a perfect answser, just to point is a good pratice to also check for nullable values, so we end up with something like this: @ViewChild(ClrForm, { static: false }) set clrForm(clrForm: ClrForm) { if(clrForm) { this.clrForm = clrForm; } };
I tried so many things and finally found this thing to be the culprit.
This is the easiest and quickest solution for this problem IMHO.
{static:false} is default (as of Angular9 iirc)
This should be the accepted answer
J
Joshua Dyck

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.


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
Don't forget to unsubscribe, or use .take(1).subscribe(), but excellent answer, thank you so much!
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
This should be marked as the actual solution. Thanks a lot!
L
Liam

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.


L
Liam

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

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

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;

This is a much better solution than all the settimeout nonesense
Nice solution, I think this is the most 'Angular' way to solve this problem.
m
micronyks

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


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
This doesn't address the OP's problem with a late(r) initialized/available component/element where the viewChild reference is desired for.
y
yaya

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

h
harmonickey

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.


But for that, how did you get to your "visible" variable that was in the parent?
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
N
NiZa

This works for me, see the example below.

import {Component, ViewChild, ElementRef} from 'angular2/core'; @Component({ selector: 'app', template: ` Toggle

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


N
Nikola Jankovic

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)


S
Sergei Panfilov

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

d
dfinki

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?


M
Mario Petrovic

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

<div [hidden]="YourVariable">
   Show Something
</div>

C
Chapdast Dev

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

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

O
Osvaldo Leiva

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

N
N-ate

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.


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.
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.
It does, if you don't know this I propose you read up on macrotasks & zone.js. Or if you would rather like a blogpost instead of official documentation: read this instead.
FYI, this is done somewhere around here: github.com/angular/zone.js/blob/master/dist/zone-mix.js#L3118
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.
J
Jeremy

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>

J
Jai

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.


R
Rajan Kashiyani

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]

A
Anjil Dhamala

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.


B
Bernoulli IT

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:

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

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


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: mokkapps.de/blog/…
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.
A
Aakash Goplani

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

Modified the code such that condition on which HTML is dependent on (*ngIf), should update first Once the condition is updated, manually trigger ChangeDetection Get the reference from ViewChild after manual cdr trigger and proceed ahead with logic.


e
eko

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


n
noro5

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