@ViewChild in * ngIf


215

Domanda

Qual è il modo più elegante per ottenere @ViewChilddopo che è stato mostrato l'elemento corrispondente nel modello?

Di seguito è riportato un esempio. Disponibile anche Plunker .

Modello:

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

Ho un componente con i suoi contenuti nascosti per impostazione predefinita. Quando qualcuno chiama il show()metodo diventa visibile. Tuttavia, prima che il rilevamento delle modifiche Angular 2 venga completato, non posso fare riferimento a viewContainerRef. Di solito avvolgo tutte le azioni richieste setTimeout(()=>{},1)come mostrato sopra. C'è un modo più corretto?

So che c'è un'opzione con ngAfterViewChecked, ma causa troppe chiamate inutili.

RISPOSTA (Plunker)


3
hai provato ad usare l'attributo [nascosto] invece di * ngIf? Ha funzionato per me in una situazione simile.
Shardul,

Risposte:


335

Utilizzare un setter per ViewChild:

 private contentPlaceholder: ElementRef;

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

Il setter viene chiamato con un riferimento elemento una volta *ngIfdiventa true.

Nota, per Angular 8 devi assicurarti di impostare { static: false }, che è un'impostazione predefinita in altre versioni Agnular:

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

Nota: se contentPlaceholder è un componente è possibile modificare ElementRef nella classe del componente:

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

27
si noti che questo setter viene chiamato inizialmente con contenuto non definito, quindi verificare la null se si fa qualcosa nel setter
Recep

1
Buona risposta, ma non lo contentPlaceholderè . ElementRefViewContainerRef
developer033

6
Come si chiama il setter?
Leandro Cusack,

2
@LeandroCusack viene chiamato automaticamente quando trova Angular <div #contentPlaceholder></div>. Tecnicamente puoi chiamarlo manualmente come qualsiasi altro setter, this.content = someElementRefma non vedo perché vorresti farlo.
parlamento

3
Solo una nota utile per chiunque si imbatta ora in questo: devi avere @ViewChild ('myComponent', {statico: falso}) dove il bit della chiave è statico: falso, che gli consente di accettare input diversi.
nospamthanks,

108

Un'alternativa per ovviare a questo è eseguire manualmente il rilevatore di modifiche.

Inietti prima il ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Quindi lo chiami dopo aver aggiornato la variabile che controlla * ngIf

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

1
Grazie! Stavo usando la risposta accettata ma stava ancora causando un errore perché i bambini erano ancora indefiniti quando ho provato a usarli qualche tempo dopo onInit(), quindi ho aggiunto il detectChangesprima di chiamare qualsiasi funzione figlio e l'ho risolto. (Ho usato sia la risposta accettata che questa risposta)
Minyc510

Super utile! Grazie!
AppDreamer il

57

Angolare 8+

Dovresti aggiungere { static: false }una seconda opzione per @ViewChild. Questo fa sì che i risultati della query vengano risolti dopo l' esecuzione del rilevamento delle modifiche, consentendo @ViewChilddi essere aggiornati dopo la modifica del valore.

Esempio:

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

Esempio di Stackblitz: https://stackblitz.com/edit/angular-d8ezsn


3
Grazie Sviatoslav. Ho provato tutto sopra ma solo la tua soluzione ha funzionato.
Peter Drinnan,

Questo ha funzionato anche per me (come ha fatto il trucco dei viewchildren). Questo è più intuitivo e più facile per l'angolare 8.
Alex,

2
Ha funzionato come un fascino :)
Sandeep K Nair,

1
Questa dovrebbe essere la risposta accettata per l'ultima versione.
Krishna Prashatt,

Sto usando <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;ed this.changeDetector.detectChanges();e ancora non funziona.
Paul Strupeikis,

21

Le risposte sopra non hanno funzionato per me perché nel mio progetto, ngIf si trova su un elemento di input. Avevo bisogno di accedere all'attributo nativeElement per concentrarmi sull'input quando ngIf è vero. Sembra che non ci sia alcun attributo nativeElement su ViewContainerRef. Ecco cosa ho fatto (seguendo la documentazione di @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(); });
}

Ho usato setTimeout prima di mettere a fuoco perché ViewChild impiega un secondo per essere assegnato. Altrimenti sarebbe indefinito.


2
Un setTimeout () di 0 ha funzionato per me. Il mio elemento nascosto da my ngIf era correttamente associato dopo un setTimeout, senza la necessità della funzione set assetInput () nel mezzo.
Will Shaver,

È possibile rilevareChanges in showAsset () e non è necessario utilizzare il timeout.
WrksOnMyMachine,

Come è questa una risposta? L'OP già menzionato usando un setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes,

12

Come è stato menzionato da altri, la soluzione più veloce e più veloce è usare [nascosto] invece di * ngIf, in questo modo il componente verrà creato ma non visibile, lì per te puoi accedervi, anche se potrebbe non essere il più efficiente modo.


1
devi notare che l'uso di "[nascosto]" potrebbe non funzionare se l'elemento non è "display: blocco". better use [style.display] = "condition? '': 'none'"
Félix Brunet

10

Questo potrebbe funzionare ma non so se sia conveniente per il tuo caso:

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

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}

1
Puoi provare ngAfterViewInit()invece di ngOnInit(). Presumo che viewContainerRefssia già inizializzato ma non contenga ancora elementi. Sembra che mi sia ricordato questo male.
Günter Zöchbauer,

Ho sbagliato scusa. AfterViewInitfunziona davvero. Ho rimosso tutti i miei commenti per non confondere le persone. Ecco un Plunker funzionante: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem

1
Questa è in realtà una buona risposta. Funziona e lo sto usando ora. Grazie!
Konstantin,

1
Questo ha funzionato per me dopo l'aggiornamento dalla 7 all'8. Per qualche motivo, l'aggiornamento ha causato la definizione del componente in afterViewInit anche con l'utilizzo di static: false per la nuova sintassi ViewChild quando il componente è stato racchiuso in un ngIf. Si noti inoltre che QueryList richiede un tipo come questo QueryList <YourComponentType>;
Alex,

Potrebbe essere il cambiamento relativo al constparametro diViewChild
Günter Zöchbauer,

9

Un altro "trucco" rapido (soluzione semplice) è solo quello di usare il tag [nascosto] invece di * ngIf, solo importante sapere che in quel caso Angular costruisce l'oggetto e lo dipinge in classe: nascosto questo è il motivo per cui ViewChild funziona senza problemi . Quindi è importante tenere presente che non si deve usare nascosto su oggetti pesanti o costosi che possono causare problemi di prestazioni

  <div class="addTable" [hidden]="CONDITION">

Se quello nascosto è dentro in un altro se poi
devi

6

Il mio obiettivo era quello di evitare qualsiasi metodo confuso che presupponesse qualcosa (ad esempio setTimeout) e alla fine ho implementato la soluzione accettata con un po 'di sapore RxJS in cima:

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

Il mio scenario: volevo lanciare un'azione su un @ViewChildelemento a seconda del router queryParams. Poiché un wrapping *ngIfè falso fino a quando la richiesta HTTP non restituisce i dati, l'inizializzazione @ViewChilddell'elemento avviene con un ritardo.

Come funziona: combineLatest emette un valore per la prima volta solo quando ciascuno degli osservabili forniti emette il primo valore da quando è combineLateststato sottoscritto il momento . Il mio soggetto tabSetInitializedemette un valore quando @ViewChildviene impostato l' elemento. Di conseguenza, ritardo l'esecuzione del codice sotto subscribefino a quando *ngIfdiventa positivo e il@ViewChild viene inizializzato.

Ovviamente non dimenticare di annullare l'iscrizione a ngOnDestroy, lo faccio usando l' ngUnsubscribeoggetto:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

grazie mille ho avuto lo stesso problema, con tabSet & ngIf, il tuo metodo mi ha fatto risparmiare un sacco di tempo e mal di testa. Saluti m8;)
Exitl0l

3

Una versione semplificata, ho riscontrato un problema simile a questo durante l'utilizzo dell'SDK JS di Google Maps.

La mia soluzione era estrarre dive ViewChildnel suo componente figlio che, se usato nel componente genitore, poteva essere nascosto / visualizzato usando un *ngIf.

Prima

HomePageComponent Modello

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

Dopo

MapComponent Modello

 <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 Modello

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

HomePageComponent Componente

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

1

Nel mio caso avevo bisogno di caricare un intero modulo solo quando il div esisteva nel modello, il che significa che la presa era all'interno di un ngif. In questo modo ogni volta che Angular ha rilevato l'elemento #geolocalisationOutlet ha creato il componente al suo interno. Anche il modulo si carica una sola volta.

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

Lavorare su Angular 8 Non è necessario importare ChangeDector

ngIf ti consente di non caricare l'elemento ed evitare di aggiungere ulteriore stress alla tua applicazione. Ecco come l'ho fatto funzionare senza ChangeDetector

elem: ElementRef;

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

Quindi, quando modifico il mio valore ngSe fosse sincero, userei setTimeout in questo modo affinché attenda solo il prossimo ciclo di modifica:

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

Ciò mi ha anche permesso di evitare di utilizzare librerie o importazioni aggiuntive.


0

per Angular 8 - una miscela di controllo null e @ViewChild static: falsehacking

per un controllo di paging in attesa di dati asincroni

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

0

Funziona per me se uso ChangeDetectorRef in 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();
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.