web-dev-qa-db-de.com

Angular 2 @ViewChild Annotation gibt undefined zurück

Ich versuche Angular 2 zu lernen.

Ich möchte über eine übergeordnete Komponente mit der @ViewChild Annotation auf eine untergeordnete Komponente zugreifen.

Hier einige Codezeilen:

InBodyContent.tsIch habe:

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

inFilterTiles.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 noch die Vorlagen (wie in 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" ist ordnungsgemäß in ico-filter-tile tag geladen (in der Tat kann ich die Kopfzeile sehen).

Hinweis: Die BodyContent-Klasse wird mithilfe von DynamicComponetLoader in eine andere Vorlage (Body) eingefügt: dcl.loadAsRoot (BodyContent, '# ico-bodyContent', Injektor):

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, wenn ich versuche, ft in das Konsolenprotokoll zu schreiben, ich undefined bekomme und natürlich eine Ausnahme bekomme, wenn ich versuche, etwas in das "tile" -Array zu schieben: 'keine Eigenschaftskacheln für "undefined") '.

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

Irgendein Vorschlag? Vielen Dank

138
Andrea Ialenti

Ich hatte ein ähnliches Problem und dachte, ich würde posten, falls jemand anderes den gleichen Fehler gemacht hätte. Zunächst ist zu beachten, AfterViewInit; Sie müssen warten, bis die Ansicht initialisiert ist, bevor Sie auf Ihren @ViewChild zugreifen können. Mein @ViewChild gab jedoch immer noch null zurück. Das Problem war mein *ngIf. Die *ngIf-Direktive tötete meine Steuerelementkomponente, 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 von @Ashg unten erwähnt, ist eine Lösung die Verwendung von @ViewChildren anstelle von @ViewChild.

228
kenecaswell

Wie bereits erwähnt, handelt es sich bei dem Problem um die ngIf, die dazu führt, dass die Ansicht undefiniert ist. Die Antwort ist, ViewChildren anstelle von ViewChild zu verwenden. 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 ViewChildren, auf denen Sie auf Änderungen achten können. In diesem Fall alle Kinder mit der Referenz #searchGrid. Hoffe das hilft.

92
Ashg

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

@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 ermöglicht.

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, die Referenz war jedoch immer undefiniert.

47
parliament

Das hat bei mir funktioniert.

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

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

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

Hier habe ich @ViewChildren und die QueryList verwendet, die es zurückgibt. Siehe eckiger Artikel über QueryList und eine @ViewChildren-Nutzungsdemo

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

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

Ich hoffe, das hilft jemandem, Zeit und Frustration zu sparen.

16
Joshua Dyck

Mein Workaround bestand darin, [style.display] = "getControlsOnStyleDisplay ()" anstelle von * ngIf = "controlsOn" zu verwenden. Der Block ist vorhanden, wird aber 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";
    }
  }
....
6
Calderas

Es muss funktionieren. 

Aber wie Günter Zöchbauer gesagt hat, muss es ein anderes Problem in der Vorlage geben. Ich habe ein bisschen Relevant-Plunkr-Answer erstellt. Bitte überprüfen Sie die Browserkonsole.

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

3
micronyks

Das funktioniert für mich, siehe das 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(); });
      }
    }

}

3
NiZa

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

3
suzanne suzanne

Meine Lösung hierfür war, die ngIf von außerhalb der untergeordneten Komponente in die untergeordnete Komponente eines div zu verschieben, das den gesamten Abschnitt von html umhüllt. Auf diese Weise wurde es immer noch ausgeblendet, wenn es sein musste, konnte aber die Komponente laden und ich konnte sie im übergeordneten Element referenzieren.

2
harmonickey

Ich behebe es, indem ich SetTimeout fü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);
  }
}
2
Osvaldo Leiva

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

Ich habe mich entschieden, das Kind zu testen, bis es erscheint, und es werden sofort Änderungen vorgenommen, was mir einen Änderungszyklus für die untergeordnete Komponente erspart.

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

Alertnativ können Sie eine maximale Anzahl von Versuchen hinzufügen oder die setTimeout um einige ms verzögern. setTimeout wirft die Funktion effektiv an das Ende der Liste der ausstehenden Vorgänge.

1
N-ate

Ich hatte ein ähnliches Problem, bei dem sich die Variable ViewChild innerhalb einer Klausel switch befand, die das viewChild-Element nicht geladen hat, bevor es referenziert wurde. Ich habe es auf halb hackige Weise gelöst, aber die ViewChild-Referenz in eine setTimeout eingeschlossen, die sofort ausgeführt wurde (d. H. 0ms)

1
Nikola Jankovic

In meinem Fall hatte ich eine Eingabevariable, die ViewChild verwendet, und ViewChild befand sich in einer * ngIf-Direktive. Der Setter versuchte also, auf ihn zuzugreifen, bevor * ngIf gerendert wird (dies würde ohne die * ngIf funktionieren, würde aber funktionieren funktioniert nicht, wenn es mit * ngIf = "true" immer auf true gesetzt wurde).

Zur Lösung verwendete ich Rxjs, um sicherzustellen, dass alle Verweise auf die ViewChild gewartet haben, bis die Ansicht gestartet wurde. Erstellen Sie zunächst einen Betreff, der nach dem Anzeigen von Init 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 ausführt.

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

Stellen Sie abschließend 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;
1
nikojpapa

Eine Art generischer Ansatz:

Sie können eine Methode erstellen, die wartet, bis ViewChild fertig 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;
}

Verwendungszweck:

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

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

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

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

Anstatt wie folgt:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>
0
jrquick