@ViewChild ใน * ngIf


215

คำถาม

วิธีที่ดีที่สุดในการรับ@ViewChildหลังจากองค์ประกอบที่เกี่ยวข้องในเทมเพลตถูกแสดงคืออะไร

ด้านล่างเป็นตัวอย่าง นอกจากนี้ยังมีPlunker

แม่แบบ:

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

ส่วนประกอบคือ:

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

ฉันมีส่วนประกอบที่ซ่อนเนื้อหาตามค่าเริ่มต้น เมื่อใครบางคนเรียกshow()วิธีการมันจะมองเห็นได้ อย่างไรก็ตามก่อนการตรวจจับการเปลี่ยนแปลง Angular 2 เสร็จสมบูรณ์ฉันไม่สามารถอ้างอิงviewContainerRefได้ ฉันมักจะห่อการกระทำที่จำเป็นทั้งหมดไว้ในsetTimeout(()=>{},1)ที่แสดงข้างต้น มีวิธีที่ถูกต้องมากขึ้นหรือไม่?

ฉันรู้ว่ามีตัวเลือกด้วยngAfterViewCheckedแต่มันทำให้เกิดการโทรที่ไร้ประโยชน์มากเกินไป

คำตอบ (พลั่วเกอร์)


3
คุณลองใช้แอตทริบิวต์ [hidden] แทน * ngIf หรือไม่ มันใช้งานได้สำหรับฉันในสถานการณ์ที่คล้ายกัน
Shardul

คำตอบ:


335

ใช้ setter สำหรับ ViewChild:

 private contentPlaceholder: ElementRef;

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

หมาเรียกว่ามีการอ้างอิงองค์ประกอบหนึ่งครั้งจะกลายเป็น*ngIftrue

หมายเหตุสำหรับ Angular 8 คุณต้องแน่ใจว่าได้ตั้งค่า{ static: false }ซึ่งเป็นการตั้งค่าเริ่มต้นในเวอร์ชัน Agnular อื่น ๆ :

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

หมายเหตุ: ถ้า contentPlaceholder เป็นส่วนประกอบคุณสามารถเปลี่ยน ElementRef เป็น Class Component ของคุณ:

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

27
ทราบว่าหมานี้จะเรียกว่าครั้งแรกที่มีเนื้อหาที่ไม่ได้กำหนดไว้เพื่อตรวจสอบ for null ถ้าทำบางสิ่งบางอย่างในหมา
Recep

1
คำตอบที่ดี แต่contentPlaceholderเป็นไม่ได้ElementRef ViewContainerRef
developer033

6
คุณเรียกตัวตั้งค่าอย่างไร
Leandro Cusack

2
@LeandroCusack <div #contentPlaceholder></div>จะได้รับการเรียกโดยอัตโนมัติเมื่อพบเชิงมุม ในทางเทคนิคคุณสามารถเรียกมันด้วยตนเองเหมือน setter อื่น ๆthis.content = someElementRefแต่ฉันไม่เห็นสาเหตุที่คุณต้องการทำเช่นนั้น
รัฐสภา

3
เพียงบันทึกที่เป็นประโยชน์สำหรับทุกคนที่เจอสิ่งนี้ - คุณต้องมี @ViewChild ('myComponent', {static: false}) โดยที่บิตที่สำคัญคือ static: false ซึ่งช่วยให้สามารถรับอินพุตที่แตกต่างกันได้
nospamthanks

107

ทางเลือกอื่นในการเอาชนะสิ่งนี้คือการเรียกใช้ตัวตรวจจับการเปลี่ยนแปลงด้วยตนเอง

คุณฉีดเป็นครั้งแรกChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

จากนั้นคุณเรียกมันว่าหลังจากอัพเดตตัวแปรที่ควบคุม * ngIf

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

1
ขอบคุณ! ฉันใช้คำตอบที่ยอมรับแล้ว แต่มันก็ยังก่อให้เกิดข้อผิดพลาดเพราะเด็กยังไม่ได้กำหนดเมื่อฉันพยายามใช้พวกเขาในบางครั้งหลังจากonInit()นั้นฉันจึงเพิ่มdetectChangesก่อนเรียกฟังก์ชั่นลูก ๆ และแก้ไข (ฉันใช้ทั้งคำตอบที่ได้รับการยอมรับและคำตอบนี้)
Minyc510

สุดยอดประโยชน์! ขอบคุณ!
AppDreamer

55

เชิงมุม 8+

คุณควรเพิ่มเป็นตัวเลือกที่สองสำหรับ{ static: false } @ViewChildสิ่งนี้ทำให้ผลลัพธ์ของแบบสอบถามได้รับการแก้ไขหลังจากการตรวจจับการเปลี่ยนแปลงทำงานซึ่งทำให้คุณ@ViewChildสามารถอัปเดตได้หลังจากการเปลี่ยนแปลงค่า

ตัวอย่าง:

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: https://stackblitz.com/edit/angular-d8ezsn


3
ขอบคุณ Sviatoslav พยายามทุกอย่างด้านบน แต่วิธีการแก้ปัญหาของคุณทำงานเท่านั้น
Peter Drinnan

สิ่งนี้ใช้ได้สำหรับฉันเช่นเดียวกับเคล็ดลับ viewchildren อันนี้ง่ายกว่าและง่ายกว่าสำหรับเชิงมุม 8
อเล็กซ์

2
ทำงานเหมือนจับใจ :)
Sandeep K Nair

1
นี่ควรเป็นคำตอบที่ได้รับการยอมรับสำหรับรุ่นล่าสุด
กฤษณะ Prashatt

ฉันใช้<mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;และthis.changeDetector.detectChanges();และมันยังไม่ทำงาน
Paul Strupeikis

21

คำตอบข้างต้นไม่ได้ผลสำหรับฉันเพราะในโครงการของฉัน ngIf อยู่ในองค์ประกอบอินพุต ฉันต้องการเข้าถึงคุณลักษณะ nativeElement เพื่อมุ่งเน้นอินพุตเมื่อ ngIf เป็นจริง ดูเหมือนว่าจะไม่มีแอตทริบิวต์ nativeElement ใน ViewContainerRef นี่คือสิ่งที่ฉันทำ (ทำตามเอกสาร @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(); });
}

ฉันใช้ setTimeout ก่อนที่จะโฟกัสเนื่องจาก ViewChild ใช้เวลาในการกำหนดหนึ่งวินาที มิฉะนั้นจะไม่ได้กำหนด


2
setTimeout () จาก 0 ใช้ได้สำหรับฉัน องค์ประกอบของฉันถูกซ่อนโดย ngIf ของฉันถูกผูกไว้อย่างถูกต้องหลังจาก setTimeout โดยไม่จำเป็นต้องมีฟังก์ชั่น set assetInput () ตรงกลาง
Will Shaver

คุณสามารถตรวจจับการเปลี่ยนแปลงใน showAsset () และไม่ต้องใช้การหมดเวลา
WrksOnMyMachine

นี่เป็นคำตอบอย่างไร OP ได้กล่าวถึงโดยใช้setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes

12

วิธีที่เร็วและเร็วที่สุดคือการใช้ [ซ่อน] แทน * ng หากวิธีนี้ส่วนประกอบจะถูกสร้างขึ้น แต่ไม่ปรากฏให้เห็นเพื่อให้คุณสามารถเข้าถึงได้แม้ว่ามันอาจจะไม่ได้ประสิทธิภาพที่สุดก็ตาม ทาง


1
คุณต้องทราบว่าการใช้ "[ซ่อน]" อาจไม่ทำงานหากองค์ประกอบนั้นไม่ใช่ "display: block" better use [style.display] = "condition? '': 'none'"
Félix Brunet

10

สิ่งนี้สามารถใช้งานได้ แต่ฉันไม่รู้ว่าสะดวกสำหรับกรณีของคุณหรือไม่:

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

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

1
คุณสามารถโปรดลองแทนngAfterViewInit() ngOnInit()ฉันสันนิษฐานว่าviewContainerRefsเริ่มต้นแล้ว แต่ยังไม่มีรายการ ดูเหมือนว่าฉันจำผิดได้
GünterZöchbauer

ขออภัยฉันผิด AfterViewInitใช้งานได้จริง ฉันได้ลบความคิดเห็นทั้งหมดของฉันเพื่อไม่ให้ผู้อื่นสับสน นี่คือ Plunker ที่ใช้งานได้: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem

1
นี่เป็นคำตอบที่ดีจริงๆ มันใช้งานได้และฉันกำลังใช้งานอยู่ตอนนี้ ขอบคุณ!
Konstantin

1
สิ่งนี้ใช้ได้กับฉันหลังจากอัปเกรดจากเชิงมุม 7 ถึง 8 ด้วยเหตุผลบางอย่างการอัปเกรดทำให้ส่วนประกอบไม่ได้ถูกกำหนดใน afterViewInit แม้ว่าจะใช้ static: false ต่อไวยากรณ์ ViewChild ใหม่เมื่อองค์ประกอบถูกห่อใน ngIf นอกจากนี้โปรดทราบว่า QueryList ต้องการประเภทตอนนี้เช่น QueryList <YourComponentType>;
อเล็กซ์

อาจเป็นการเปลี่ยนแปลงที่เกี่ยวข้องกับconstพารามิเตอร์ของViewChild
GünterZöchbauer

9

"เคล็ดลับ" อีกอย่างรวดเร็ว (การแก้ปัญหาที่ง่าย)เพียงแค่ใช้แท็ก [ซ่อน] แทนที่จะเป็น * ng ถ้าสำคัญที่ต้องรู้ว่าในกรณีนั้น Angular สร้างวัตถุและทาสีภายใต้คลาส: ซ่อนนี่คือสาเหตุที่ ViewChild ทำงานโดยไม่มีปัญหา . ดังนั้นโปรดจำไว้ว่าคุณไม่ควรใช้สิ่งที่ซ่อนอยู่ในของหนัก ๆ หรือของมีราคาแพงซึ่งอาจทำให้เกิดปัญหาด้านประสิทธิภาพ

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

หากที่ซ่อนอยู่นั้นอยู่ในอีกถ้าจำเป็นต้องเปลี่ยนแปลงหลายสิ่ง
VIKAS

6

เป้าหมายของฉันคือการหลีกเลี่ยงวิธีการแฮ็กที่คิดว่าบางสิ่ง (เช่น setTimeout) และฉันก็ลงเอยด้วยการนำวิธีการแก้ปัญหาที่ยอมรับมามาใช้ซึ่งมีรสชาติของRxJSอยู่ด้านบน:

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

สถานการณ์ของฉัน:ผมอยากจะยิงการกระทำในองค์ประกอบขึ้นอยู่กับเราเตอร์@ViewChild queryParamsเนื่องจากการห่อ*ngIfเป็นเท็จจนกระทั่งคำขอ HTTP ส่งคืนข้อมูลการเริ่มต้น@ViewChildองค์ประกอบจะเกิดขึ้นเมื่อมีการล่าช้า

มันทำงานอย่างไร: combineLatestปล่อยค่าเป็นครั้งแรกเฉพาะเมื่อ Observables ที่ให้มาแต่ละตัวปล่อยค่าแรกตั้งแต่วินาทีที่combineLatestสมัครสมาชิก หัวเรื่องของฉันtabSetInitializedส่งเสียงค่าเมื่อ@ViewChildองค์ประกอบถูกตั้งค่า ฉันจะประวิงเวลาการเรียกใช้โค้ดภายใต้subscribeจนกว่าจะถึง*ngIfผลบวกและ@ViewChildจะเริ่มต้นได้

แน่นอนว่าอย่าลืมยกเลิกการสมัครใน ngOnDestroy ฉันทำได้โดยใช้ngUnsubscribeหัวเรื่อง:

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

ขอบคุณมากฉันมีปัญหาเดียวกันกับ tabSet & ng ถ้าวิธีการของคุณช่วยฉันได้มากเวลาและปวดหัว ไชโย m8;)
Exitl0l

3

เวอร์ชันที่เรียบง่ายฉันมีปัญหาคล้ายกันนี้เมื่อใช้ Google Maps JS SDK

วิธีการแก้ปัญหาของฉันคือการแยกdivและViewChildเป็นมันขององค์ประกอบของเด็กเองซึ่งเมื่อนำมาใช้ในองค์ประกอบที่ผู้ปกครองก็สามารถที่จะเป็นที่ซ่อน / *ngIfแสดงการใช้

ก่อน

HomePageComponent แบบ

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

HomePageComponent ตัวแทน

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

หลังจาก

MapComponent แบบ

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

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 แบบ

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

HomePageComponent ตัวแทน

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

1

ในกรณีของฉันฉันจำเป็นต้องโหลดโมดูลทั้งหมดเฉพาะเมื่อ div มีอยู่ในเทมเพลตหมายความว่าเต้าเสียบอยู่ภายใน ngif ด้วยวิธีนี้ทุกครั้งที่มีการตรวจพบองค์ประกอบ #geolocalisationOutlet จะสร้างองค์ประกอบภายในขึ้นมา โมดูลโหลดครั้งเดียวเช่นกัน

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

การทำงานกับ Angular 8 ไม่จำเป็นต้องนำเข้า ChangeDector

n ถ้าไม่อนุญาตให้คุณโหลดองค์ประกอบและหลีกเลี่ยงการเพิ่มความเครียดให้กับแอปพลิเคชันของคุณ นี่คือวิธีที่ฉันทำให้มันทำงานโดยไม่มี ChangeDetector

elem: ElementRef;

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

จากนั้นเมื่อฉันเปลี่ยนค่า ngIf ของฉันเป็นความจริงฉันจะใช้ setTimeout เช่นนี้เพื่อรอเพียงรอบการเปลี่ยนแปลงถัดไป:

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

สิ่งนี้ทำให้ฉันสามารถหลีกเลี่ยงการใช้ไลบรารีหรือการนำเข้าเพิ่มเติม


0

สำหรับAngular 8 - การผสมผสานระหว่างการตรวจสอบโมฆะและ@ViewChild static: falseแฮกเกอร์

สำหรับการควบคุมการเพจที่รอข้อมูล async

@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

มันใช้งานได้สำหรับฉันถ้าฉันใช้ ChangeDetectorRef ใน 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();
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.