@ViewChild w * ngIf


215

Pytanie

Jaki jest najbardziej elegancki sposób uzyskania @ViewChildpo pokazaniu odpowiedniego elementu w szablonie?

Poniżej znajduje się przykład. Dostępny również Plunker .

Szablon:

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

Składnik:

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

Mam komponent z domyślnie ukrytą zawartością. Kiedy ktoś wywoła show()metodę, staje się widoczna. Jednak zanim zakończy się wykrywanie zmian w Angular 2, nie mogę się do niego odwoływać viewContainerRef. Zwykle zawijam wszystkie wymagane działania, setTimeout(()=>{},1)jak pokazano powyżej. Czy jest bardziej poprawny sposób?

Wiem, że istnieje opcja ngAfterViewChecked, ale powoduje zbyt wiele bezużytecznych połączeń.

ODPOWIEDŹ (Plunker)


3
próbowałeś użyć atrybutu [ukryty] zamiast * ngIf? To działało dla mnie w podobnej sytuacji.
Shardul

Odpowiedzi:


335

Użyj narzędzia do ustawiania ViewChild:

 private contentPlaceholder: ElementRef;

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

Seter jest wywoływany z referencją elementu, gdy *ngIfstaje się true.

Uwaga: w przypadku Angulara 8 musisz się upewnić, że { static: false }jest to ustawienie domyślne w innych wersjach Agnulara:

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

Uwaga: jeśli contentPlaceholder jest składnikiem, możesz zmienić ElementRef na klasę składnika:

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

27
pamiętać, że seter nazywa się początkowo z nieokreślonej zawartości, więc sprawdzić wartość null, jeśli robi coś w seter
Recepa

1
Dobra odpowiedź, ale nie contentPlaceholderjest . ElementRefViewContainerRef
developer033

6
Jak dzwonisz do setera?
Leandro Cusack

2
@LeandroCusack jest wywoływany automatycznie, gdy Angular znajdzie <div #contentPlaceholder></div>. Technicznie możesz to nazwać ręcznie, jak każdy inny setter, this.content = someElementRefale nie rozumiem, dlaczego chcesz to zrobić.
parlament

3
Tylko przydatna uwaga dla każdego, kto teraz się z tym zetknie - musisz mieć @ViewChild („myComponent”, {static: false}), gdzie bitem klucza jest static: false, co pozwala na przyjmowanie różnych danych wejściowych.
nospamthanks

107

Alternatywą do rozwiązania tego problemu jest ręczne uruchomienie detektora zmian.

Najpierw wstrzykujesz ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Następnie wywołujesz go po aktualizacji zmiennej kontrolującej * ngIf

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

1
Dzięki! Używałem zaakceptowanej odpowiedzi, ale nadal powodowała ona błąd, ponieważ dzieci były nadal niezdefiniowane, gdy próbowałem ich użyć jakiś czas później onInit(), więc dodałem detectChangesprzed wywołaniem dowolnej funkcji potomnej i naprawiłem ją. (Użyłem zarówno zaakceptowanej odpowiedzi, jak i tej odpowiedzi)
Minyc510,

Super pomocny! Dzięki!
AppDreamer

55

Angular 8+

Powinieneś dodać { static: false }jako drugą opcję dla @ViewChild. Powoduje to, że wyniki zapytania zostaną rozwiązane po uruchomieniu wykrywania zmian, umożliwiając @ViewChildaktualizację po zmianie wartości.

Przykład:

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

Przykład Stackblitz: https://stackblitz.com/edit/angular-d8ezsn


3
Dziękuję Światosławiu. Próbowałem wszystkiego powyżej, ale działało tylko twoje rozwiązanie.
Peter Drinnan,

To również zadziałało dla mnie (podobnie jak sztuczka dla uczniów). Ten jest bardziej intuicyjny i łatwiejszy dla kątownika 8.
Alex

2
Działa jak urok :)
Sandeep K Nair

1
To powinna być zaakceptowana odpowiedź dla najnowszej wersji.
Krishna Prashatt

Używam <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;a this.changeDetector.detectChanges();i nadal nie działa.
Paul Strupeikis,

21

Powyższe odpowiedzi nie działały dla mnie, ponieważ w moim projekcie ngIf znajduje się na elemencie wejściowym. Potrzebowałem dostępu do atrybutu nativeElement, aby skupić się na danych wejściowych, gdy ngIf jest prawdą. Wygląda na to, że nie ma atrybutu nativeElement na ViewContainerRef. Oto co zrobiłem (zgodnie z dokumentacją @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(); });
}

Użyłem setTimeout przed ustawieniem ostrości, ponieważ ViewChild zajmuje sekundę. W przeciwnym razie byłoby niezdefiniowane.


2
SetTimeout () 0 działało dla mnie. Mój element ukryty przez mój ngIf został poprawnie powiązany po setTimeout, bez potrzeby korzystania z funkcji set assetInput () pośrodku.
Will Shaver,

Możesz wykryć zmiany w showAsset () i nie musisz używać limitu czasu.
WrksOnMyMachine

Jak to jest odpowiedź? OP wspomniano już przy użyciu setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes

12

Jak wspomniano wcześniej, najszybszym i najszybszym rozwiązaniem jest użycie [ukryty] zamiast * ng Jeśli w ten sposób komponent zostanie utworzony, ale niewidoczny, możesz mieć do niego dostęp, chociaż może nie być to najbardziej wydajny sposób.


1
musisz zauważyć, że użycie „[ukryty]” może nie działać, jeśli element nie jest typu „display: block”. lepsze użycie [style.display] = "condition? '': 'none'"
Félix Brunet

10

Może to działać, ale nie wiem, czy jest to wygodne w twoim przypadku:

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

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

1
Czy możesz spróbować ngAfterViewInit()zamiast ngOnInit(). Zakładam, że viewContainerRefsjest już zainicjowany, ale jeszcze nie zawiera elementów. Wygląda na to, że źle to zapamiętałem.
Günter Zöchbauer

Przepraszam pomyliłam się. AfterViewInitfaktycznie działa. Usunąłem wszystkie moje komentarze, aby nie mylić ludzi. Oto działający Plunker: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem

1
To właściwie dobra odpowiedź. Działa i używam tego teraz. Dzięki!
Konstantin,

1
Działa to dla mnie po aktualizacji z kątowej 7 do 8. Z jakiegoś powodu uaktualnienie spowodowało niezdefiniowanie komponentu w afterViewInit nawet przy użyciu static: false zgodnie z nową składnią ViewChild, gdy komponent został zawinięty w ngIf. Zauważ też, że QueryList wymaga typu takiego jak ten QueryList <TwojaKomponentowa>;
Alex

Może to być zmiana związana z constparametremViewChild
Günter Zöchbauer

9

Kolejną szybką „sztuczką” (łatwym rozwiązaniem) jest po prostu użycie tagu [ukryty] zamiast * ngIf, po prostu ważne jest, aby wiedzieć, że w tym przypadku Angular zbuduje obiekt i pomaluje go pod klasą: ukryty dlatego ViewChild działa bez problemu . Dlatego ważne jest, aby pamiętać, że nie należy używać ukrytych przedmiotów ciężkich lub drogich, które mogą powodować problemy z wydajnością

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

Jeśli to ukryte jest w innym, jeśli trzeba zmienić wiele rzeczy
VIKAS KOHLI,

6

Moim celem było uniknięcie jakichkolwiek podejrzanych metod, które zakładają coś (np. SetTimeout) i ostatecznie wdrożyłem zaakceptowane rozwiązanie z odrobiną smaku RxJS na górze:

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

Mój scenariusz: chciałem uruchomić akcję na @ViewChildelemencie w zależności od routera queryParams. Ponieważ zawijanie *ngIfjest fałszywe, dopóki żądanie HTTP nie zwróci danych, inicjalizacja @ViewChildelementu następuje z opóźnieniem.

Jak to działa: combineLatest emituje wartość po raz pierwszy tylko wtedy, gdy każdy z dostarczonych Obserwatorów emituje pierwszą wartość od momentu combineLatestsubskrypcji. Mój przedmiot tabSetInitializedemituje wartość, gdy @ViewChildelement jest ustawiany. W związku z tym opóźniam wykonanie kodu, subscribedopóki nie *ngIfzmieni się na dodatnią i nie @ViewChildzostanie zainicjowana.

Oczywiście nie zapomnij zrezygnować z subskrypcji ngOnDestroy, robię to za pomocą ngUnsubscribetematu:

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

wielkie dzięki, miałem ten sam problem z tabSet & ngIf, twoja metoda pozwoliła mi zaoszczędzić dużo czasu i bólu głowy. Pozdrawiam m8;)
Wyjdź 10

3

Uproszczona wersja, miałem podobny problem do tego podczas korzystania z Google Maps JS SDK.

Moim rozwiązaniem było wyodrębnienie divi ViewChildumieszczenie go we własnym komponencie podrzędnym, który po zastosowaniu w komponencie nadrzędnym mógł zostać ukryty / wyświetlony przy użyciu *ngIf.

Przed

HomePageComponent Szablon

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

HomePageComponent Składnik

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

Po

MapComponent Szablon

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

MapComponent Składnik

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

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

HomePageComponent Składnik

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

1

W moim przypadku musiałem załadować cały moduł tylko wtedy, gdy div istniał w szablonie, co oznacza, że ​​wylot znajdował się w ngif. W ten sposób za każdym razem, gdy kątowy wykrywał element #geolocalizationOutlet, tworzył w nim komponent. Moduł ładuje się tylko raz.

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

Praca na Angular 8 Nie trzeba importować ChangeDector

ngIf pozwala nie ładować elementu i unikać dodawania dodatkowych obciążeń do aplikacji. Oto jak działam bez ChangeDetectora

elem: ElementRef;

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

Następnie, gdy zmienię wartość ngIf na prawdę, użyję setTimeout w ten sposób, aby poczekał tylko na następny cykl zmian:

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

Pozwoliło mi to również uniknąć korzystania z jakichkolwiek dodatkowych bibliotek lub importów.


0

dla Angular 8 - połączenie sprawdzania zera i @ViewChild static: falsehakowania

dla kontrolki stronicowania oczekującej na dane asynchroniczne

@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

Działa dla mnie, jeśli używam ChangeDetectorRef w 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();
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.