@ViewChild em * ngIf

215

Questão

Qual é a maneira mais elegante de obter @ViewChilddepois que o elemento correspondente no modelo foi mostrado?

Abaixo está um exemplo. Também Plunker disponível.

Modelo:

<div id="layout" *ngIf="display">
    <div #contentPlaceholder></div>
</div>

Componente:

export class AppComponent {

    display = false;
    @ViewChild('contentPlaceholder', {read: ViewContainerRef}) viewContainerRef;

    show() {
        this.display = true;
        console.log(this.viewContainerRef); // undefined
        setTimeout(()=> {
            console.log(this.viewContainerRef); // OK
        }, 1);
    }
}

Eu tenho um componente com seu conteúdo oculto por padrão. Quando alguém chama o show()método, ele se torna visível. No entanto, antes que a detecção de alteração do Angular 2 seja concluída, não posso fazer referência a viewContainerRef. Normalmente, envolvo todas as ações necessárias, setTimeout(()=>{},1)como mostrado acima. Existe uma maneira mais correta?

Eu sei que existe uma opção com ngAfterViewChecked, mas causa muitas chamadas inúteis.

RESPOSTA (Plunker)

sinedsem
fonte
3
você tentou usar o atributo [hidden] em vez de * ngIf? Funcionou para mim em uma situação semelhante.
Shardul 27/01

Respostas:

335

Use um setter para o ViewChild:

 private contentPlaceholder: ElementRef;

 @ViewChild('contentPlaceholder') set content(content: ElementRef) {
    if(content) { // initially setter gets called with undefined
        this.contentPlaceholder = content;
    }
 }

O setter é chamado com uma referência de elemento uma vez que *ngIfse torna true.

Observe que, no Angular 8, você precisa definir { static: false }, que é uma configuração padrão em outras versões do Agnular:

 @ViewChild('contentPlaceholder', { static: false })

Nota: se contentPlaceholder for um componente, você poderá alterar ElementRef para sua classe de componente:

  private contentPlaceholder: MyCustomComponent;
  @ViewChild('contentPlaceholder') set content(content: MyCustomComponent) {
     if(content) { // initially setter gets called with undefined
          this.contentPlaceholder = content;
     }
  }
parlamento
fonte
27
note que este setter é chamado inicialmente com conteúdo indefinido, de modo a verificar para null se fazer algo na setter
Recep
1
Boa resposta, mas não contentPlaceholderé . ElementRefViewContainerRef
developer033
6
Como você chama o levantador?
Leandro Cusack
2
@LeandroCusack é chamado automaticamente quando o Angular encontra <div #contentPlaceholder></div>. Tecnicamente, você pode chamá-lo manualmente como qualquer outro levantador, this.content = someElementRefmas não vejo por que você desejaria fazer isso.
parliament
3
Apenas uma observação útil para quem se deparar com isso agora - você precisa ter o @ViewChild ('myComponent', {static: false}) onde o bit da chave é static: false, que permite receber entradas diferentes.
nospamthanks 27/01
107

Uma alternativa para superar isso é executar o detector de alterações manualmente.

Você injeta primeiro o ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Você o chama depois de atualizar a variável que controla o * ngIf

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }
Jefferson Lima
fonte
1
Obrigado! Eu estava usando a resposta aceita, mas ainda estava causando um erro, porque os filhos ainda estavam indefinidos quando tentei usá-los algum tempo depois onInit(), então adicionei o detectChangesantes de chamar qualquer função filho e foi corrigida. (Eu usei tanto a resposta aceita e esta resposta)
Minyc510
Super útil! Obrigado!
AppDreamer 17/03
55

Angular 8+

Você deve adicionar { static: false }como uma segunda opção para @ViewChild. Isso faz com que os resultados da consulta sejam resolvidos após a execução da detecção de alterações, permitindo que você @ViewChildseja atualizado após a alteração do valor.

Exemplo:

export class AppComponent {
    @ViewChild('contentPlaceholder', { static: false }) contentPlaceholder: ElementRef;

    display = false;

    constructor(private changeDetectorRef: ChangeDetectorRef) {
    }

    show() {
        this.display = true;
        this.changeDetectorRef.detectChanges(); // not required
        console.log(this.contentPlaceholder);
    }
}

Exemplo de Stackblitz: https://stackblitz.com/edit/angular-d8ezsn

Sviatoslav Oleksiv
fonte
3
Obrigado Sviatoslav. Tentei de tudo acima, mas apenas sua solução funcionou.
Peter Drinnan
Isso também funcionou para mim (como os truques dos viewchildren). Este é mais intuitiva e mais fácil para angular 8.
Alex
2
Trabalhou como um encanto :)
Sandeep K Nair
1
Essa deve ser a resposta aceita para a versão mais recente.
Krishna Prashatt 30/09/19
Eu estou usando <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;e this.changeDetector.detectChanges();e ele ainda não funciona.
Paul Strupeikis 6/10/19
21

As respostas acima não funcionaram para mim porque no meu projeto, o ngIf está em um elemento de entrada. Eu precisava de acesso ao atributo nativeElement para focar na entrada quando ngIf for true. Parece não haver atributo nativeElement em ViewContainerRef. Aqui está o que eu fiz (seguindo a documentação do @ViewChild ):

<button (click)='showAsset()'>Add Asset</button>
<div *ngIf='showAssetInput'>
    <input #assetInput />
</div>

...

private assetInputElRef:ElementRef;
@ViewChild('assetInput') set assetInput(elRef: ElementRef) {
    this.assetInputElRef = elRef;
}

...

showAsset() {
    this.showAssetInput = true;
    setTimeout(() => { this.assetInputElRef.nativeElement.focus(); });
}

Eu usei setTimeout antes de focar porque o ViewChild leva um segundo para ser atribuído. Caso contrário, seria indefinido.

zebraco
fonte
2
Um setTimeout () de 0 funcionou para mim. Meu elemento oculto pelo meu ngIf foi vinculado corretamente após um setTimeout, sem a necessidade da função set assetInput () no meio.
quer
Você pode detectar alterações em showAsset () e não precisar usar o tempo limite.
WrksOnMyMachine
Como isso é uma resposta? O OP já mencionado usando a setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes
12

Como foi mencionado por outros, a solução mais rápida e rápida é usar [oculto] em vez de * ngSe assim o componente for criado, mas não visível, para que você possa ter acesso a ele, embora possa não ser o mais eficiente maneira.

user3728728
fonte
1
você deve observar que o uso de "[oculto]" pode não funcionar se o elemento não for "display: block". melhor uso [style.display] = "condition? '': 'none'"
Félix Brunet
10

Isso pode funcionar, mas não sei se é conveniente para o seu caso:

@ViewChildren('contentPlaceholder', {read: ViewContainerRef}) viewContainerRefs: QueryList;

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}
Günter Zöchbauer
fonte
1
Você pode tentar, em ngAfterViewInit()vez de ngOnInit(). Presumi que viewContainerRefsjá foi inicializado, mas ainda não contém itens. Parece que me lembrei disso errado.
Günter Zöchbauer
Desculpa eu estava errado. AfterViewInitrealmente funciona. Eu removi todos os meus comentários para não confundir as pessoas. Aqui está um Plunker em funcionamento: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem
1
Esta é realmente uma boa resposta. Funciona e estou usando isso agora. Obrigado!
Konstantin
1
Isso funcionou para mim após a atualização do angular 7 para 8. Por algum motivo, a atualização fez com que o componente fosse indefinido no afterViewInit, mesmo com o uso de static: false de acordo com a nova sintaxe do ViewChild quando o componente foi envolvido em um ngIf. Observe também que o QueryList agora exige um tipo como este QueryList <YourComponentType>;
Alex
Pode ser a alteração relacionada ao constparâmetro deViewChild
Günter Zöchbauer
9

Outro "truque" rápido (solução fácil) é apenas usar a tag [hidden] em vez de * ngIf, é importante saber que nesse caso o Angular cria o objeto e o pinta na classe: oculto é por isso que o ViewChild funciona sem problemas . Portanto, é importante ter em mente que você não deve ocultar itens pesados ​​ou caros que possam causar problemas de desempenho

  <div class="addTable" [hidden]="CONDITION">
Ou Yaacov
fonte
Se isso escondido está dentro de outra se então precisa mudar muitas coisas
Vikas Kohli
6

Meu objetivo era evitar métodos hacky que assumissem algo (por exemplo, setTimeout) e acabei implementando a solução aceita com um pouco de sabor RxJS no topo:

  private ngUnsubscribe = new Subject();
  private tabSetInitialized = new Subject();
  public tabSet: TabsetComponent;
  @ViewChild('tabSet') set setTabSet(tabset: TabsetComponent) {
    if (!!tabSet) {
      this.tabSet = tabSet;
      this.tabSetInitialized.next();
    }
  }

  ngOnInit() {
    combineLatest(
      this.route.queryParams,
      this.tabSetInitialized
    ).pipe(
      takeUntil(this.ngUnsubscribe)
    ).subscribe(([queryParams, isTabSetInitialized]) => {
      let tab = [undefined, 'translate', 'versions'].indexOf(queryParams['view']);
      this.tabSet.tabs[tab > -1 ? tab : 0].active = true;
    });
  }

Meu cenário: eu queria disparar uma ação em um @ViewChildelemento dependendo do roteadorqueryParams . Como um agrupamento *ngIfé falso até que a solicitação HTTP retorne os dados, a inicialização do @ViewChildelemento ocorre com um atraso.

Como funciona: combineLatest emite um valor pela primeira vez somente quando cada um dos Observáveis ​​fornecidos emite o primeiro valor desde que o momento combineLatestfoi assinado. Meu Assunto tabSetInitializedemite um valor quando o@ViewChild elemento está sendo definido. Com isso, atraso a execução do código subscribeaté que ele se *ngIftorne positivo e @ViewChildseja inicializado.

Claro que não se esqueça de cancelar a inscrição no ngOnDestroy, eu faço isso usando o ngUnsubscribeAssunto:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
Filip Juncu
fonte
muito obrigado, tive o mesmo problema, com tabSet e ngIf, seu método me salvou muito tempo e dor de cabeça. Cheers m8;)
Exitl0l
3

Uma versão simplificada, tive um problema semelhante ao usar o SDK JS do Google Maps.

Minha solução foi extrair o dive ViewChildpara o seu próprio componente filho, que quando usado no componente pai foi capaz de ser oculto / exibido usando um *ngIf.

Antes

HomePageComponent Modelo

<div *ngIf="showMap">
  <div #map id="map" class="map-container"></div>
</div>

HomePageComponent Componente

@ViewChild('map') public mapElement: ElementRef; 

public ionViewDidLoad() {
    this.loadMap();
});

private loadMap() {

  const latLng = new google.maps.LatLng(-1234, 4567);
  const mapOptions = {
    center: latLng,
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  };
   this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
}

public toggleMap() {
  this.showMap = !this.showMap;
 }

Depois de

MapComponent Modelo

 <div>
  <div #map id="map" class="map-container"></div>
</div>

MapComponent Componente

@ViewChild('map') public mapElement: ElementRef; 

public ngOnInit() {
    this.loadMap();
});

private loadMap() {

  const latLng = new google.maps.LatLng(-1234, 4567);
  const mapOptions = {
    center: latLng,
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  };
   this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
}

HomePageComponent Modelo

<map *ngIf="showMap"></map>

HomePageComponent Componente

public toggleMap() {
  this.showMap = !this.showMap;
 }
Eugene
fonte
1

No meu caso, eu precisava carregar um módulo inteiro apenas quando a div existia no modelo, significando que a saída estava dentro de um ngif. Dessa maneira, sempre que o angular detectou o elemento #geolocalisationOutlet, ele criou o componente dentro dele. O módulo carrega apenas uma vez também.

constructor(
    public wlService: WhitelabelService,
    public lmService: LeftMenuService,
    private loader: NgModuleFactoryLoader,
    private injector: Injector
) {
}

@ViewChild('geolocalisationOutlet', {read: ViewContainerRef}) set geolocalisation(geolocalisationOutlet: ViewContainerRef) {
    const path = 'src/app/components/engine/sections/geolocalisation/geolocalisation.module#GeolocalisationModule';
    this.loader.load(path).then((moduleFactory: NgModuleFactory<any>) => {
        const moduleRef = moduleFactory.create(this.injector);
        const compFactory = moduleRef.componentFactoryResolver
            .resolveComponentFactory(GeolocalisationComponent);
        if (geolocalisationOutlet && geolocalisationOutlet.length === 0) {
            geolocalisationOutlet.createComponent(compFactory);
        }
    });
}

<div *ngIf="section === 'geolocalisation'" id="geolocalisation">
     <div #geolocalisationOutlet></div>
</div>
Gabb1995
fonte
1

Eu acho que o uso do adiamento da lodash faz muito sentido, especialmente no meu caso, onde meu tubo @ViewChild()estava dentroasync

Timo
fonte
0

Trabalhando no Angular 8 Não há necessidade de importar o ChangeDector

ngIf permite que você não carregue o elemento e evite adicionar mais estresse ao seu aplicativo. Veja como eu o executei sem o ChangeDetector

elem: ElementRef;

@ViewChild('elemOnHTML', {static: false}) set elemOnHTML(elemOnHTML: ElementRef) {
    if (!!elemOnHTML) {
      this.elem = elemOnHTML;
    }
}

Então, quando eu altero meu valor ngIf para ser verdade, eu usaria setTimeout como este para aguardar apenas o próximo ciclo de alteração:

  this.showElem = true;
  console.log(this.elem); // undefined here
  setTimeout(() => {
    console.log(this.elem); // back here through ViewChild set
    this.elem.do();
  });

Isso também me permitiu evitar o uso de bibliotecas ou importações adicionais.

Manuel BM
fonte
0

para o Angular 8 - uma mistura de verificação nula e @ViewChild static: falsehackers

para um controle de paginação aguardando dados assíncronos

@ViewChild(MatPaginator, { static: false }) set paginator(paginator: MatPaginator) {
  if(!paginator) return;
  paginator.page.pipe(untilDestroyed(this)).subscribe(pageEvent => {
    const updated: TSearchRequest = {
      pageRef: pageEvent.pageIndex,
      pageSize: pageEvent.pageSize
    } as any;
    this.dataGridStateService.alterSearchRequest(updated);
  });
}
jenson-button-event
fonte
0

Funciona para mim se eu usar o ChangeDetectorRef no Angular 9

@ViewChild('search', {static: false})
public searchElementRef: ElementRef;

constructor(private changeDetector: ChangeDetectorRef) {}

//then call this when this.display = true;
show() {
   this.display = true;
   this.changeDetector.detectChanges();
}
Ivan Sim
fonte