@ViewChild in * ngIf

215

Frage

Was ist der eleganteste Weg, @ViewChildnachdem das entsprechende Element in der Vorlage angezeigt wurde?

Unten ist ein Beispiel. Auch Plunker erhältlich.

Vorlage:

<div id="layout" *ngIf="display">
    <div #contentPlaceholder></div>
</div>

Komponente:

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

Ich habe eine Komponente, deren Inhalt standardmäßig ausgeblendet ist. Wenn jemand die show()Methode aufruft , wird sie sichtbar. Bevor jedoch die Erkennung von Angular 2-Änderungen abgeschlossen ist, kann ich nicht darauf verweisen viewContainerRef. Normalerweise verpacke ich alle erforderlichen Aktionen setTimeout(()=>{},1)wie oben gezeigt. Gibt es einen korrekteren Weg?

Ich weiß, dass es eine Option gibt ngAfterViewChecked, aber sie verursacht zu viele nutzlose Anrufe.

ANTWORT (Plunker)

sinedsem
quelle
3
Haben Sie versucht, das Attribut [hidden] anstelle von * ngIf zu verwenden? In einer ähnlichen Situation hat es bei mir funktioniert.
Shardul

Antworten:

335

Verwenden Sie einen Setter für das ViewChild:

 private contentPlaceholder: ElementRef;

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

Der Setter heißt ein Element mit Bezug einmal *ngIfwird true.

Beachten Sie, dass Sie für Angular 8 sicherstellen müssen, dass dies { static: false }die Standardeinstellung in anderen Agnular-Versionen ist:

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

Hinweis: Wenn contentPlaceholder eine Komponente ist, können Sie ElementRef in Ihre Komponentenklasse ändern:

  private contentPlaceholder: MyCustomComponent;
  @ViewChild('contentPlaceholder') set content(content: MyCustomComponent) {
     if(content) { // initially setter gets called with undefined
          this.contentPlaceholder = content;
     }
  }
Parlament
quelle
27
Beachten Sie, dass dieser Setter anfänglich mit undefiniertem Inhalt aufgerufen wird. Überprüfen Sie daher, ob im Setter etwas passiert
Recep
1
Gute Antwort, aber contentPlaceholderist ElementRefnicht ViewContainerRef.
Entwickler033
6
Wie nennt man den Setter?
Leandro Cusack
2
@LeandroCusack wird automatisch aufgerufen, wenn Angular findet <div #contentPlaceholder></div>. Technisch kann man es wie jeden anderen Setter manuell aufrufen, this.content = someElementRefaber ich verstehe nicht, warum man das machen möchte.
Parlament
3
Nur eine hilfreiche Anmerkung für alle, die jetzt darauf stoßen - Sie benötigen @ViewChild ('myComponent', {static: false}), wobei das Schlüsselbit das static: false ist, wodurch verschiedene Eingaben möglich sind.
Nospamthanks
107

Eine Alternative, um dies zu überwinden, besteht darin, den Änderungsdetektor manuell auszuführen.

Sie injizieren zuerst die ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Anschließend rufen Sie es auf, nachdem Sie die Variable aktualisiert haben, die die * ngIf steuert

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }
Jefferson Lima
quelle
1
Vielen Dank! Ich habe die akzeptierte Antwort verwendet, aber sie hat immer noch einen Fehler verursacht, da die Kinder immer noch undefiniert waren, als ich versuchte, sie irgendwann danach zu verwenden onInit(). Deshalb habe ich die detectChangesFunktion hinzugefügt, bevor ich eine untergeordnete Funktion aufgerufen habe, und sie wurde behoben. (Ich habe sowohl die akzeptierte Antwort als auch diese Antwort verwendet)
Minyc510
Super hilfreich! Vielen Dank!
AppDreamer
55

Winkel 8+

Sie sollten { static: false }als zweite Option für hinzufügen @ViewChild. Dies führt dazu, dass die Abfrageergebnisse aufgelöst werden, nachdem die Änderungserkennung ausgeführt wurde, sodass Sie @ViewChildnach Änderungen des Werts aktualisiert werden können.

Beispiel:

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

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

Sviatoslav Oleksiv
quelle
3
Vielen Dank, Sviatoslav. Versuchte alles oben, aber nur Ihre Lösung funktionierte.
Peter Drinnan
Dies funktionierte auch für mich (ebenso wie der Trick mit den Viewchildren). Dieser ist intuitiver und einfacher für Winkel 8.
Alex
2
Arbeitete wie ein Zauber :)
Sandeep K Nair
1
Dies sollte die akzeptierte Antwort für die neueste Version sein.
Krishna Prashatt
Ich verwende <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;und this.changeDetector.detectChanges();es funktioniert immer noch nicht.
Paul Strupeikis
21

Die obigen Antworten haben bei mir nicht funktioniert, da sich in meinem Projekt die ngIf auf einem Eingabeelement befindet. Ich brauchte Zugriff auf das nativeElement-Attribut, um mich auf die Eingabe zu konzentrieren, wenn ngIf wahr ist. Es scheint kein nativeElement-Attribut in ViewContainerRef zu geben. Folgendes habe ich getan ( gemäß der @ ViewChild-Dokumentation ):

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

Ich habe setTimeout vor dem Fokussieren verwendet, da die Zuweisung des ViewChild eine Sekunde dauert. Sonst wäre es undefiniert.

Zebraco
quelle
2
Ein setTimeout () von 0 hat bei mir funktioniert. Mein von meiner ngIf ausgeblendetes Element wurde nach einem setTimeout korrekt gebunden, ohne dass die Funktion set assetInput () in der Mitte erforderlich war.
Will Shaver
Sie können Änderungen in showAsset () erkennen und müssen das Zeitlimit nicht verwenden.
WrksOnMyMachine
Wie ist das eine Antwort? Das bereits erwähnte OP mit einem setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes
12

Wie bereits von anderen erwähnt, besteht die schnellste und schnellste Lösung darin, [versteckt] anstelle von * ngIf zu verwenden. Auf diese Weise wird die Komponente erstellt, aber nicht sichtbar. Dort können Sie darauf zugreifen, obwohl dies möglicherweise nicht die effizienteste ist Weg.

user3728728
quelle
1
Sie müssen beachten, dass die Verwendung von "[versteckt]" möglicherweise nicht funktioniert, wenn das Element nicht "display: block" ist. Verwenden Sie besser [style.display] = "Bedingung? '': 'keine'"
Félix Brunet
10

Das könnte funktionieren, aber ich weiß nicht, ob es für Ihren Fall geeignet ist:

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

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}
Günter Zöchbauer
quelle
1
Kannst du es bitte versuchen ngAfterViewInit()statt ngOnInit(). Ich habe angenommen, dass dies viewContainerRefsbereits initialisiert ist, aber noch keine Elemente enthält. Scheint, als hätte ich mich falsch daran erinnert.
Günter Zöchbauer
Tut mir leid ich lag falsch. AfterViewInitfunktioniert tatsächlich. Ich habe alle meine Kommentare entfernt, um die Leute nicht zu verwirren. Hier ist ein funktionierender Plunker: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem
1
Dies ist eigentlich eine gute Antwort. Es funktioniert und ich benutze es jetzt. Vielen Dank!
Konstantin
1
Dies funktionierte für mich nach dem Upgrade von Winkel 7 auf 8. Aus irgendeinem Grund führte das Upgrade dazu, dass die Komponente in afterViewInit nicht definiert wurde, selbst wenn static: false gemäß der neuen ViewChild-Syntax verwendet wurde, als die Komponente in eine ngIf eingeschlossen wurde. Beachten Sie auch, dass für die QueryList jetzt ein Typ wie dieser erforderlich ist. QueryList <IhrKomponententyp>;
Alex
Könnte die Änderung in Bezug auf den constParameter vonViewChild
Günter Zöchbauer
9

Ein weiterer schneller "Trick" (einfache Lösung) ist die Verwendung des [hidden] -Tags anstelle von * ngIf. Es ist nur wichtig zu wissen, dass Angular in diesem Fall das Objekt erstellt und unter der Klasse malt: hidden funktioniert die ViewChild daher problemlos . Beachten Sie daher, dass Sie keine versteckten Gegenstände für schwere oder teure Gegenstände verwenden sollten, die Leistungsprobleme verursachen können

  <div class="addTable" [hidden]="CONDITION">
Oder Yaacov
quelle
Wenn das Verborgene in einem anderen
steckt, müssen
6

Mein Ziel war es, alle hackigen Methoden zu vermeiden, die etwas voraussetzen (z. B. setTimeout), und am Ende implementierte ich die akzeptierte Lösung mit etwas RxJS- Flair :

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

Mein Szenario: Ich wollte @ViewChildje nach Router eine Aktion für ein Element auslösen queryParams. Da ein Wrapping *ngIffalsch ist, bis die HTTP-Anforderung die Daten zurückgibt, erfolgt die Initialisierung des @ViewChildElements mit einer Verzögerung.

So funktioniert es: combineLatest Gibt zum ersten Mal einen Wert aus, wenn jedes der bereitgestellten Observables den ersten Wert seit dem combineLatestAbonnieren des Moments ausgibt. Mein Betreff tabSetInitializedgibt einen Wert aus, wenn das @ViewChildElement festgelegt wird. Damit verzögere ich die Ausführung des Codes unter, subscribebis das *ngIfpositiv wird und das @ViewChildinitialisiert wird.

Vergessen Sie natürlich nicht, sich bei ngOnDestroy abzumelden. Ich mache das mit dem ngUnsubscribeBetreff:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
Filip Juncu
quelle
Vielen Dank Ich hatte das gleiche Problem mit tabSet & ngIf, Ihre Methode hat mir viel Zeit und Kopfschmerzen gespart. Cheers m8;)
Exitl0l
3

Als vereinfachte Version hatte ich ein ähnliches Problem wie bei der Verwendung des Google Maps JS SDK.

Meine Lösung bestand darin, das divund ViewChildin eine eigene untergeordnete Komponente zu extrahieren, die bei Verwendung in der übergeordneten Komponente mit einem ausgeblendet / angezeigt werden konnte *ngIf.

Vor

HomePageComponent Vorlage

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

HomePageComponent Komponente

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

Nach dem

MapComponent Vorlage

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

MapComponent Komponente

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

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

HomePageComponent Komponente

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

In meinem Fall musste ich ein ganzes Modul nur laden, wenn das div in der Vorlage vorhanden war, was bedeutet, dass sich der Ausgang in einem ngif befand. Auf diese Weise wurde jedes Mal, wenn der Winkel das Element #geolocalisationOutlet erkannte, die darin enthaltene Komponente erstellt. Das Modul wird ebenfalls nur einmal geladen.

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
quelle
1

Ich denke, die Verwendung von Aufschub von Lodash ist sehr sinnvoll, besonders in meinem Fall, in dem ich mich @ViewChild()in einem asyncRohr befand

Timo
quelle
0

Arbeiten mit Angular 8 ChangeDector muss nicht importiert werden

Mit ngIf können Sie das Element nicht laden und vermeiden, dass Ihre Anwendung stärker belastet wird. Hier ist, wie ich es ohne ChangeDetector zum Laufen gebracht habe

elem: ElementRef;

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

Wenn ich dann meinen ngIf-Wert so ändere, dass er wahr ist, würde ich setTimeout wie folgt verwenden, damit es nur auf den nächsten Änderungszyklus wartet:

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

Dadurch konnte ich auch vermeiden, zusätzliche Bibliotheken oder Importe zu verwenden.

Manuel BM
quelle
0

für Angular 8 - eine Mischung aus Null-Checking und @ViewChild static: falseHackery

für eine Paging-Steuerung, die auf asynchrone Daten wartet

@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
quelle
0

Es funktioniert für mich, wenn ich ChangeDetectorRef in Angular 9 verwende

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