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