@ViewChild dans * ngIf

216

Question

Quelle est la façon la plus élégante d'obtenir @ViewChildaprès que l'élément correspondant dans le modèle a été affiché?

Voici un exemple. Plunker également disponible.

Modèle:

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

Composant:

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 avec son contenu caché par défaut. Quand quelqu'un appelle la show()méthode, elle devient visible. Cependant, avant la fin de la détection du changement Angular 2, je ne peux pas faire référence à viewContainerRef. J'encapsule généralement toutes les actions requises setTimeout(()=>{},1)comme indiqué ci-dessus. Y a-t-il une manière plus correcte?

Je sais qu'il y a une option avec ngAfterViewChecked, mais cela provoque trop d'appels inutiles.

RÉPONSE (Plunker)

sinedsem
la source
3
avez-vous essayé d'utiliser l'attribut [masqué] au lieu de * ngIf? Cela a fonctionné pour moi pour une situation similaire.
Shardul

Réponses:

336

Utilisez un setter pour 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é avec une référence d'élément *ngIfdevient une fois true.

Remarque, pour Angular 8, vous devez vous assurer de définir { static: false }, qui est un paramètre par défaut dans d'autres versions Agnular:

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

Remarque: si contentPlaceholder est un composant, vous pouvez changer ElementRef en votre classe de composant:

  private contentPlaceholder: MyCustomComponent;
  @ViewChild('contentPlaceholder') set content(content: MyCustomComponent) {
     if(content) { // initially setter gets called with undefined
          this.contentPlaceholder = content;
     }
  }
parlement
la source
27
notez que ce setter est appelé initialement avec un contenu indéfini, donc vérifiez si null si vous faites quelque chose dans le setter
Recep
1
Bonne réponse, mais contentPlaceholderest ElementRefpas ViewContainerRef.
developer033
6
Comment appelez-vous le passeur?
Leandro Cusack
2
@LeandroCusack, il est appelé automatiquement lorsque Angular le trouve <div #contentPlaceholder></div>. Techniquement, vous pouvez l'appeler manuellement comme n'importe quel autre setter, this.content = someElementRefmais je ne vois pas pourquoi vous voudriez le faire.
parlement
3
Juste une note utile pour tous ceux qui rencontrent cela maintenant - vous devez avoir @ViewChild ('myComponent', {static: false}) où le bit clé est le static: false, ce qui lui permet de prendre différentes entrées.
nospamthanks
108

Une alternative pour résoudre ce problème consiste à exécuter le détecteur de changement manuellement.

Vous injectez d'abord ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

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

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }
Jefferson Lima
la source
1
Merci! J'utilisais la réponse acceptée mais cela causait toujours une erreur car les enfants n'étaient toujours pas définis lorsque j'ai essayé de les utiliser quelque temps après onInit(), j'ai donc ajouté le detectChangesavant d'appeler une fonction enfant et il l'a corrigé. (J'ai utilisé à la fois la réponse acceptée et cette réponse)
Minyc510
Super utile! Merci!
AppDreamer
57

Angulaire 8+

Vous devez ajouter { static: false }une deuxième option pour @ViewChild. Cela entraîne la résolution des résultats de la requête après l' exécution de la détection des modifications, ce qui vous permet @ViewChildd'être mis à jour après la modification de la valeur.

Exemple:

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

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

Sviatoslav Oleksiv
la source
3
Merci Sviatoslav. J'ai tout essayé ci-dessus, mais seule votre solution a fonctionné.
Peter Drinnan
Cela a également fonctionné pour moi (tout comme le tour des enfants). Celui-ci est plus intuitif et plus facile pour les angulaires 8.
Alex
2
A fonctionné comme un charme :)
Sandeep K Nair
1
Cela devrait être la réponse acceptée pour la dernière version.
Krishna Prashatt
J'utilise <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;et this.changeDetector.detectChanges();ça ne marche toujours pas.
Paul Strupeikis
21

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 ngIf est vrai. Il ne semble pas y avoir d'attribut nativeElement sur ViewContainerRef. Voici ce que j'ai fait (en suivant la documentation de @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 de me concentrer car le ViewChild prend une seconde pour être affecté. Sinon, ce ne serait pas défini.

zebraco
la source
2
Un setTimeout () de 0 a fonctionné pour moi. Mon élément caché par mon ngIf était correctement lié après un setTimeout, sans avoir besoin de la fonction set assetInput () au milieu.
Will Shaver
Vous pouvez détecter les changements dans showAsset () et ne pas avoir à utiliser le délai d'expiration.
WrksOnMyMachine
Comment est-ce une réponse? L'OP 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?
Juan Mendes
12

Comme cela a été mentionné par d'autres, la solution la plus rapide et la plus rapide consiste à utiliser [caché] au lieu de * ngIf, de cette façon le composant sera créé mais non visible, là pour vous pouvez y avoir accès, bien qu'il ne soit pas le plus efficace façon.

user3728728
la source
1
vous devez noter que l'utilisation de "[caché]" peut ne pas fonctionner si l'élément n'est pas de "display: block". mieux utiliser [style.display] = "condition? '': 'aucun'"
Félix Brunet
10

Cela pourrait fonctionner mais je ne sais pas si cela convient à votre cas:

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

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}
Günter Zöchbauer
la source
1
Pouvez-vous essayer ngAfterViewInit()au lieu de ngOnInit(). J'ai supposé qu'il viewContainerRefsest déjà initialisé mais ne contient pas encore d'éléments. On dirait que je me suis souvenu de ce mal.
Günter Zöchbauer
Pardon, j'avais tort. AfterViewInitfonctionne réellement. J'ai supprimé tous mes commentaires afin de ne pas confondre les gens. Voici un Plunker fonctionnel: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem
1
C'est en fait une bonne réponse. Cela fonctionne et je l'utilise maintenant. Merci!
Konstantin
1
Cela a fonctionné pour moi après la mise à niveau d'angular 7 à 8. Pour une raison quelconque, la mise à niveau a indéfini le composant dans afterViewInit même avec l'utilisation de static: false selon la nouvelle syntaxe ViewChild lorsque le composant a été encapsulé dans un ngIf. Notez également que la QueryList nécessite maintenant un type comme celui-ci QueryList <YourComponentType>;
Alex
Pourrait être le changement lié au constparamètre deViewChild
Günter Zöchbauer
9

Une autre "astuce" rapide (solution facile) consiste simplement à utiliser la balise [hidden] au lieu de * ngIf, juste important de savoir que dans ce cas, Angular construit l'objet et le peint sous classe: caché c'est pourquoi ViewChild fonctionne sans problème . Il est donc important de garder à l'esprit que vous ne devez pas l'utiliser caché sur des articles lourds ou coûteux qui peuvent entraîner des problèmes de performances

  <div class="addTable" [hidden]="CONDITION">
Ou Yaacov
la source
Si ce caché est à l'intérieur dans un autre, alors il faut changer beaucoup de choses
VIKAS KOHLI
6

Mon objectif était d'éviter toute méthode hacky qui suppose quelque chose (par exemple setTimeout) et j'ai fini par implémenter la solution acceptée avec un peu de saveur RxJS en plus:

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

Mon scénario: je voulais déclencher une action sur un @ViewChildélément en fonction du routeur queryParams. Du fait qu'un habillage *ngIfest faux jusqu'à ce que la requête HTTP renvoie les données, l'initialisation de l' @ViewChildélément se produit avec un retard.

Comment ça marche: combineLatest n'émet une valeur pour la première fois que lorsque chacun des observables fournis émet la première valeur depuis que le moment a combineLatestété souscrit. Mon sujet tabSetInitializedémet une valeur lors de la @ViewChilddéfinition de l' élément. Avec cela, je retarde l'exécution du code sous subscribejusqu'à ce que les *ngIfvirages soient positifs et l' @ViewChildinitialisation.

Bien sûr, n'oubliez pas de vous désinscrire sur ngOnDestroy, je le fais en utilisant le ngUnsubscribesujet:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
Filip Juncu
la source
merci beaucoup J'ai eu le même problème, avec tabSet & ngIf, votre méthode m'a fait gagner beaucoup de temps et de maux de tête. Vive m8;)
Exitl0l
3

Une version simplifiée, j'ai eu un problème similaire à cela lors de l'utilisation du SDK Google Maps JS.

Ma solution était d'extraire le divet ViewChilddans son propre composant enfant qui, lorsqu'il était utilisé dans le composant parent, pouvait être masqué / affiché à l'aide d'un *ngIf.

Avant

HomePageComponent Modèle

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

HomePageComponent Composant

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

Après

MapComponent Modèle

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

MapComponent Composant

@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 Modèle

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

HomePageComponent Composant

public toggleMap() {
  this.showMap = !this.showMap;
 }
Eugène
la source
1

Dans mon cas, je n'avais besoin de charger un module entier que lorsque la div existait dans le modèle, ce qui signifie que la sortie était à l'intérieur d'un ngif. De cette façon, chaque fois que angular détectait l'élément #geolocalisationOutlet, il créait le composant à l'intérieur. Le module ne se charge qu'une seule fois également.

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
la source
1

Je pense que l'utilisation du report de lodash a beaucoup de sens, surtout dans mon cas où mon tuyau @ViewChild()était à l'intérieurasync

Timo
la source
0

Travailler sur Angular 8 Pas besoin d'importer ChangeDector

ngIf vous permet de ne pas charger l'élément et d'éviter d'ajouter plus de stress à votre application. Voici comment je l'ai fait fonctionner sans ChangeDetector

elem: ElementRef;

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

Ensuite, lorsque je change ma valeur ngIf pour être vraie, j'utilise setTimeout comme ceci pour qu'il n'attende que le prochain cycle de changement:

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

Cela m'a également permis d'éviter d'utiliser des bibliothèques ou des importations supplémentaires.

Manuel BM
la source
0

pour Angular 8 - un mélange de vérification nulle et@ViewChild static: false piratage

pour un contrôle de pagination en attente de données asynchrones

@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);
  });
}
événement jenson-button
la source
0

Cela fonctionne pour moi si j'utilise ChangeDetectorRef dans 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
la source