Angular 2 L’annotation @ViewChild renvoie undefined

J’essaie d’apprendre angular 2.

Je voudrais accéder à un composant enfant à partir d’un composant parent à l’aide de l’annotation @ViewChild .

Voici quelques lignes de code:

Dans BodyContent.ts j’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: ssortingng) { console.log(this.ft); var startingFilter = { title: 'cognomi', values: [ 'griffin' , 'simpson' ]} this.ft.tiles.push(startingFilter); } } 

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

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

BodyContent.html

 

FilterTiles.html

 

Tiles loaded

... stuff ...

Le template FilterTiles.html est correctement chargé dans la balise ico-filter-tiles (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, je suis undefined , et bien sûr, j’obtiens une exception lorsque j’essaye d’insérer quelque chose à l’intérieur du tableau “tiles”:

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

Toute suggestion? Merci

J’ai eu un problème similaire et j’ai pensé que je posterais si quelqu’un d’autre faisait la même erreur. Tout d’abord, une chose à prendre en compte est AfterViewInit ; vous devez attendre que la vue soit initialisée avant de pouvoir accéder à votre @ViewChild . Cependant, mon @ViewChild toujours null. Le problème était mon *ngIf . La directive *ngIf était en train de tuer mon composant de contrôle, donc je ne pouvais 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: `   `, 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 .

Le problème mentionné précédemment est le ngIf qui rend la vue indéfinie. La réponse est d’utiliser ViewChildren au lieu de ViewChild. J’ai eu un problème similaire où je ne voulais pas qu’une grid soit affichée jusqu’à ce que toutes les données de référence aient été chargées.

html:

  

Results

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 private SearchGrid: GridComponent public ngAfterViewInit(): void { this.Grids.changes.subscribe((comps: QueryList ) => { this.SearchGrid = comps.first; }); } } 

Ici, nous utilisons ViewChildren sur lequel vous pouvez écouter les changements. Dans ce cas, tous les enfants avec la référence #searchGrid. J’espère que cela t’aides.

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 à nouveau avec une référence une fois que ngIf lui aura permis le rendu.

Mon problème était autre chose. Je n’avais pas inclus le module contenant mes “FilterTiles” dans mes applications. Le modèle n’a pas généré d’erreur mais la référence était toujours indéfinie.

Cela a fonctionné pour moi.

Mon composant nommé “my-component”, par exemple, était affiché avec * ngIf = “showMe” comme ceci:

  

Ainsi, lorsque le composant est initialisé, le composant n’est pas encore affiché jusqu’à ce que “showMe” soit vrai. Ainsi, mes références @ViewChild étaient toutes indéfinies.

C’est là que j’ai utilisé @ViewChildren et la liste QueryList qu’il renvoie. Voir l’article angular sur QueryList et une démonstration d’utilisation de @ViewChildren .

Vous pouvez utiliser la QueryList que @ViewChildren renvoie et vous abonner aux modifications apscopes 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; // this reference is just pointing to a template reference variable in the component html file (ie 
) @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 sortingggers *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 aidera quelqu’un à gagner du temps et de la frustration.

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

 @Component({ selector: 'app', template: `  ... export class AppComponent { @ViewChild(ControlsComponent) controls:ControlsComponent; controlsOn:boolean = false; getControlsOnStyleDisplay() { if(this.controlsOn) { return "block"; } else { return "none"; } } .... 

Ça doit marcher.

Mais comme Günter Zöchbauer l’a dit, il doit y avoir un autre problème dans le modèle. J’ai créé un peu Relevant-Plunkr-Answer . Il est conseillé de vérifier la console du navigateur.

boot.ts

 @Component({ selector: 'my-app' , template: `

BodyContent

` , 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: '

Filter tiles

' }) export class FilterTiles { public tiles = []; public constructor(){}; }

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

Merci…

Ma solution à cela consistait à déplacer le ngIf de l’extérieur du composant enfant vers l’intérieur du composant enfant sur une div qui englobait la totalité de la section 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.

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

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

Ma solution était de remplacer * ngIf par [hidden]. L’inconvénient était que tous les composants enfants étaient présents dans le code DOM. Mais travaillé pour mes besoins.

J’ai eu un problème similaire, où le ViewChild était à l’intérieur d’une clause switch qui ne chargeait pas l’élément viewChild avant d’être référencé. Je l’ai résolu d’une manière semi-hacky mais en enveloppant la référence ViewChild dans un setTimeout qui s’exécutait immédiatement (c.-à-d. 0ms)

Dans mon cas, je savais que le composant enfant serait toujours présent, mais je voulais modifier l’état avant l’initialisation de l’enfant pour économiser du travail.

J’ai choisi de tester l’enfant jusqu’à ce qu’il apparaisse et d’apporter des modifications immédiatement, ce qui m’a sauvé un cycle de changement sur 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)); } } 

Alertnativement, vous pouvez append un nombre maximum de tentatives ou append un délai de quelques ms au setTimeout . setTimeout lance efficacement la fonction au bas de la liste des opérations en attente.

Dans mon cas, j’avais une variable de saisie utilisant le ViewChild, et le ViewChild était à l’intérieur d’une directive * ngIf, donc le setter essayait d’y accéder avant que le * ngIf ne soit rendu (cela fonctionnerait bien sans le * ngIf, mais ne fonctionne pas s’il a toujours été défini sur true avec * ngIf = “true”).

Pour résoudre, j’ai utilisé Rxjs pour m’assurer que toute référence à ViewChild attendait que la vue soit lancée. Tout d’abord, créez un object qui se termine après la vue init.

 export class MyComponent implements AfterViewInit { private _viewInitWaiter$ = new Subject(); ngAfterViewInit(): void { this._viewInitWaiter$.complete(); } } 

Ensuite, créez une fonction qui exécute un lambda une fois le sujet terminé.

 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; 

Je le répare en ajoutant simplement SetTimeout après avoir défini le composant visible

Mon HTML:

  

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