web-dev-qa-db-fra.com

L'annotation @ViewChild angulaire 2 renvoie undefined

J'essaie d'apprendre Angular 2.

Je souhaite accéder à un composant enfant à partir d'un composant parent à l'aide de @ViewChild Annotation.

Voici quelques lignes de code:

DansBodyContent.tsj'ai:

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

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

Enfin, voici les modèles (comme suggéré dans les commentaires):

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>

Le modèle FilterTiles.html est correctement chargé dans ico-filter-tiles tag (en effet, je peux voir l’en-tête).

Remarque: la classe BodyContent est injectée dans un autre modèle (Body) à l'aide de DynamicComponetLoader: dcl.loadAsRoot (BodyContent, '# ico-bodyContent', injecteur):

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

   } 
}

Le problème est que lorsque j'essaie d'écrire ft dans le journal de la console, j'obtiens undefined et, bien sûr, une exception apparaît lorsque j'essaie d'insérer quelque chose dans le tableau "tiles": 'no property tiles for "undefined"'.

Encore une chose: le composant FilterTiles semble être correctement chargé, car je peux voir le modèle html correspondant.

Toute suggestion? Merci

138
Andrea Ialenti

J'avais un problème similaire et je pensais poster si quelqu'un d'autre faisait la même erreur. Premièrement, une chose à considérer est AfterViewInit; vous devez attendre que la vue soit initialisée avant de pouvoir accéder à votre @ViewChild. Cependant, mon @ViewChild retournait toujours null. Le problème était mon *ngIf. La directive *ngIf détruisait mon composant de contrôle, je ne pouvais donc pas le référencer.

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

J'espère que cela pourra aider.

MODIFIER
Comme mentionné par @Ashg ci-dessous, une solution consiste à utiliser @ViewChildren au lieu de @ViewChild.

228
kenecaswell

Comme mentionné précédemment, le problème est le ngIf qui rend la vue non définie. La réponse consiste à utiliser ViewChildren au lieu de ViewChild. J'avais un problème similaire: je ne voulais pas qu'une grille soit affichée avant que toutes les données de référence aient été chargées.

html:

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

Code du composant

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


    }
}

Nous utilisons ici ViewChildren sur lequel vous pouvez écouter les modifications. Dans ce cas, tous les enfants avec la référence #searchGrid. J'espère que cela t'aides.

92
Ashg

Vous pouvez utiliser un setter pour @ViewChild()

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

Si vous avez un wrapper ngIf, le setter sera appelé avec undefined, puis avec une référence une fois que ngIf lui permet de s'afficher.

Mon problème était autre chose cependant. Je n'avais pas inclus le module contenant mes "FilterTiles" dans mes app.modules. Le modèle n'a pas généré d'erreur, mais la référence a toujours été indéfinie.

47
parliament

Cela a fonctionné pour moi.

Mon composant nommé 'mon composant', par exemple, a été affiché avec * ngIf = "showMe" Comme ceci:

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

Ainsi, lorsque le composant est initialisé, le composant ne s'affiche pas jusqu'à ce que "showMe" devienne vrai. Ainsi, mes références @ViewChild étaient toutes indéfinies.

C’est là que j’ai utilisé @ViewChildren et la liste de requêtes qu’elle renvoie. Voir article angulaire sur QueryList et une démo d'utilisation de @ViewChildren

Vous pouvez utiliser la liste de requêtes que @ViewChildren renvoie et vous abonner à toute modification apportée aux éléments référencés à l'aide de rxjs, comme indiqué ci-dessous. @ViewChid n'a pas cette capacité.

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

J'espère que cela aide quelqu'un à économiser du temps et de la frustration.

16
Joshua Dyck

Ma solution consistait à utiliser [style.display] = "getControlsOnStyleDisplay ()" au lieu de * ngIf = "controlsOn". Le bloc est là mais il n'est pas affiché.

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

Ça doit marcher. 

Mais comme Günter Zöchbauer a déclaré qu'il devait y avoir un autre problème dans le modèle. J'ai créé un peu Relevant-Plunkr-Answer . Merci de vérifier la console du navigateur.

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

Il fonctionne comme un charme. Veuillez vérifier vos balises et vos références.

Merci...

3
micronyks

Cela fonctionne pour moi, voir l'exemple ci-dessous.

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

}

3
NiZa

Ma solution à cela était de remplacer * ngIf par [hidden]. Inconvénient: tous les composants enfants étaient présents dans le code DOM. Mais a travaillé pour mes besoins.

3
suzanne suzanne

Ma solution à cela était de déplacer le ngIf de l'extérieur du composant enfant à l'intérieur du composant enfant sur une div qui englobait toute la section de HTML. De cette façon, il était toujours caché quand il le fallait, mais il était capable de charger le composant et je pouvais le référencer dans le parent.

2
harmonickey

Je résous le problème en ajoutant SetTimeout après avoir visible le composant

Mon HTML:

<input #txtBus *ngIf[show]>

Mon composant 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);
  }
}
2
Osvaldo Leiva

Dans mon cas, je savais que le composant enfant serait toujours présent, mais je voulais modifier l'état avant que l'enfant ne s'initialise pour économiser du travail.

J'ai choisi de tester l'enfant jusqu'à l'apparition de celui-ci et d'apporter des modifications immédiatement, ce qui m'a épargné un cycle de modifications pour le composant enfant.

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

Alertiquement, vous pouvez ajouter un nombre maximal de tentatives ou ajouter un délai de quelques ms à la variable setTimeout. setTimeout jette effectivement la fonction au bas de la liste des opérations en attente.

1
N-ate

J'ai eu un problème similaire, où ViewChild était à l'intérieur d'une clause switch qui ne chargeait pas l'élément viewChild avant qu'il ne soit référencé. Je l'ai résolu de manière semi-simpliste mais en enveloppant la référence ViewChild dans une setTimeout exécutée immédiatement (c'est-à-dire 0 ms)

1
Nikola Jankovic

Dans mon cas, j'avais un setter de variable d'entrée utilisant ViewChild, et ViewChild se trouvant à l'intérieur d'une directive * ngIf; ne fonctionne pas si elle a toujours été définie sur true avec * ngIf = "true").

Pour résoudre ce problème, j'ai utilisé Rxjs pour vérifier que toute référence à ViewChild attendait que la vue soit lancée. Tout d'abord, créez un sujet qui se termine après l'affichage de la vue.

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

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

Ensuite, créez une fonction qui exécute un lambda à la fin du sujet.

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

Enfin, assurez-vous que les références à ViewChild utilisent cette fonction.

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

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;
1
nikojpapa

Une sorte d'approche générique:

Vous pouvez créer une méthode qui attendra que ViewChild soit prêt

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
  })
0
Sergei Panfilov

Pour moi, le problème était que je faisais référence à l'ID de l'élément.

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

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

Au lieu de cela:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>
0
jrquick