angular - onpush - @ViewChild en*ngIf
on push angular (11)
Como se mencionó por otro, la solución más rápida y rápida es usar [oculto] en lugar de * ng Si, de esta forma, el componente se creará pero no será visible, para que pueda tener acceso a él, aunque podría no ser el más eficiente camino.
Pregunta
¿Cuál es la forma más elegante de obtener
@ViewChild
después de
@ViewChild
elemento correspondiente en la plantilla?
A continuación se muestra un ejemplo. También Plunker disponible.
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);
}
}
Tengo un componente con su contenido oculto por defecto.
Cuando alguien llama al método
show()
, se vuelve visible.
Sin embargo, antes de que se complete la detección de cambio de Angular 2, no puedo hacer referencia a
viewContainerRef
.
Usualmente
setTimeout(()=>{},1)
todas las acciones requeridas en
setTimeout(()=>{},1)
como se muestra arriba.
¿Hay una forma más correcta?
Sé que hay una opción con
ngAfterViewChecked
, pero causa demasiadas llamadas inútiles.
RESPUESTA (Plunker)
Creo que usar
diferir de lodash
tiene mucho sentido, especialmente en mi caso donde mi
@ViewChild()
estaba dentro de una tubería
async
En mi caso, necesitaba cargar un módulo completo solo cuando el div existía en la plantilla, lo que significa que la salida estaba dentro de un ngif. De esta manera, cada vez que angular detecta el elemento #geolocalisationOutlet, crea el componente dentro de él. El módulo solo se carga una vez también.
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>
Esto me estaba funcionando. Simplemente aplique algún retraso después de vincular los datos al formulario.
setTimeout(() => { this.changeDetector.detectChanges(); }, 500);
Esto podría funcionar, pero no sé si es conveniente para su caso:
@ViewChildren(''contentPlaceholder'', {read: ViewContainerRef}) viewContainerRefs: QueryList;
ngAfterViewInit() {
this.viewContainerRefs.changes.subscribe(item => {
if(this.viewContainerRefs.toArray().length) {
// shown
}
})
}
La respuesta aceptada usando una Lista de consultas no funcionó para mí. Sin embargo, lo que sí funcionó fue usar un setter para ViewChild:
private contentPlaceholder: ElementRef;
@ViewChild(''contentPlaceholder'') set content(content: ElementRef) {
this.contentPlaceholder = content;
}
El setter se llama una vez * ngIf se convierte en verdadero.
Las respuestas anteriores no me funcionaron porque en mi proyecto, el ngIf está en un elemento de entrada. Necesitaba acceso al atributo nativeElement para centrarme en la entrada cuando ngIf es verdadero. Parece que no hay un atributo nativeElement en ViewContainerRef. Esto es lo que hice (siguiendo la documentación 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(); });
}
Usé setTimeout antes de enfocar porque ViewChild tarda un segundo en asignarse. De lo contrario, sería indefinido.
Mi objetivo era evitar cualquier método hacky que suponga algo (por ejemplo, setTimeout) y terminé implementando la solución aceptada con un poco de sabor RxJS en la parte superior:
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;
});
}
Mi escenario:
quería
@ViewChild
una acción en un elemento
@ViewChild
dependiendo del router
queryParams
.
Debido a una envoltura
*ngIf
es falsa hasta que la solicitud HTTP devuelve los datos, la inicialización del elemento
@ViewChild
ocurre con un retraso.
Cómo funciona:
combineLatest
emite un valor por primera vez solo cuando cada uno de los Observables proporcionados emite el primer valor desde el momento en que se suscribió
combineLatest
.
Mi Asunto
tabSetInitialized
emite un valor cuando se establece el elemento
@ViewChild
.
Con esto, demoro la ejecución del código bajo
subscribe
hasta que
*ngIf
vuelve positivo y el
@ViewChild
se inicializa.
Por supuesto, no olvide darse de baja en ngOnDestroy, lo hago usando el Asunto
ngUnsubscribe
:
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
Una alternativa para superar esto es ejecutar el detector de cambios manualmente.
Primero se inyecta
ChangeDetectorRef
:
constructor(private changeDetector : ChangeDetectorRef) {}
Luego lo llama después de actualizar la variable que controla el * ngIf
show() {
this.display = true;
this.changeDetector.detectChanges();
}
Una versión simplificada, tuve un problema similar al usar el SDK de Google Maps JS.
Mi solución fue extraer el
div
y
ViewChild
en su propio componente hijo que, cuando se usaba en el componente padre, podía
*ngIf
/ mostrarse usando un
*ngIf
.
antes de
HomePageComponent
Template
<div *ngIf="showMap">
<div #map id="map" class="map-container"></div>
</div>
HomePageComponent
Component
@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;
}
Después
Plantilla
MapComponent
<div>
<div #map id="map" class="map-container"></div>
</div>
Componente
MapComponent
@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
Template
<map *ngIf="showMap"></map>
HomePageComponent
Component
public toggleMap() {
this.showMap = !this.showMap;
}
Otro "truco" rápido (solución fácil) es usar la etiqueta [oculta] en lugar de * ngIf, solo es importante saber que en ese caso Angular construye el objeto y lo pinta en clase: oculto es por eso que ViewChild funciona sin problemas . Por lo tanto, es importante tener en cuenta que no debe usar elementos ocultos o pesados que puedan causar problemas de rendimiento
<div class="addTable" [hidden]="CONDITION">