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


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.


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.


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.


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.


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

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.


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.


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;

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


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

}


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)


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.


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

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.


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

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>

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.


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


-3

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.