web-dev-qa-db-fra.com

@ViewChild dans * ngIf

Question

Quel est le moyen le plus élégant d'obtenir @ViewChild après l'affichage de l'élément correspondant dans le modèle?

Ci-dessous un exemple. Aussi Plunker 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 méthode show(), elle devient visible. Cependant, avant que la détection de Angular 2 ne soit terminée, je ne peux pas faire référence à viewContainerRef. J'emballe habituellement toutes les actions requises dans setTimeout(()=>{},1) comme indiqué ci-dessus. Y a-t-il un moyen plus correct?

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

REPONSE (Plunker)

153
sinedsem

La réponse acceptée à l'aide d'une liste de requêtes ne fonctionnait pas pour moi. Cependant, ce qui fonctionnait utilisait un configurateur pour ViewChild:

 private contentPlaceholder: ElementRef;

 @ViewChild('contentPlaceholder') set content(content: ElementRef) {
    this.contentPlaceholder = content;
 }

Le passeur est appelé une fois * ngIf devient vrai.

255
parliament

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

Vous commencez par injecter la ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Ensuite, vous l’appelez après la mise à jour de la variable qui contrôle le * ngIf

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

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 pour pouvoir me concentrer sur l'entrée lorsque ngIf est true. Il semble n'y avoir aucun attribut nativeElement sur ViewContainerRef. Voici ce que j'ai fait (après 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 de faire la mise au point, car ViewChild met une seconde à être affecté. Sinon, ce serait indéfini.

19
zebraco

Comme cela a été mentionné par d’autres, la solution la plus rapide et la plus rapide consiste à utiliser [hidden] à la place de * ngIf, pour que le composant soit créé mais non visible, car vous pouvez y accéder car il n’est peut-être pas le plus efficace. façon.

11
user3728728

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
   }
 })
}
8
Günter Zöchbauer

n autre "truc" rapide (solution simple) consiste simplement à utiliser la balise [hidden] au lieu de * ngIf, il est simplement important de savoir que, dans ce cas, Angular construit l'objet et le peint. under class: hidden c'est pourquoi ViewChild fonctionne sans problème. Il est donc important de garder à l'esprit que vous ne devez pas utiliser des objets cachés ou lourds qui pourraient causer des problèmes de performances

  <div class="addTable" [hidden]="CONDITION">
6
Or Yaacov

angulaire 8

Vous devez ajouter { static: false } comme deuxième option pour @ViewChild

Exemple:

export class AppComponent  {

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

  display = false;

  constructor(private changeDetectorRef: ChangeDetectorRef) {

  }

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

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

5
Sviatoslav Oleksiv

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

  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 élément @ViewChild en fonction du routeur queryParams. En raison d'un wrapping *ngIf étant faux jusqu'à ce que la requête HTTP renvoie les données, l'initialisation de l'élément @ViewChild s'effectue 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 le moment auquel combineLatest a été abonné. Mon sujet tabSetInitialized émet une valeur lorsque l'élément @ViewChild est en cours de définition. De ce fait, je retarde l'exécution du code sous subscribe jusqu'à ce que le *ngIf devienne positif et que le @ViewChild soit initialisé.

Bien sûr, n'oubliez pas de vous désabonner sur ngOnDestroy, je le fais en utilisant la ngUnsubscribe Subject:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
3
Filip Juncu

Une version simplifiée, j'avais un problème similaire à celui-ci lorsque j'utilisais le SDK JS Google Maps.

Ma solution consistait à extraire les divet ViewChild dans 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;
 }
1
Eugene

Je pense que l'utilisation de différer de lodash a beaucoup de sens, en particulier dans mon cas où ma @ViewChild() était à l'intérieur de async pipe

0
Timo

Dans mon cas, je devais charger un module entier uniquement lorsque la div existait dans le modèle, ce qui signifie que la sortie se trouvait à l'intérieur d'un ngif. Ainsi, chaque fois que angular détectait l'élément #geolocalisationOutlet, il créait le composant à l'intérieur de celui-ci. Le module ne charge qu'une 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>
0
Gabb1995