L'annotation Angular 2 @ViewChild renvoie undefined

242

J'essaie d'apprendre Angular 2.

Je souhaite 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: string) {
        console.log(this.ft);
        var startingFilter = {
            title: 'cognomi',
            values: [
                'griffin'
                , 'simpson'
            ]}
        this.ft.tiles.push(startingFilter);
    } 
}

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

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

   } 
}

Le problème est que lorsque j'essaie d'écrire ftdans le journal de la console, j'obtiens undefined, et bien sûr, j'obtiens une exception lorsque j'essaie de pousser quelque chose à l'intérieur du tableau «tuiles»: «pas de tuiles de propriété pour« non défini »» .

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

Toute suggestion? Merci

Andrea Ialenti
la source
Ça a l'air correct. Peut-être quelque chose avec le modèle, mais il n'est pas inclus dans votre question.
Günter Zöchbauer
1
D'accord avec Günter. J'ai créé un plunkr avec votre code et de simples modèles associés et cela fonctionne. Voir ce lien: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Nous avons besoin des modèles ;-)
Thierry Templier
1
ftne serait pas défini dans le constructeur, mais dans un gestionnaire d'événements click, il serait déjà défini.
Günter Zöchbauer
5
Vous utilisez loadAsRoot, qui a un problème connu avec la détection des modifications. Assurez-vous simplement d'utiliser loadNextToLocationou loadIntoLocation.
Eric Martinez
1
Le problème était loadAsRoot. Une fois que j'ai remplacé loadIntoLocationle problème a été résolu. Si vous faites votre commentaire comme réponse, je peux le marquer comme accepté
Andrea Ialenti

Réponses:

374

J'ai eu un problème similaire et j'ai pensé publier au cas où quelqu'un d'autre aurait fait la même erreur. Tout d'abord, une chose à considérer est AfterViewInit; vous devez attendre que la vue soit initialisée avant de pouvoir accéder à votre @ViewChild. Cependant, mon @ViewChildretourne toujours nul. Le problème était mon *ngIf. La *ngIfdirective tuait mon composant de contrôles, donc je ne pouvais pas y faire référence.

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.

EDIT
Comme mentionné par @Ashg ci - dessous , une solution consiste à utiliser à la @ViewChildrenplace de @ViewChild.

kenecaswell
la source
9
@kenecaswell Vous avez donc trouvé la meilleure façon de résoudre le problème. Je suis également confronté au même problème. J'ai beaucoup * ngIf donc cet élément sera dans le seul après tout vrai, mais j'ai besoin de la référence de l'élément. Toute façon de résoudre cela>
monica
4
J'ai trouvé que le composant enfant n'est pas défini dans ngAfterViewInit () si vous utilisez ngIf. J'ai essayé de mettre de longs délais d'attente mais toujours aucun effet. Cependant, le composant enfant est disponible ultérieurement (c'est-à-dire en réponse aux événements de clic, etc.). Si je n'utilise pas ngIf et qu'il est défini comme prévu dans ngAfterViewInit (). Il y a plus d'informations sur la communication parents / enfants ici angular.io/docs/ts/latest/cookbook/…
Matthew Hegarty
3
J'ai utilisé bootstrap ngClass+ hiddenclass au lieu de ngIf. Ça a marché. Merci!
Rahmathullah M
9
Cela ne résout pas le problème, utilisez la solution ci-dessous en utilisant @ViewChildren allez obtenir une référence au contrôle enfant une fois qu'il sera disponible
Ashg
20
Cela prouve juste le "problème", non? Il n'affiche pas de solution.
Miguel Ribeiro
146

Le problème, comme mentionné précédemment, est celui ngIfqui rend la vue non définie. La réponse est d'utiliser ViewChildrenau lieu de ViewChild. J'ai eu un problème similaire où je ne voulais pas qu'une grille soit affichée jusqu'à ce 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 de 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;
        });


    }
}

Ici, nous utilisons ViewChildrensur 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.

Ashg
la source
4
Je voudrais ajouter que dans certains cas, lorsque vous essayez de changer par exemple. this.SearchGridpropriétés que vous devez utiliser comme syntaxe setTimeout(()=>{ ///your code here }, 1); pour éviter l'exception: l'expression a changé après avoir été vérifiée
rafalkasa
3
Comment faites-vous cela si vous souhaitez placer votre balise #searchGrid sur un élément HTML normal au lieu d'un élément Angular2? (Par exemple, <div #searchGrid> </div> et ceci est à l'intérieur d'un bloc * ngIf?
Vern Jensen
1
c'est la bonne réponse pour mon cas d'utilisation! Merci, j'ai besoin d'accéder à un composant car il est disponible via ngIf =
Frozen_byte
1
cela fonctionne parfaitement sur les réponses ajax, maintenant les *ngIftravaux, et après le rendu, nous pouvons enregistrer un ElementRef des composants dinamic.
elporfirio
4
N'oubliez pas non plus de l'affecter à un abonnement, puis de vous désabonner de celui
tam.teixeira
64

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 permettra de s'afficher.

Mais mon problème était autre chose. 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 était toujours indéfinie.

parlement
la source
3
Cela ne fonctionne pas pour moi - je reçois le premier non défini, mais je ne reçois pas le deuxième appel avec la référence. L'application est un ng2 ... est-ce une fonctionnalité ng4 +?
Jay Cummins
@Jay Je pense que c'est parce que vous n'avez pas enregistré le composant avec Angular, dans ce cas FilterTiles. J'ai déjà rencontré ce problème pour cette raison auparavant.
parlement
1
Fonctionne pour Angular 8 en utilisant #paginator sur l'élément html et des annotations comme@ViewChild('paginator', {static: false})
Qiteq
1
S'agit-il d'un rappel pour les modifications de ViewChild?
Yasser Nascimento
24

Cela a fonctionné pour moi.

Mon composant nommé 'mon-composant', par exemple, était affiché en utilisant * ngIf = "showMe" comme ceci:

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

Ainsi, lorsque le composant est initialisé, le composant n'est pas encore affiché tant que "showMe" n'est pas vrai. Ainsi, mes références @ViewChild n'étaient pas définies.

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

Vous pouvez utiliser la QueryList retournée par @ViewChildren et vous abonner à toutes les modifications apportées aux éléments référencés à l'aide de rxjs, comme indiqué ci-dessous. @ViewChild 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 aidera quelqu'un à gagner du temps et à éviter la frustration.

Joshua Dyck
la source
3
En effet, votre solution semble être la meilleure approche répertoriée jusqu'à présent. REMARQUE Nous devons garder à l'esprit que la solution du top 73 est désormais obsolète ... puisque la directive: [...] la déclaration n'est plus prise en charge dans Angular 4. IOW ça ne fonctionnera PAS dans le scénario Angular 4
PeteZaria
5
N'oubliez pas de vous désinscrire ou d'utiliser .take(1).subscribe(), mais excellente réponse, merci beaucoup!
Blair Connolly
2
Excellente solution. J'ai souscrit aux changements de référence dans ngAfterViewInit () plutôt que dans ngOnChanges (). Mais j'ai dû ajouter un setTimeout pour se débarrasser de l'erreur ExpressionChangedAfterChecked
Josf
Cela devrait être marqué comme la solution réelle. Merci beaucoup!
platzhersh
10

Ma solution de contournement était d'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";
    }
  }
....
Calderas
la source
Avoir une page où une liste d'éléments est affichée dans un tableau, ou l'élément d'édition est affiché, en fonction de la valeur de la variable showList. Débarrassé de l'erreur gênante de la console en utilisant le [style.display] = "! ShowList", combiné avec * ngIf = "! ShowList".
razvanone
9

Ce qui a résolu mon problème était de m'assurer qu'il staticétait réglé sur false.

@ViewChild(ClrForm, {static: false}) clrForm;

Lorsque cette staticoption est désactivée, la @ViewChildréférence est mise à jour par Angular lorsque la *ngIfdirective change.

Paul LeBeau
la source
1
Ceci est presque une réponse parfaite, juste pour pointer est une bonne pratique pour vérifier également les valeurs nullables, donc nous nous retrouvons avec quelque chose comme ceci: @ViewChild (ClrForm, {static: false}) set clrForm (clrForm: ClrForm) {if (clrForm) {this.clrForm = clrForm; }};
Klaus Klein
J'ai essayé tellement de choses et j'ai finalement trouvé que c'était le coupable.
Manish Sharma
8

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 a fonctionné pour mes besoins.

suzanne suzanne
la source
5

Dans mon cas, j'avais un setter de variable d'entrée utilisant le ViewChild, et le ViewChildétait à l'intérieur d'une *ngIfdirective, donc le setter essayait d'y accéder avant le *ngIfrendu (cela fonctionnerait bien sans le *ngIf, mais ne fonctionnerait pas s'il était toujours défini sur vrai avec *ngIf="true").

Pour résoudre, j'ai utilisé Rxjs pour m'assurer que toute référence à l' ViewChildattendu jusqu'à ce que la vue soit lancée. Tout d'abord, créez un objet qui se termine après le début de la vue.

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

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

Ensuite, créez une fonction qui prend et 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;
nikojpapa
la source
1
C'est une bien meilleure solution que tous les non-sens de décompte
Liam
4

Ça doit marcher.

Mais comme l'a dit Günter Zöchbauer , il doit y avoir un autre problème dans le modèle. J'ai créé un peu Relevant-Plunkr-Answer . Veuillez 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 étiquettes et références.

Merci...

micronyks
la source
1
Si le problème est le même que le mien, pour le dupliquer, vous devez mettre un * ngIf dans le modèle autour de <filter> </filter> .. Apparemment, si le ngIf renvoie false, ViewChild ne se câble pas et renvoie null
Dan Chase
2

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

}

NiZa
la source
2

J'ai eu un problème similaire, où ViewChildétait à l'intérieur d'une switchclause qui ne chargeait pas l'élément viewChild avant qu'il soit référencé. Je l'ai résolu de manière semi-hacky mais en encapsulant la ViewChildréférence dans un setTimeoutqui s'est exécuté immédiatement (ie 0 ms)

Nikola Jankovic
la source
1

Ma solution à cela était de déplacer le ngIf de l'extérieur du composant enfant vers l'intérieur du composant enfant sur un div qui enveloppait la section entière de html. De cette façon, il était toujours caché quand il le fallait, mais j'ai pu charger le composant et je pouvais le référencer dans le parent.

harmonickey
la source
Mais pour cela, comment êtes-vous arrivé à votre variable "visible" qui était dans le parent?
Dan Chase
1

Je le corrige en ajoutant simplement 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);
  }
}
Osvaldo Leiva
la source
1

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

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

N-mangé
la source
0

Une sorte d'approche générique:

Vous pouvez créer une méthode qui attendra d' ViewChildêtre prête

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

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

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

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

Au lieu de comme ça:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>
Jeremy Quick
la source
0

Si vous utilisez Ionic, vous devrez utiliser le ionViewDidEnter()crochet du cycle de vie. Ionic exécute des éléments supplémentaires (principalement liés à l'animation) qui provoquent généralement des erreurs inattendues comme celle-ci, d'où la nécessité de quelque chose qui s'exécute après ngOnInit , ngAfterContentInitetc.

I have
la source
-1

Voici quelque chose qui a fonctionné pour moi.

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

Donc, fondamentalement, je vérifie chaque seconde jusqu'à ce que le *ngIfdevienne vrai, puis je fais mes trucs liés au ElementRef.

Anjil Dhamala
la source
-3

La solution qui a fonctionné pour moi a été d'ajouter la directive dans les déclarations dans app.module.ts

noro5
la source