L'annotazione Angular 2 @ViewChild restituisce undefined


242

Sto cercando di imparare Angular 2.

Vorrei accedere a un componente figlio da un componente padre usando l' annotazione @ViewChild .

Ecco alcune righe di codice:

In BodyContent.ts ho:

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

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

Finalmente ecco i template (come suggerito nei commenti):

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>

Il modello FilterTiles.html è caricato correttamente nel tag ico-filter-tiles (in effetti sono in grado di vedere l'intestazione).

Nota: la classe BodyContent viene iniettata all'interno di un altro modello (Body) utilizzando 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);

   } 
}

Il problema è che quando provo a scrivere ftnel registro della console, ottengo undefined, e ovviamente ottengo un'eccezione quando provo a inserire qualcosa all'interno dell'array "tiles": "nessuna proprietà tile per" undefined "" .

Ancora una cosa: il componente FilterTiles sembra essere caricato correttamente, dato che sono in grado di vedere il modello html per esso.

Qualche suggerimento? Grazie


Sembra corretto Forse qualcosa con il modello, ma non è incluso nella tua domanda.
Günter Zöchbauer,

1
Concordato con Günter. Ho creato un plunkr con il tuo codice e semplici modelli associati e funziona. Vedi questo link: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Abbiamo bisogno dei modelli ;-)
Thierry Templier,

1
ftnon sarebbe impostato nel costruttore, ma in un gestore eventi click sarebbe già impostato.
Günter Zöchbauer,

5
Stai utilizzando loadAsRoot, che ha un problema noto con il rilevamento delle modifiche. Solo per assicurarsi di provare a usare loadNextToLocationo loadIntoLocation.
Eric Martinez,

1
Il problema era loadAsRoot. Una volta sostituito con loadIntoLocationil problema è stato risolto. Se fai il tuo commento come risposta posso contrassegnarlo come accettato
Andrea Ialenti

Risposte:


374

Ho avuto un problema simile e ho pensato di pubblicare nel caso in cui qualcun altro avesse commesso lo stesso errore. Innanzitutto, una cosa da considerare è AfterViewInit; devi attendere l'inizializzazione della vista prima di poter accedere a @ViewChild. Tuttavia, il mio @ViewChildstava ancora tornando null. Il problema era mio *ngIf. La *ngIfdirettiva stava uccidendo il mio componente di controllo, quindi non potevo fare riferimento.

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

Spero che aiuti.

MODIFICA
Come menzionato da @Ashg di seguito , al @ViewChildrenposto di utilizzare una soluzione @ViewChild.


9
@kenecaswell Quindi hai trovato il modo migliore per risolvere il problema. Sto affrontando anche lo stesso problema. Ho molti * ngIf in modo tale che l'elemento sia nel solo dopo tutto vero, ma ho bisogno del riferimento dell'elemento. Un modo per risolverlo>
monica,

4
Ho scoperto che il componente figlio è 'indefinito' in ngAfterViewInit () se si utilizza ngIf. Ho provato a mettere timeout lunghi ma ancora nessun effetto. Tuttavia, il componente figlio è disponibile in un secondo momento (ad es. In risposta a eventi di clic ecc.). Se non uso ngIf ed è definito come previsto in ngAfterViewInit (). C'è di più sulla comunicazione genitore / figlio qui angular.io/docs/ts/latest/cookbook/…
Matthew Hegarty,

3
Ho usato bootstrap ngClass+ hiddenclass invece di ngIf. Ha funzionato Grazie!
Rahmathullah M,

9
Questo non risolve il problema, usa la soluzione seguente usando @ViewChildren per ottenere un riferimento al controllo figlio una volta che è disponibile
Ashg,

20
Questo dimostra solo il "problema", giusto? Non pubblica una soluzione.
Miguel Ribeiro,

146

Il problema, come menzionato in precedenza, è il ngIfmotivo per cui la vista non è definita. La risposta è usare al ViewChildrenposto di ViewChild. Ho avuto un problema simile in cui non volevo che fosse mostrata una griglia finché non fossero stati caricati tutti i dati di riferimento.

html:

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

Codice componente

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


    }
}

Qui stiamo usando ViewChildrensu cui è possibile ascoltare le modifiche. In questo caso tutti i bambini con il riferimento #searchGrid. Spero che questo ti aiuti.


4
Vorrei aggiungerlo in alcuni casi quando provi a cambiare ad es. this.SearchGridproprietà da usare come sintassi setTimeout(()=>{ ///your code here }, 1); per evitare Eccezione: l'espressione è cambiata dopo che è stata verificata
rafalkasa,

3
Come si fa se si desidera posizionare il tag #searchGrid su un normale elemento HTML anziché su un elemento Angular2? (Ad esempio, <div #searchGrid> </div> e questo è all'interno di un blocco * ngIf?
Vern Jensen,

1
questa è la risposta corretta per il mio caso d'uso! Grazie, ho bisogno di accedere a un componente che diventa disponibile tramite ngIf =
Frozen_byte,

1
questo funziona perfettamente su risposte ajax, ora *ngIffunziona, e dopo il rendering possiamo salvare un ElementRef dai componenti dinamici.
elporfirio,

4
Inoltre, non dimenticare di assegnarlo a un abbonamento e quindi annullare l'iscrizione da esso
tam.teixeira,

64

Puoi usare un setter per @ViewChild()

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

Se si dispone di un wrapper ngIf, il setter verrà chiamato con undefined, quindi nuovamente con un riferimento quando ngIf gli consente di eseguire il rendering.

Il mio problema era qualcos'altro però. Non avevo incluso il modulo contenente i miei "FilterTiles" nei miei moduli di app. Il modello non ha generato un errore ma il riferimento era sempre indefinito.


3
Questo non funziona per me - ottengo il primo indefinito, ma non ricevo la seconda chiamata con il riferimento. L'app è una ng2 ... questa è la funzione ng4 +?
Jay Cummins,

@Jay Credo che questo sia perché non hai registrato il componente con Angular, in questo caso FilterTiles. Ho riscontrato questo problema per quel motivo prima.
parlamento,

1
Funziona con Angular 8 usando #paginator su elemento html e annotazioni come@ViewChild('paginator', {static: false})
Qiteq

1
È un callback per le modifiche di ViewChild?
Yasser Nascimento,

24

Questo ha funzionato per me.

Il mio componente chiamato 'my-component', ad esempio, è stato visualizzato usando * ngIf = "showMe" in questo modo:

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

Pertanto, quando il componente viene inizializzato, il componente non viene ancora visualizzato fino a quando "showMe" è true. Pertanto, i miei riferimenti a @ViewChild erano tutti indefiniti.

È qui che ho usato @ViewChildren e la QueryList che restituisce. Vedi l' articolo angolare su QueryList e una demo sull'uso di @ViewChildren .

È possibile utilizzare l'Elenco query che @ViewChildren restituisce e sottoscrivere eventuali modifiche agli elementi di riferimento utilizzando rxjs come mostrato di seguito. @ViewChild non ha questa 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

Spero che questo aiuti qualcuno a risparmiare un po 'di tempo e frustrazione.


3
In effetti la tua soluzione sembra essere l'approccio migliore elencato finora. NOTA Dobbiamo tenere presente che la soluzione 73 principale è ora DEPRECATA ... poiché la direttiva: la [...] dichiarazione NON è più supportata in Angular 4. IOW NON funzionerà nello scenario Angular 4
PeteZaria

5
Non dimenticare di annullare l'iscrizione o di utilizzare .take(1).subscribe(), ma un'ottima risposta, grazie mille!
Blair Connolly,

2
Soluzione eccellente Ho sottoscritto le modifiche ref in ngAfterViewInit () anziché in ngOnChanges (). Ma ho dovuto aggiungere un setTimeout per eliminare ExpressionChangedAfterChecked error
Josf

Questo dovrebbe essere contrassegnato come la soluzione effettiva. Molte grazie!
platzhersh,

10

La mia soluzione alternativa era usare al [style.display]="getControlsOnStyleDisplay()"posto di *ngIf="controlsOn". Il blocco è presente ma non viene visualizzato.

@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";
    }
  }
....

Avere una pagina in cui viene mostrato un elenco di elementi in una tabella o l'elemento di modifica, in base al valore della variabile showList. Sbarazzati del fastidioso errore della console usando [style.display] = "! ShowList", combinato con * ngIf = "! ShowList".
razvanone,

9

Ciò che risolveva il mio problema era assicurarsi che staticfosse impostato su false.

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

Se staticdisattivato, il @ViewChildriferimento viene aggiornato da Angular quando viene modificata la *ngIfdirettiva.


1
Questa è una risposta quasi perfetta, solo per puntare è una buona pratica per verificare anche i valori nullable, quindi finiamo con qualcosa del genere: @ViewChild (ClrForm, {static: false}) set clrForm (clrForm: ClrForm) {if (clrForm) {this.clrForm = clrForm; }};
Klaus Klein, il

Ho provato così tante cose e finalmente ho trovato questa cosa come colpevole.
Manish Sharma,

8

La mia soluzione a questo è stato di sostituire *ngIf con [hidden]. Il rovescio della medaglia era che tutti i componenti figlio erano presenti nel codice DOM. Ma ha funzionato per le mie esigenze.


5

Nel mio caso, avevo un setter di variabili di input che utilizzava il ViewChild, ed ViewChildera all'interno di una *ngIfdirettiva, quindi il setter stava provando ad accedervi prima del *ngIfrendering (avrebbe funzionato bene senza il *ngIf, ma non avrebbe funzionato se fosse sempre impostato su vero con *ngIf="true").

Per risolvere, ho usato Rxjs per essere sicuro di ogni riferimento all'attesa fino all'avvio ViewChilddella vista. Innanzitutto, crea un soggetto che completa quando dopo view init.

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

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

Quindi, crea una funzione che accetta ed esegue un lambda dopo il completamento del soggetto.

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

Infine, assicurati che i riferimenti a ViewChild utilizzino questa funzione.

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

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;

1
Questa è una soluzione molto migliore di tutte le assurdità assurde
Liam

4

Deve funzionare.

Ma come ha detto Günter Zöchbauer, ci deve essere qualche altro problema nel modello. Ho creato un po ' Relevant-Plunkr-Answer . Si prega di controllare la console del browser.

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

Esso funziona magicamente. Per favore ricontrolla i tuoi tag e riferimenti.

Grazie...


1
Se il problema è uguale al mio, per duplicare dovrai inserire un * ngIf nel modello attorno a <filter> </filter> .. Apparentemente se ngIf restituisce falso, ViewChild non viene cablato e restituisce null
Dan Chase

2

Questo funziona per me, vedi l'esempio di seguito.

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

}


2

Ho avuto un problema simile, in cui ViewChildera all'interno di una switchclausola che non stava caricando l'elemento viewChild prima che venisse referenziato. L'ho risolto in modo semi-disordinato ma avvolgendo il ViewChildriferimento in un setTimeoutche è stato eseguito immediatamente (ovvero 0 ms)


1

La mia soluzione a questo era di spostare ngIf dall'esterno del componente figlio all'interno del componente figlio su un div che includesse l'intera sezione di HTML. In quel modo era ancora nascosto quando doveva essere, ma era in grado di caricare il componente e potevo fare riferimento a esso nel genitore.


Ma per quello, come sei arrivato alla tua variabile "visibile" che era nel genitore?
Dan Chase,

1

Lo aggiusto semplicemente aggiungendo SetTimeout dopo aver impostato il componente visibile

Il mio HTML:

<input #txtBus *ngIf[show]>

Il mio componente 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);
  }
}

1

Nel mio caso, sapevo che il componente figlio sarebbe sempre stato presente, ma volevo modificare lo stato prima dell'inizializzazione figlio per risparmiare lavoro.

Ho scelto di testare il bambino fino a quando non è apparso e apportare modifiche immediatamente, il che mi ha salvato un ciclo di modifica sul componente figlio.

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

In modo allertivo, è possibile aggiungere un numero massimo di tentativi o aggiungere un ritardo di alcuni ms al file setTimeout. setTimeoutlancia efficacemente la funzione in fondo all'elenco delle operazioni in sospeso.


0

Una sorta di approccio generico:

È possibile creare un metodo che attenderà fino ViewChilda quando non sarà pronto

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

Uso:

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

0

Per me il problema era che stavo facendo riferimento all'ID sull'elemento.

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

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

Invece di così:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>

0

Se stai usando Ionic dovrai usare il ionViewDidEnter()gancio del ciclo di vita. Ionico gestisce alcune cose supplementari (principalmente di animazione-correlate) che provoca in genere errori imprevisti come questo, da qui la necessità di qualcosa che corre dopo ngOnInit , ngAfterContentInite così via.


-1

Ecco qualcosa che ha funzionato per me.

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

Quindi fondamentalmente ho impostato un controllo ogni secondo fino a quando non *ngIfdiventa realtà e poi faccio le mie cose relative al ElementRef.


-3

La soluzione che ha funzionato per me è stata quella di aggiungere la direttiva nelle dichiarazioni in app.module.ts

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.