415 votes

@ViewChild dans *ngIf

Question

Quelle est la façon la plus élégante d'obtenir @ViewChild après l'affichage de l'élément correspondant dans le modèle ?

Voici un exemple. Voir aussi Plunker disponible.

Composant.template.html :

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

Component.component.ts :

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

J'ai un composant dont le contenu est masqué par défaut. Lorsque quelqu'un appelle show() il devient visible. Cependant, avant que la détection de changement d'Angular 2 ne soit terminée, je ne peux pas faire référence à viewContainerRef . J'ai l'habitude de regrouper toutes les actions requises dans setTimeout(()=>{},1) comme indiqué ci-dessus. Existe-t-il une méthode plus correcte ?

Je sais qu'il existe une option avec ngAfterViewChecked mais cela entraîne trop d'appels inutiles.

RÉPONSE (Plunker)

6 votes

Avez-vous essayé d'utiliser l'attribut [hidden] au lieu de *ngIf ? Cela a fonctionné pour moi dans une situation similaire.

588voto

parliament Points 1816

Utiliser un setter pour le ViewChild :

 private contentPlaceholder: ElementRef;

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

Le setter est appelé une fois avec une référence d'élément. *ngIf devient true .

Remarque : pour Angular 8, il faut s'assurer que le paramètre { static: false } qui est un paramètre par défaut dans les autres versions d'Angular :

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

Remarque : si ContentPlaceholder est un composant, vous pouvez remplacer ElementRef par la classe de votre composant :

  private contentPlaceholder: MyCustomComponent;

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

35 votes

Notez que ce setter est appelé initialement avec un contenu non défini, donc vérifiez que le contenu est nul si vous faites quelque chose dans le setter.

1 votes

Bonne réponse, mais contentPlaceholder es ElementRef pas ViewContainerRef .

0 votes

J'ai rencontré un problème similaire récemment, il passait toujours undefined même après que *ngIf soit vrai. Cela s'est avéré être causé par le fait de ne pas inclure le composant dans mon NgModule. Par exemple, j'ai fait @ViewChild(MatCheckbox) alors que MatCheckbox n'était pas enregistré dans le NgModule. Je ne suis pas sûr de savoir pourquoi le modèle n'a pas lancé une erreur lorsque j'ai utilisé <mat-checkbox> en premier lieu, mais il aurait probablement explosé avec la compilation AOT de toute façon.

162voto

Une alternative pour surmonter ce problème est d'exécuter le détecteur de changement manuellement.

Vous injectez d'abord le ChangeDetectorRef :

constructor(private changeDetector : ChangeDetectorRef) {}

Puis vous l'appelez après avoir mis à jour la variable qui contrôle le *ngIf

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }

4 votes

Merci. J'utilisais la réponse acceptée mais cela causait toujours une erreur parce que les enfants étaient toujours indéfinis lorsque j'ai essayé de les utiliser quelque temps plus tard. onInit() J'ai donc ajouté le detectChanges avant d'appeler une fonction enfant et cela a réglé le problème. (J'ai utilisé à la fois la réponse acceptée et cette réponse)

0 votes

Super utile ! Merci !

0 votes

J'ai dû lancer le CDR également, le ViewChild n'a pas été mis à jour assez tôt quand j'en ai eu besoin. Cela peut se produire si vous faites appel à l'enfant dans la même fonction que celle où vous mettez à jour la fonction *ngIf propriété. Dans ce cas, il se peut que les modifications n'aient pas encore été détectées et que la propriété ViewChild soit toujours indéfinie.

23voto

zebraco Points 153

Les réponses ci-dessus n'ont pas fonctionné pour moi car dans mon projet, le ngIf est sur un élément d'entrée. J'avais besoin d'accéder à l'attribut nativeElement afin de me concentrer sur l'entrée lorsque le ngIf est vrai. Il semble qu'il n'y ait pas d'attribut nativeElement sur ViewContainerRef. Voici ce que j'ai fait (en suivant Documentation @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(); });
}

J'ai utilisé setTimeout avant la mise au point parce que le ViewChild prend une seconde pour être assigné. Sinon, ce serait indéfini.

2 votes

Un setTimeout() de 0 a fonctionné pour moi. Mon élément caché par mon ngIf a été correctement lié après un setTimeout, sans qu'il soit nécessaire d'utiliser la fonction set assetInput() au milieu.

1 votes

Vous pouvez détecter les changements dans showAsset() et ne pas avoir à utiliser le délai d'attente.

1 votes

En quoi est-ce une réponse ? Le PO a déjà mentionné l'utilisation d'un setTimeout ? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?

21voto

user3728728 Points 331

Comme d'autres l'ont mentionné, la solution la plus rapide est d'utiliser [hidden] au lieu de *ngIf. En adoptant cette approche, le composant sera créé mais non visible, vous y aurez donc accès. Ce n'est peut-être pas la méthode la plus efficace.

3 votes

Il faut noter que l'utilisation de "[hidden]" peut ne pas fonctionner si l'élément n'est pas de type "display : block". il vaut mieux utiliser [style.display]="condition ? '' : 'none'"

14voto

Günter Zöchbauer Points 21340

Cela pourrait fonctionner mais je ne sais pas si c'est adapté à votre cas :

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

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}

1 votes

Pouvez-vous essayer ngAfterViewInit() au lieu de ngOnInit() . J'ai supposé que viewContainerRefs est déjà initialisé mais ne contient pas encore d'éléments. Il semble que je me sois mal souvenu de cela.

0 votes

Désolé, j'avais tort. AfterViewInit fonctionne réellement. J'ai enlevé tous mes commentaires afin de ne pas embrouiller les gens. Voici un Plunker qui fonctionne : plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview

1 votes

C'est en fait une bonne réponse. Elle fonctionne et je l'utilise maintenant. Merci.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X