Angular 2 @ ViewChild-Annotation gibt undefiniert zurück

241

Ich versuche Angular 2 zu lernen.

Ich möchte mit der @ ViewChild- Annotation von einer übergeordneten Komponente aus auf eine untergeordnete Komponente zugreifen .

Hier einige Codezeilen:

In BodyContent.ts habe ich:

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

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

Zum Schluss hier die Vorlagen (wie in den Kommentaren vorgeschlagen):

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>

Die Vorlage FilterTiles.html wurde korrekt in das Tag ico-filter-tiles geladen (tatsächlich kann ich den Header sehen).

Hinweis: Die BodyContent-Klasse wird mit DynamicComponetLoader in eine andere Vorlage (Body) eingefügt: 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);

   } 
}

Das Problem ist, dass ich beim Versuch, ftin das Konsolenprotokoll zu schreiben undefined, eine Ausnahme erhalte , wenn ich versuche, etwas in das Array "tiles" zu verschieben: 'no property tiles for "undefined"' .

Noch etwas: Die FilterTiles-Komponente scheint korrekt geladen zu sein, da ich die HTML-Vorlage dafür sehen kann.

Irgendein Vorschlag? Vielen Dank

Andrea Ialenti
quelle
Sieht richtig aus. Vielleicht etwas mit der Vorlage, aber es ist nicht in Ihrer Frage enthalten.
Günter Zöchbauer
1
Einverstanden mit Günter. Ich habe einen Plunkr mit Ihrem Code und einfachen zugehörigen Vorlagen erstellt und es funktioniert. Siehe diesen Link: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Wir brauchen die Vorlagen ;-)
Thierry Templier
1
ftwürde nicht im Konstruktor festgelegt werden, aber in einem Click-Event-Handler wäre es bereits festgelegt.
Günter Zöchbauer
5
Sie verwenden loadAsRoot, was ein bekanntes Problem mit der Änderungserkennung hat. Nur um sicherzugehen, versuchen Sie es mit loadNextToLocationoder loadIntoLocation.
Eric Martinez
1
Das Problem war loadAsRoot. Einmal habe ich durch loadIntoLocationdas Problem ersetzt wurde gelöst. Wenn Sie Ihren Kommentar als Antwort
abgeben,

Antworten:

372

Ich hatte ein ähnliches Problem und dachte, ich würde posten, falls jemand anderes den gleichen Fehler machen sollte. Erstens ist eine Sache zu berücksichtigen AfterViewInit; Sie müssen warten, bis die Ansicht initialisiert wurde, bevor Sie auf Ihre zugreifen können @ViewChild. Meiner gab @ViewChildjedoch immer noch null zurück. Das Problem war mein *ngIf. Die *ngIfAnweisung hat meine Steuerelementkomponente beendet, sodass ich nicht darauf verweisen konnte.

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

Hoffentlich hilft das.

EDIT
Wie erwähnt @Ashg unten , ist eine Lösung zu verwenden , @ViewChildrenstatt @ViewChild.

Kenecaswell
quelle
9
@kenecaswell Also hast du den besseren Weg gefunden, um das Problem zu lösen. Ich stehe auch vor dem gleichen Problem. Ich habe viele * ngIf, so dass das Element in der einzigen wahr sein wird, aber ich brauche die Elementreferenz. Jeder Weg, um dieses
Problem
4
Ich habe festgestellt, dass die untergeordnete Komponente in ngAfterViewInit () bei Verwendung von ngIf 'undefiniert' ist. Ich habe versucht, lange Auszeiten zu setzen, aber immer noch keine Wirkung. Die untergeordnete Komponente ist jedoch später verfügbar (dh als Reaktion auf Klickereignisse usw.). Wenn ich ngIf nicht verwende und es wie erwartet in ngAfterViewInit () definiert ist. Es gibt mehr über Eltern / Kind-Kommunikation hier angular.io/docs/ts/latest/cookbook/…
Matthew Hegarty
3
Ich habe stattdessen Bootstrap ngClass+ hiddenKlasse verwendet ngIf. Das hat funktioniert. Vielen Dank!
Rahmathullah M
9
Dies löst das Problem nicht. Verwenden Sie die folgende Lösung mit @ViewChildren. Holen Sie sich einen Verweis auf das
untergeordnete
20
Dies beweist nur das "Problem", oder? Es wird keine Lösung veröffentlicht.
Miguel Ribeiro
144

Wie bereits erwähnt, ist das Problem das, ngIfwas dazu führt, dass die Ansicht undefiniert ist. Die Antwort ist, ViewChildrenstatt zu verwenden ViewChild. Ich hatte ein ähnliches Problem, bei dem ich nicht wollte, dass ein Raster angezeigt wird, bis alle Referenzdaten geladen wurden.

html:

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

Komponentencode

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


    }
}

Hier verwenden wir, ViewChildrenauf die Sie auf Änderungen warten können. In diesem Fall alle Kinder mit der Referenz #searchGrid. Hoffe das hilft.

Ashg
quelle
4
Ich möchte das in einigen Fällen hinzufügen, wenn Sie versuchen, zu ändern, z. this.SearchGridEigenschaften, die Sie Syntax verwenden sollten, um setTimeout(()=>{ ///your code here }, 1); zu vermeiden Ausnahme: Ausdruck hat sich geändert, nachdem es überprüft wurde
rafalkasa
3
Wie gehen Sie vor, wenn Sie Ihr # searchGrid-Tag auf einem normalen HTML-Element anstelle eines Angular2-Elements platzieren möchten? (Zum Beispiel <div #searchGrid> </ div> und dies ist innerhalb eines * ngIf-Blocks?
Vern Jensen
1
Dies ist die richtige Antwort für meinen Anwendungsfall! Vielen Dank, ich muss auf eine Komponente zugreifen, die über ngIf =
Frozen_byte
1
Dies funktioniert perfekt bei Ajax-Antworten, jetzt *ngIffunktioniert es, und nach dem Rendern können wir ein ElementRef aus den Dinamic-Komponenten speichern.
Elporfirio
4
Vergessen Sie auch nicht, es einem Abonnement zuzuweisen und es dann abzubestellen
tam.teixeira
63

Sie könnten einen Setter für verwenden @ViewChild()

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

Wenn Sie einen ngIf-Wrapper haben, wird der Setter mit undefined und dann erneut mit einer Referenz aufgerufen, sobald ngIf das Rendern zulässt.

Mein Problem war jedoch etwas anderes. Ich hatte das Modul mit meinen "FilterTiles" nicht in meine app.modules aufgenommen. Die Vorlage hat keinen Fehler ausgegeben, aber die Referenz war immer undefiniert.

Parlament
quelle
3
Das funktioniert bei mir nicht - ich bekomme den ersten undefinierten, aber ich bekomme nicht den zweiten Anruf mit der Referenz. App ist ein ng2 ... ist das ng4 + Feature?
Jay Cummins
@ Jay Ich glaube, das liegt daran, dass Sie die Komponente in diesem Fall nicht bei Angular registriert haben FilterTiles. Ich bin aus diesem Grund schon einmal auf dieses Problem gestoßen.
Parlament
1
Funktioniert für Angular 8 mit #paginator für HTML-Elemente und Anmerkungen wie@ViewChild('paginator', {static: false})
Qiteq
1
Ist dies ein Rückruf für ViewChild-Änderungen?
Yasser Nascimento
24

Das hat bei mir funktioniert.

Meine Komponente mit dem Namen 'my-component' wurde beispielsweise mit * ngIf = "showMe" wie folgt angezeigt:

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

Wenn die Komponente initialisiert wird, wird sie erst angezeigt, wenn "showMe" wahr ist. Daher waren meine @ ViewChild-Referenzen alle undefiniert.

Hier habe ich @ViewChildren und die zurückgegebene QueryList verwendet. Siehe eckigen Artikel über QueryList und eine @ ViewChildren-Verwendungsdemo .

Sie können die von @ViewChildren zurückgegebene QueryList verwenden und alle Änderungen an den referenzierten Elementen mithilfe von rxjs abonnieren (siehe unten). @ViewChild hat diese Fähigkeit nicht.

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

Hoffe, das hilft jemandem, Zeit und Frust zu sparen.

Joshua Dyck
quelle
3
In der Tat scheint Ihre Lösung der beste Ansatz zu sein, der bisher aufgeführt wurde. HINWEIS Wir müssen bedenken, dass die Top-73-Lösung jetzt VERRINGERT ist ... da die Anweisung: [...] in Angular 4 nicht mehr unterstützt wird. IOW funktioniert in Angular 4-Szenario nicht
PeteZaria
5
Vergessen Sie nicht, sich abzumelden oder zu verwenden .take(1).subscribe(), aber eine ausgezeichnete Antwort, vielen Dank!
Blair Connolly
2
Hervorragende Lösung. Ich habe die Ref-Änderungen in ngAfterViewInit () und nicht in ngOnChanges () abonniert. Aber ich musste ein setTimeout hinzufügen, um den ExpressionChangedAfterChecked-Fehler zu
beseitigen
Dies sollte als die tatsächliche Lösung markiert werden. Vielen Dank!
Platzhersh
10

Meine Problemumgehung bestand darin, [style.display]="getControlsOnStyleDisplay()"statt zu verwenden *ngIf="controlsOn". Der Block ist vorhanden, wird jedoch nicht angezeigt.

@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
quelle
Haben Sie eine Seite, auf der eine Liste von Elementen in einer Tabelle angezeigt wird, oder das Bearbeitungselement wird angezeigt, basierend auf dem Wert der Variablen showList. Behebung des lästigen Konsolenfehlers mithilfe von [style.display] = "! ShowList" in Kombination mit * ngIf = "! ShowList".
Rasvanone
8

Meine Lösung war zu ersetzen *ngIf mit [hidden]. Nachteil war, dass alle untergeordneten Komponenten im Code-DOM vorhanden waren. Aber für meine Anforderungen gearbeitet.

suzanne suzanne
quelle
8

Was mein Problem löste, war sicherzustellen, dass staticeingestellt war false.

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

Mit staticausgeschaltet wird, das @ViewChildwird Bezug von Angular aktualisiert , wenn die *ngIfRichtlinie ändert.

Paul LeBeau
quelle
Dies ist fast eine perfekte Antwort. Nur zu zeigen, ist eine gute Übung, um auch nach nullbaren Werten zu suchen. Am Ende haben wir also Folgendes: @ViewChild (ClrForm, {static: false}) set clrForm (clrForm: ClrForm) {if (clrForm) {this.clrForm = clrForm; }};
Klaus Klein
5

In meinem Fall hatte ich einen Eingabevariablen-Setter, der das verwendete ViewChild, und das ViewChildbefand sich innerhalb einer *ngIfDirektive, sodass der Setter versuchte, vor dem *ngIfRendern darauf zuzugreifen (es würde ohne das gut funktionieren *ngIf, würde aber nicht funktionieren, wenn es immer auf gesetzt wäre wahr mit *ngIf="true").

Zum Lösen habe ich Rxjs verwendet, um sicherzustellen, dass auf die Wartezeit verwiesen wird, ViewChildbis die Ansicht initiiert wurde. Erstellen Sie zunächst einen Betreff, der nach dem Start der Ansicht abgeschlossen wird.

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

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

Erstellen Sie dann eine Funktion, die nach Abschluss des Betreffs ein Lambda übernimmt und ausführt.

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

Stellen Sie schließlich sicher, dass Verweise auf ViewChild diese Funktion verwenden.

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

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;
nikojpapa
quelle
1
Dies ist eine viel bessere Lösung als alle unsinnigen Settimeout
Liam
4

Es muss funktionieren.

Aber wie Günter Zöchbauer sagte, muss es ein anderes Problem in der Vorlage geben. Ich habe irgendwie Relevant-Plunkr-Answer erstellt . Bitte überprüfen Sie die Konsole des Browsers.

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

Es wirkt wie ein Zauber. Bitte überprüfen Sie Ihre Tags und Referenzen.

Vielen Dank...

Mikronyken
quelle
1
Wenn das Problem mit meinem identisch ist, müssen Sie zum Duplizieren ein * ngIf in die Vorlage um <filter> </ filter> einfügen. Wenn das ngIf false zurückgibt, wird ViewChild anscheinend nicht verkabelt und gibt null
Dan zurück Chase
2

Das funktioniert bei mir, siehe Beispiel unten.

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
quelle
2

Ich hatte ein ähnliches Problem, bei dem ViewChildsich das in einer switchKlausel befand, die das viewChild-Element nicht geladen hat, bevor auf es verwiesen wurde. Ich habe es auf eine halb-hackige Art und Weise gelöst, aber die ViewChildReferenz in eine setTimeout, die sofort ausgeführt wurde (dh 0ms), eingewickelt.

Nikola Jankovic
quelle
1

Meine Lösung bestand darin, die ngIf von außerhalb der untergeordneten Komponente in die untergeordnete Komponente eines Div zu verschieben, das den gesamten HTML-Abschnitt umhüllte. Auf diese Weise wurde es immer noch versteckt, wenn es sein musste, aber ich konnte die Komponente laden und ich konnte sie im übergeordneten Element referenzieren.

harmonischer Schlüssel
quelle
Aber wie sind Sie dazu zu Ihrer "sichtbaren" Variablen gekommen, die sich im übergeordneten Element befand?
Dan Chase
1

Ich behebe es, indem ich SetTimeout hinzufüge, nachdem die Komponente sichtbar gemacht wurde

Mein HTML:

<input #txtBus *ngIf[show]>

Meine Komponente 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
quelle
1

In meinem Fall wusste ich, dass die untergeordnete Komponente immer vorhanden sein würde, wollte jedoch den Status vor der Initialisierung des untergeordneten Elements ändern, um Arbeit zu sparen.

Ich teste das Kind, bis es angezeigt wird, und nehme sofort Änderungen vor, wodurch mir ein Änderungszyklus für die untergeordnete Komponente erspart wurde.

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

Sie können eine maximale Anzahl von Versuchen oder eine Verzögerung von einigen ms hinzufügen setTimeout. setTimeoutWirft die Funktion effektiv an den unteren Rand der Liste der ausstehenden Vorgänge.

N-aß
quelle
0

Eine Art generischer Ansatz:

Sie können eine Methode erstellen, die wartet, bis ViewChildsie bereit ist

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

Verwendung:

  // Now you can do it in any place of your code
  waitWhileViewChildIsReady(this, 'yourViewChildName').subscribe(() =>{
      // your logic here
  })
Sergei Panfilov
quelle
0

Für mich war das Problem, dass ich auf die ID des Elements verwies.

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

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

Statt so:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>
Jeremy Quick
quelle
0

Wenn Sie Ionic verwenden, müssen Sie den ionViewDidEnter()Lifecycle-Hook verwenden. Ionic läuft einige zusätzliche Sachen (vor allem Animationen bezogen) , die typischerweise unerwartete Fehler wie dies verursacht, daher die Notwendigkeit für etwas , das läuft nach ngOnInit , ngAfterContentInitund so weiter.

Jai
quelle
-1

Hier ist etwas, das für mich funktioniert hat.

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

Also setze ich im Grunde jede Sekunde einen Scheck, bis das *ngIfwahr wird, und dann mache ich meine Sachen im Zusammenhang mit dem ElementRef.

Anjil Dhamala
quelle
-3

Die Lösung, die für mich funktioniert hat, bestand darin, die Direktive in Deklarationen in app.module.ts einzufügen

noro5
quelle