A anotação Angular 2 @ViewChild retorna indefinida

241

Estou tentando aprender Angular 2.

Gostaria de acessar um componente filho de um componente pai usando a Anotação @ViewChild .

Aqui estão algumas linhas de código:

Em BodyContent.ts eu tenho:

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

enquanto em 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, aqui os modelos (como sugerido nos comentários):

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>

O modelo FilterTiles.html foi carregado corretamente na tag ico-filter-tiles (na verdade, eu consigo ver o cabeçalho).

Nota: a classe BodyContent é injetada dentro de outro modelo (Body) usando DynamicComponetLoader: dcl.loadAsRoot (BodyContent, '# ico-bodyContent', injetor):

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

   } 
}

O problema é que, quando tento gravar ftno log do console, recebo undefinede, é claro, recebo uma exceção ao tentar enviar algo para dentro da matriz "tiles": 'nenhum bloco de propriedades para "indefinido"' .

Mais uma coisa: o componente FilterTiles parece estar carregado corretamente, pois consigo ver o modelo html.

Alguma sugestão? obrigado

Andrea Ialenti
fonte
Parece correto. Talvez algo com o modelo, mas ele não está incluído na sua pergunta.
Günter Zöchbauer
1
Concordou com Günter. Eu criei um plunkr com seu código e modelos simples associados e ele funciona. Consulte este link: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Precisamos que os modelos ;-)
Thierry Templier
1
ftnão seria definido no construtor, mas em um manipulador de eventos de clique já seria definido.
Günter Zöchbauer
5
Você está usando loadAsRoot, que tem um problema conhecido com a detecção de alterações. Apenas para ter certeza de tentar usar loadNextToLocationou loadIntoLocation.
Eric Martinez
1
O problema era loadAsRoot. Uma vez que eu substituí com loadIntoLocationo problema foi resolvido. Se você fizer seu comentário como resposta, posso marcá-lo como aceito.
Andrea Ialenti

Respostas:

373

Eu tive um problema semelhante e pensei em publicar caso alguém cometesse o mesmo erro. Primeiro, uma coisa a considerar é AfterViewInit; você precisa aguardar a inicialização da visualização antes de poder acessar sua @ViewChild. No entanto, meu @ViewChildainda estava retornando nulo. O problema foi meu *ngIf. A *ngIfdiretiva estava matando meu componente de controles, então não pude fazer referência a ela.

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

EDIT
Como mencionado por @Ashg abaixo , uma solução é usar em @ViewChildrenvez de @ViewChild.

Kenecaswell
fonte
9
@kenecaswell Então, você encontrou a melhor maneira de resolver o problema. Eu também estou enfrentando o mesmo problema. Eu tenho muitos * ngIf, para que esse elemento seja o único, afinal, verdadeiro, mas eu preciso da referência do elemento. Qualquer maneira de resolver isso>
monica
4
Eu descobri que o componente filho é 'indefinido' em ngAfterViewInit () se estiver usando ngIf. Eu tentei colocar longos tempos de espera, mas ainda sem efeito. No entanto, o componente filho está disponível posteriormente (ou seja, em resposta a eventos de clique, etc.). Se eu não usar ngIf e estiver definido como esperado em ngAfterViewInit (). Há mais informações sobre comunicação entre pais e filhos aqui angular.io/docs/ts/latest/cookbook/…
Matthew Hegarty
3
Eu usei bootstrap ngClass+ hiddenclasse em vez de ngIf. Isso funcionou. Obrigado!
Rahmathullah M
9
Isso não resolve o problema, use a solução abaixo usando @ViewChildren ir obter uma referência para o controle filho logo que esteja disponível
ASHG
20
Isso apenas prova o "problema", certo? Não publica uma solução.
Miguel Ribeiro
145

O problema mencionado anteriormente é o ngIfque está causando a indefinição da exibição. A resposta é usar em ViewChildrenvez de ViewChild. Eu tive um problema semelhante em que não queria que uma grade fosse exibida até que todos os dados de referência tivessem sido carregados.

html:

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

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


    }
}

Aqui estamos usando o ViewChildrenqual você pode ouvir as alterações. Nesse caso, qualquer criança com a referência #searchGrid. Espero que isto ajude.

Ashg
fonte
4
Gostaria de acrescentar que, em alguns casos, quando você tenta alterar, por exemplo. this.SearchGridpropriedades que você deve usar sintaxe como setTimeout(()=>{ ///your code here }, 1); a Exceção evitar: Expression mudou depois que foi verificada
rafalkasa
3
Como você faz isso se deseja colocar sua tag #searchGrid em um elemento HTML normal em vez de em um elemento Angular2? (Por exemplo, <div #searchGrid> </ div> e este está dentro de um bloco * ngIf?
Vern Jensen
1
esta é a resposta correta para o meu caso de uso! Graças eu preciso acessar um componente como ele vem disponível através ngIf =
Frozen_byte
1
isso funciona perfeitamente nas respostas ajax, agora *ngIffunciona e, depois da renderização, podemos salvar um ElementRef dos componentes dinâmicos.
elporfirio 24/10
4
Também não se esqueça de atribuí-la a uma assinatura e, em seguida, unsubscribe a partir dele
tam.teixeira
63

Você pode usar um setter para @ViewChild()

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

Se você tiver um wrapper ngIf, o setter será chamado com indefinido e, novamente, com uma referência quando o ngIf permitir que ele seja renderizado.

Meu problema era outra coisa. Eu não incluí o módulo que contém meus "FilterTiles" nos meus app.modules. O modelo não gerou um erro, mas a referência sempre foi indefinida.

parlamento
fonte
3
Isso não está funcionando para mim - recebo a primeira indefinida, mas não recebo a segunda chamada com a referência. App é um ng2 ... é esse recurso ng4 +?
Jay Cummins
@ Jay Eu acredito que isso ocorre porque você não registrou o componente no Angular, neste caso FilterTiles. Eu já encontrei esse problema por esse motivo antes.
parlamento
1
Funciona para o Angular 8 usando #paginator no elemento html e anotação como @ViewChild('paginator', {static: false})
Qiteq 20/01
1
É um retorno de chamada para alterações no ViewChild?
Yasser Nascimento
24

Isso funcionou para mim.

Meu componente chamado 'my-component', por exemplo, foi exibido usando * ngIf = "showMe" da seguinte forma:

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

Portanto, quando o componente é inicializado, o componente ainda não é exibido até que "showMe" seja verdadeiro. Assim, minhas referências @ViewChild eram todas indefinidas.

Foi aqui que usei o @ViewChildren e o QueryList que ele retorna. Consulte o artigo angular sobre QueryList e uma demonstração de uso do @ViewChildren .

Você pode usar o QueryList que o @ViewChildren retorna e se inscrever em quaisquer alterações nos itens referenciados usando o rxjs, como mostrado abaixo. O @ViewChild não tem essa capacidade.

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 isso ajude alguém a economizar tempo e frustração.

Joshua Dyck
fonte
3
Na verdade, sua solução parece ser a melhor abordagem listada até agora. OBSERVAÇÃO Temos que ter em mente que a solução das 73 principais agora está SUBSTITUÍDA ... uma vez que a diretiva: [...] a declaração não é mais suportada no Angular 4. IOW não funcionará no cenário do Angular 4
PeteZaria
5
Não se esqueça de cancelar a assinatura ou usar .take(1).subscribe(), mas excelente resposta, muito obrigado!
Blair Connolly
2
Excelente solução. Eu assinei as alterações ref em ngAfterViewInit () em vez de ngOnChanges (). Mas eu tive que adicionar um setTimeout para se livrar do erro ExpressionChangedAfterChecked
Josf
Isso deve ser marcado como a solução real. Muito obrigado!
platzhersh
10

Minha solução alternativa era usar em [style.display]="getControlsOnStyleDisplay()"vez de *ngIf="controlsOn". O bloco está lá, mas não é exibido.

@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
fonte
Tenha uma página em que uma lista de itens seja mostrada em uma tabela ou o item de edição seja mostrado, com base no valor da variável showList. Livre-se do irritante erro do console usando o [style.display] = "! ShowList", combinado com * ngIf = "! ShowList".
razvanone
9

O que resolveu o meu problema foi garantir que staticestava definido como false.

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

Com staticdesativada, a @ViewChildreferência é atualizada pelo Angular quando a *ngIfdiretiva é alterada.

Paul LeBeau
fonte
1
Este é um respondedor quase perfeito, apenas para apontar é uma boa prática também verificar valores anuláveis, portanto, terminamos com algo assim: @ViewChild (ClrForm, {static: false}) set clrForm (clrForm: ClrForm) {if (clrForm) {this.clrForm = clrForm; }};
Klaus Klein
Tentei muitas coisas e finalmente achei que era a culpada.
Manish Sharma
8

Minha solução para isso era substituir *ngIf com [hidden]. A desvantagem foi que todos os componentes filhos estavam presentes no código DOM. Mas trabalhou para minhas necessidades.

Suzanne Suzanne
fonte
5

No meu caso, eu tinha um setter de variável de entrada usando o ViewChild, e ViewChildestava dentro de uma *ngIfdiretiva; portanto, o setter estava tentando acessá-lo antes da *ngIfrenderização (funcionaria bem sem o *ngIf, mas não funcionaria se estivesse sempre definido como verdadeiro com *ngIf="true").

Para resolver, usei o Rxjs para garantir que qualquer referência ao ViewChildesperado até o início da exibição. Primeiro, crie um Assunto que seja concluído quando depois da visualização init.

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

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

Em seguida, crie uma função que obtenha e execute uma lambda após a conclusão do assunto.

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

Por fim, verifique se as referências ao ViewChild usam essa função.

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

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;
nikojpapa
fonte
1
Esta é uma solução muito melhor do que todo o nonesense setTimeout
Liam
4

Isso deve funcionar.

Mas como Günter Zöchbauer disse que deve haver algum outro problema no modelo. Eu criei uma resposta meio relevante . Por favor, verifique o console do 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(){};
 }

Ele funciona como um encanto. Verifique suas tags e referências.

Obrigado...

micronyks
fonte
1
Se o problema for o mesmo que o meu, para duplicar, você precisará colocar um * ngIf no modelo em torno de <filter> </filter>. Aparentemente, se o ngIf retornar falso, o ViewChild não será conectado e retornará nulo
Dan Chase
2

Isso funciona para mim, veja o exemplo abaixo.

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

Eu tive um problema semelhante, onde ViewChildestava dentro de uma switchcláusula que não carregava o elemento viewChild antes de ser referenciado. Eu o resolvi de maneira semi-hacky, mas envolvendo a ViewChildreferência em uma setTimeoutexecutada imediatamente (ou seja, 0ms)

Nikola Jankovic
fonte
1

Minha solução para isso foi mover o ngIf de fora do componente filho para dentro do componente filho em uma div que envolvesse toda a seção do html. Dessa forma, ele ainda estava ficando oculto quando precisava, mas conseguiu carregar o componente e eu pude referenciá-lo no pai.

harmonickey
fonte
Mas, para isso, como você chegou à sua variável "visível" que estava no pai?
Dan Chase
1

Corrijo-o apenas adicionando SetTimeout após definir o componente visível

Meu HTML:

<input #txtBus *ngIf[show]>

Meu 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);
  }
}
Osvaldo Leiva
fonte
1

No meu caso, eu sabia que o componente filho sempre estaria presente, mas queria alterar o estado anterior à inicialização do filho para economizar trabalho.

Eu escolho testar a criança até que ela apareça e faça alterações imediatamente, o que me salvou de um ciclo de alterações no componente filho.

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

Em alerta, você pode adicionar um número máximo de tentativas ou adicionar alguns ms de atraso ao arquivo setTimeout. setTimeoutlança efetivamente a função no final da lista de operações pendentes.

N-ate
fonte
0

Uma espécie de abordagem genérica:

Você pode criar um método que espere até ViewChildque esteja 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
  })
Sergei Panfilov
fonte
0

Para mim, o problema era que eu estava referenciando o ID no elemento.

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

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

Em vez de assim:

@ViewChild('surveyForm') slides:IonSlides;

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

Se você estiver usando o Ionic, precisará usar o ionViewDidEnter()gancho do ciclo de vida. Ionic corre algum material adicional (principalmente de animação relacionadas), que normalmente provoca erros inesperados como este, daí a necessidade de algo que é executado depois ngOnInit , ngAfterContentInite assim por diante.

Jai
fonte
-1

Aqui está algo que funcionou para mim.

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

Então, basicamente, eu faço uma verificação a cada segundo até que *ngIfse torne realidade e depois faço minhas coisas relacionadas à ElementRef.

Anjil Dhamala
fonte
-3

A solução que funcionou para mim foi adicionar a diretiva nas declarações em app.module.ts

noro5
fonte