@ViewChild en * ngIf

215

Pregunta

¿Cuál es la forma más elegante de obtener @ViewChilddespués de que se muestre el 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 show()método, se vuelve visible. Sin embargo, antes de que se complete la detección de cambios en Angular 2, no puedo hacer referencia a ellos viewContainerRef. Normalmente envuelvo todas las acciones requeridas 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)

sinedsem
fuente
3
¿Intentaste usar el atributo [oculto] en lugar de * ngIf? Me funcionó para una situación similar.
Shardul

Respuestas:

335

Use un setter para ViewChild:

 private contentPlaceholder: ElementRef;

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

El colocador se llama con un elemento de referencia una vez que *ngIfse convierte true.

Tenga en cuenta que para Angular 8 debe asegurarse de configurar { static: false }, que es una configuración predeterminada en otras versiones de Agnular:

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

Nota: si contentPlaceholder es un componente, puede cambiar ElementRef a su componente Clase:

  private contentPlaceholder: MyCustomComponent;
  @ViewChild('contentPlaceholder') set content(content: MyCustomComponent) {
     if(content) { // initially setter gets called with undefined
          this.contentPlaceholder = content;
     }
  }
parlamento
fuente
27
tenga en cuenta que este setter se llama inicialmente con contenido indefinido, así que verifique si hay algo nulo si hace algo en el setter
Recep
1
Buena respuesta, pero no lo contentPlaceholderes . ElementRefViewContainerRef
developer033
66
¿Cómo llamas al setter?
Leandro Cusack el
2
@LeandroCusack se llama automáticamente cuando Angular encuentra <div #contentPlaceholder></div>. Técnicamente, puede llamarlo manualmente como cualquier otro setter, this.content = someElementRefpero no veo por qué querría hacer eso.
Parlamento
3
Solo una nota útil para cualquiera que se encuentre con esto ahora: debe tener @ViewChild ('myComponent', {static: false}) donde el bit de clave es static: false, lo que le permite tomar diferentes entradas.
nospamthanks
108

Una alternativa para superar esto es ejecutar el detector de cambios manualmente.

Primero se inyecta el 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();
    }
Jefferson Lima
fuente
1
¡Gracias! Estaba usando la respuesta aceptada pero todavía estaba causando un error porque los niños aún no estaban definidos cuando intenté usarlos en algún momento después onInit(), así que agregué la función detectChangesantes de llamar a cualquier niño y lo solucionó. (
Usé
Super útil! ¡Gracias!
AppDreamer
57

Angular 8+

Debe agregar { static: false }como segunda opción para @ViewChild. Esto hace que los resultados de la consulta se resuelvan después de que se ejecute la detección de cambios, lo que le permite @ViewChildactualizarse después de que cambie el valor.

Ejemplo:

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

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

Sviatoslav Oleksiv
fuente
3
Gracias Sviatoslav Intenté todo lo anterior pero solo su solución funcionó.
Peter Drinnan
Esto también funcionó para mí (al igual que el truco de viewchildren). Este es más intuitivo y fácil para angular 8.
Alex
2
Funcionó como un encanto :)
Sandeep K Nair
1
Esta debería ser la respuesta aceptada para la última versión.
Krishna Prashatt
Estoy usando <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;y this.changeDetector.detectChanges();todavía no funciona.
Paul Strupeikis
21

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.

zebraco
fuente
2
Un setTimeout () de 0 funcionó para mí. Mi elemento oculto por mi ngIf se unió correctamente después de un setTimeout, sin la necesidad de la función set assetInput () en el medio.
Will Shaver
Puede detectar cambios en showAsset () y no tener que usar el tiempo de espera.
WrksOnMyMachine
¿Cómo es esta una respuesta? El OP ya mencionó el uso de un setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes
12

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 manera, el componente se creará pero no será visible, allí puede tener acceso a él, aunque podría no ser el más eficiente camino.

usuario3728728
fuente
1
debe tener en cuenta que el uso de "[oculto]" puede no funcionar si el elemento no es "display: block". mejor uso [style.display] = "condition? '': 'none'"
Félix Brunet
10

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
   }
 })
}
Günter Zöchbauer
fuente
1
¿Puedes intentarlo en ngAfterViewInit()lugar de ngOnInit()? Supuse que viewContainerRefsya está inicializado pero que aún no contiene elementos. Parece que recordé esto mal.
Günter Zöchbauer
Lo siento estaba equivocado. AfterViewInitEn realidad funciona. He eliminado todos mis comentarios para no confundir a las personas. Aquí hay un Plunker en funcionamiento: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem
1
Esta es realmente una buena respuesta. Funciona y estoy usando esto ahora. ¡Gracias!
Konstantin
1
Esto funcionó para mí después de la actualización de angular 7 a 8. Por alguna razón, la actualización causó que el componente no se definiera en afterViewInit incluso con el uso de static: false según la nueva sintaxis de ViewChild cuando el componente se envolvió en un ngIf. También tenga en cuenta que la QueryList requiere un tipo ahora como este QueryList <YourComponentType>;
Alex
Podría ser el cambio relacionado con el constparámetro deViewChild
Günter Zöchbauer
9

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 en objetos pesados ​​o caros que pueden causar problemas de rendimiento

  <div class="addTable" [hidden]="CONDITION">
O Yaacov
fuente
Si eso está oculto dentro de otro si es necesario cambiar muchas cosas
VIKAS KOHLI
6

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 activar una acción en un @ViewChildelemento dependiendo del enrutador queryParams. Debido a que el ajuste *ngIfes falso hasta que la solicitud HTTP devuelve los datos, la inicialización del @ViewChildelemento 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 que combineLatestse suscribió el momento . Mi Asunto tabSetInitializedemite un valor cuando se establece el @ViewChildelemento. Con esto, retrasé la ejecución del código bajo subscribehasta que las *ngIfvueltas sean positivas y @ViewChildse inicialice.

Por supuesto, no olvide darse de baja en ngOnDestroy, lo hago usando el ngUnsubscribeAsunto:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
Filip Juncu
fuente
muchas gracias. He tenido el mismo problema, con tabSet & ng. Si su método me ahorró mucho tiempo y dolor de cabeza. Cheers m8;)
Salida el
3

Una versión simplificada, tuve un problema similar al usar el SDK de Google Maps JS.

Mi solución fue extraer divy ViewChilden su propio componente hijo que, cuando se usaba en el componente padre, podía ocultarse / mostrarse usando un *ngIf.

antes de

HomePageComponent Modelo

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

HomePageComponent Componente

@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

MapComponent Modelo

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

MapComponent Componente

@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 Modelo

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

HomePageComponent Componente

public toggleMap() {
  this.showMap = !this.showMap;
 }
Eugene
fuente
1

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>
Gabb1995
fuente
1

Creo que usar diferir de lodash tiene mucho sentido, especialmente en mi caso donde @ViewChild()estaba dentro de la asynctubería

Timo
fuente
0

Trabajando en Angular 8 No es necesario importar ChangeDector

ngIf le permite no cargar el elemento y evitar agregar más estrés a su aplicación. Así es como lo hice funcionar sin ChangeDetector

elem: ElementRef;

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

Luego, cuando cambio mi valor ngIf para que sea verdadero, usaría setTimeout como este para que espere solo el próximo ciclo de cambio:

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

Esto también me permitió evitar el uso de bibliotecas o importaciones adicionales.

Manuel BM
fuente
0

para Angular 8 : una mezcla de comprobación nula y @ViewChild static: falsepiratería

para un control de paginación esperando datos asíncronos

@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);
  });
}
jenson-button-event
fuente
0

Funciona para mí si uso ChangeDetectorRef en 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
fuente