La anotación angular 2 @ViewChild devuelve indefinida

242

Estoy tratando de aprender Angular 2.

Me gustaría acceder a un componente secundario desde un componente primario utilizando la Anotación @ViewChild .

Aquí algunas líneas de código:

En BodyContent.ts tengo:

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

mientras que en 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 aquí las plantillas (como se sugiere en los comentarios):

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>

La plantilla FilterTiles.html está cargada correctamente en la etiqueta ico-filter-tiles (de hecho, puedo ver el encabezado).

Nota: la clase BodyContent se inyecta dentro de otra plantilla (Body) usando DynamicComponetLoader: dcl.loadAsRoot (BodyContent, '# ico-bodyContent', inyector):

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

   } 
}

El problema es que cuando trato de escribir ften el registro de la consola, obtengo undefined, y por supuesto obtengo una excepción cuando intento insertar algo dentro de la matriz de "mosaicos": 'no hay mosaicos de propiedades para "indefinido"' .

Una cosa más: el componente FilterTiles parece estar cargado correctamente, ya que puedo ver la plantilla html para él.

¿Cualquier sugerencia? Gracias

Andrea Ialenti
fuente
Parece correcto Tal vez algo con la plantilla, pero no está incluido en su pregunta.
Günter Zöchbauer
1
De acuerdo con Günter. Creé un plunkr con su código y plantillas simples asociadas y funciona. Vea este enlace: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Necesitamos las plantillas ;-)
Thierry Templier
1
ftno se establecería en el constructor, pero en un controlador de eventos click ya estaría configurado.
Günter Zöchbauer
55
Estás utilizando loadAsRoot, que tiene un problema conocido con la detección de cambios. Solo para asegurarte de usar loadNextToLocationo loadIntoLocation.
Eric Martinez
1
El problema era loadAsRoot. Una vez que lo reemplacé con loadIntoLocationel problema se resolvió. Si haces tu comentario como respuesta, puedo marcarlo como aceptado
Andrea Ialenti

Respuestas:

374

Tuve un problema similar y pensé en publicar en caso de que alguien cometiera el mismo error. Primero, una cosa a considerar es AfterViewInit; debe esperar a que se inicialice la vista antes de poder acceder a su @ViewChild. Sin embargo, mi @ViewChildtodavía regresaba nulo. El problema fue mi *ngIf. La *ngIfdirectiva estaba matando mi componente de controles, así que no pude hacer referencia a él.

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

Espero que ayude.

EDITAR
Como se menciona en @Ashg a continuación , una solución es usar en @ViewChildrenlugar de @ViewChild.

Kenecaswell
fuente
99
@kenecaswell Entonces, ¿encontraste la mejor manera de resolver el problema? También estoy enfrentando el mismo problema. Tengo muchos * ngIf para que ese elemento sea verdadero después de todo, pero necesito la referencia del elemento. Cualquier forma de resolver esto>
monica
44
Encontré que el componente hijo está 'indefinido' en ngAfterViewInit () si usa ngIf. Traté de poner largos tiempos de espera, pero aún sin efecto. Sin embargo, el componente hijo está disponible más tarde (es decir, en respuesta a eventos de clic, etc.). Si no uso ngIf y se define como se espera en ngAfterViewInit (). Aquí encontrará más información sobre la comunicación entre padres e hijos angular.io/docs/ts/latest/cookbook/…
Matthew Hegarty
3
Usé bootstrap ngClass+ hiddenclass en lugar de ngIf. Eso funciono. ¡Gracias!
Rahmathullah M
99
Esto no resuelve el problema, use la solución a continuación usando @ViewChildren, vaya a obtener una referencia al control secundario una vez que esté disponible
Ashg
20
Esto solo prueba el "problema", ¿verdad? No publica una solución.
Miguel Ribeiro
146

El problema como se mencionó anteriormente es el ngIfque hace que la vista sea indefinida. La respuesta es usar en ViewChildrenlugar de ViewChild. Tuve un problema similar en el que no quería que se mostrara una cuadrícula hasta que se hayan cargado todos los datos de referencia.

html:

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

Código de 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;
        });


    }
}

Aquí estamos utilizando ViewChildrenen el que puede escuchar los cambios. En este caso, cualquier niño con la referencia #searchGrid. Espero que esto ayude.

Ashg
fuente
44
Me gustaría agregar eso en algunos casos cuando intentas cambiar, por ejemplo. this.SearchGridpropiedades que debe usar setTimeout(()=>{ ///your code here }, 1); para evitar la sintaxis Excepción: la expresión ha cambiado después de que se verificó
rafalkasa
3
¿Cómo haces esto si quieres colocar tu etiqueta #searchGrid en un elemento HTML normal en lugar de un elemento Angular2? (Por ejemplo, <div #searchGrid> </div> y esto está dentro de un bloque * ngIf?
Vern Jensen
1
¡Esta es la respuesta correcta para mi caso de uso! Gracias, necesito acceder a un componente, ya que está disponible a través de ngIf =
Frozen_byte
1
esto funciona perfecto en las respuestas ajax, ahora *ngIffunciona, y después de renderizar podemos guardar un ElementRef de los componentes dinámicos.
elporfirio
44
Además, no olvide asignarlo a una suscripción y luego cancelar la suscripción a la misma
tam.teixeira
64

Podrías usar un setter para @ViewChild()

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

Si tiene un contenedor ngIf, se llamará al setter con undefined, y luego nuevamente con una referencia una vez que ngIf le permita renderizar.

Sin embargo, mi problema era otra cosa. No había incluido el módulo que contenía mis "FilterTiles" en mi app.modules. La plantilla no arrojó un error, pero la referencia siempre fue indefinida.

parlamento
fuente
3
Esto no funciona para mí: obtengo la primera indefinida, pero no recibo la segunda llamada con la referencia. La aplicación es un ng2 ... ¿es esta función ng4 +?
Jay Cummins
@ Jay Creo que esto se debe a que no ha registrado el componente con Angular, en este caso FilterTiles. He encontrado ese problema por esa razón antes.
Parlamento
1
Funciona para Angular 8 usando #paginator en elemento html y anotaciones como@ViewChild('paginator', {static: false})
Qiteq
1
¿Es esto una devolución de llamada para los cambios de ViewChild?
Yasser Nascimento
24

Esto funcionó para mí.

Mi componente llamado 'my-component', por ejemplo, se mostró usando * ngIf = "showMe" así:

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

Entonces, cuando el componente se inicializa, el componente aún no se muestra hasta que "showMe" sea verdadero. Por lo tanto, mis referencias @ViewChild no estaban definidas.

Aquí es donde utilicé @ViewChildren y la QueryList que devuelve. Vea el artículo angular en QueryList y una demostración de uso de @ViewChildren .

Puede usar la Lista de consultas que devuelve @ViewChildren y suscribirse a cualquier cambio en los elementos a los que se hace referencia utilizando rxjs como se ve a continuación. @ViewChild no tiene esta habilidad.

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

Espero que esto ayude a alguien a ahorrar algo de tiempo y frustración.

Joshua Dyck
fuente
3
De hecho, su solución parece ser el mejor enfoque enumerado hasta ahora. NOTA Debemos tener en cuenta que la solución top 73 ahora está DEPRECADA ... ya que la directiva: [...] la declaración ya no se admite en Angular 4. AHORA NO funcionará en el escenario Angular 4
PeteZaria
55
No te olvides de darte de baja o usar .take(1).subscribe(), pero excelente respuesta, ¡muchas gracias!
Blair Connolly
2
Excelente solucion. Me he suscrito a los cambios de referencia en ngAfterViewInit () en lugar de ngOnChanges (). Pero tuve que agregar un setTimeout para deshacerme del error ExpressionChangedAfterChecked
Josf
Esto debe marcarse como la solución real. ¡Muchas gracias!
platzhersh
10

Mi solución era usar en [style.display]="getControlsOnStyleDisplay()"lugar de *ngIf="controlsOn". El bloque está ahí pero no se muestra.

@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
fuente
Tenga una página donde se muestre una lista de elementos en una tabla, o se muestre el elemento de edición, según el valor de la variable showList. Eliminó el molesto error de la consola utilizando [style.display] = "! ShowList", combinado con * ngIf = "! ShowList".
razvanone
9

Lo que resolvió mi problema fue asegurarme de que staticestaba configurado en false.

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

Con staticdesactivado, @ViewChildAngular actualiza la referencia cuando *ngIfcambia la directiva.

Paul LeBeau
fuente
1
Esta es una respuesta casi perfecta, solo para señalar es una buena práctica también verificar los valores anulables, por lo que terminamos con algo como esto: @ViewChild (ClrForm, {static: false}) set clrForm (clrForm: ClrForm) {if (clrForm) {this.clrForm = clrForm; }};
Klaus Klein
Intenté muchas cosas y finalmente encontré que esto era el culpable.
Manish Sharma
8

Mi solución a esto fue reemplazar *ngIf con [hidden]. Lo malo era que todos los componentes secundarios estaban presentes en el código DOM. Pero funcionó para mis requisitos.

suzanne suzanne
fuente
5

En mi caso, tenía un setter de variables de entrada usando ViewChild, y ViewChildestaba dentro de una *ngIfdirectiva, por lo que el setter intentaba acceder antes que el *ngIfrenderizado (funcionaría bien sin el *ngIf, pero no funcionaría si siempre estuviera configurado en cierto con *ngIf="true").

Para resolverlo, utilicé Rxjs para asegurarme de que cualquier referencia al ViewChildesperado hasta que se iniciara la vista. Primero, cree un Asunto que se complete después de ver init.

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

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

Luego, cree una función que tome y ejecute una lambda después de que el sujeto se complete.

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

Finalmente, asegúrese de que las referencias a ViewChild usen esta función.

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

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;
nikojpapa
fuente
1
Esta es una solución mucho mejor que todo el settimeout sin sentido
Liam
4

Debe funcionar

Pero como dijo Günter Zöchbauer , debe haber algún otro problema en la plantilla. He creado un poco Relevant-Plunkr-Answer . Por favor, compruebe la consola del navegador.

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

Funciona a las mil maravillas. Por favor revise sus etiquetas y referencias.

Gracias...

micronyks
fuente
1
Si el problema es el mismo que el mío, para duplicarlo necesitaría poner un * ngIf en la plantilla alrededor de <filter> </filter> .. Aparentemente, si ngIf devuelve falso, ViewChild no se conecta y devuelve nulo
Dan Chase
2

Esto funciona para mí, vea el ejemplo a continuación.

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

Tuve un problema similar, donde ViewChildestaba dentro de una switchcláusula que no estaba cargando el elemento viewChild antes de que fuera referenciado. Lo resolví de una manera semi-hacky pero envolviendo la ViewChildreferencia en una setTimeoutque se ejecutó de inmediato (es decir, 0 ms)

Nikola Jankovic
fuente
1

Mi solución a esto fue mover el ngIf desde el exterior del componente secundario al interior del componente secundario en un div que envolvía toda la sección de html. De esa manera, todavía se ocultaba cuando era necesario, pero podía cargar el componente y podía hacer referencia a él en el padre.

clave armónica
fuente
Pero para eso, ¿cómo llegaste a tu variable "visible" que estaba en el padre?
Dan Chase,
1

Lo arreglo simplemente agregando SetTimeout después de establecer visible el componente

Mi HTML:

<input #txtBus *ngIf[show]>

My Component 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
fuente
1

En mi caso, sabía que el componente hijo siempre estaría presente, pero quería alterar el estado antes de que el niño se inicializara para ahorrar trabajo.

Elijo probar el niño hasta que apareció y hacer cambios de inmediato, lo que me ahorró un ciclo de cambio en el componente hijo.

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

Alertativamente, puede agregar un número máximo de intentos o agregar algunos ms de retraso al setTimeout. setTimeoutefectivamente lanza la función al final de la lista de operaciones pendientes.

N-ate
fuente
0

Un tipo de enfoque genérico:

Puede crear un método que esperará hasta ViewChildque esté listo

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

Para mí, el problema era que hacía referencia a la ID del elemento.

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

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

En lugar de esto:

@ViewChild('surveyForm') slides:IonSlides;

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

Si usa Ionic, deberá usar el ionViewDidEnter()gancho del ciclo de vida. Iónica corre un poco de materia adicional (principalmente relacionados con la animación) que normalmente provoca errores inesperados como éste, de ahí la necesidad de algo que se ejecuta después ngOnInit , ngAfterContentInity así sucesivamente.

Jai
fuente
-1

Aquí hay algo que funcionó para mí.

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

Así que básicamente establezco una verificación cada segundo hasta que se *ngIfhace realidad y luego hago mis cosas relacionadas con el ElementRef.

Anjil Dhamala
fuente
-3

La solución que funcionó para mí fue agregar la directiva en las declaraciones en app.module.ts

noro5
fuente