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.
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.
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();
}
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.
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.
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
}
})
}
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">
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
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();
}
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 div
et 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;
}
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
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>