@ViewChild trong * ngNếu


215

Câu hỏi

Cách thanh lịch nhất để có được @ViewChildsau khi yếu tố tương ứng trong mẫu được hiển thị là gì?

Dưới đây là một ví dụ. Cũng có sẵn Plunker .

Bản mẫu:

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

Thành phần:

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

Tôi có một thành phần với nội dung của nó được ẩn theo mặc định. Khi ai đó gọi show()phương thức, nó sẽ hiển thị. Tuy nhiên, trước khi phát hiện thay đổi Angular 2 hoàn tất, tôi không thể tham khảo viewContainerRef. Tôi thường gói tất cả các hành động cần thiết vào setTimeout(()=>{},1)như hình trên. Có cách nào đúng hơn không?

Tôi biết có một tùy chọn với ngAfterViewChecked, nhưng nó gây ra quá nhiều cuộc gọi vô dụng.

TRẢ LỜI (Plunker)


3
bạn đã thử sử dụng thuộc tính [hidden] thay vì * ng If chưa? Nó làm việc cho tôi cho một tình huống tương tự.
Shardul

Câu trả lời:


335

Sử dụng trình cài đặt cho ViewChild:

 private contentPlaceholder: ElementRef;

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

Setter được gọi với một tham chiếu phần tử một lần *ngIftrở thành true.

Lưu ý, đối với Angular 8, bạn phải đảm bảo đặt { static: false }, đây là cài đặt mặc định trong các phiên bản Agnular khác:

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

Lưu ý: nếu contentPlaceholder là một thành phần, bạn có thể thay đổi ElementRef thành Lớp thành phần của mình:

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

27
lưu ý rằng trình thiết lập này được gọi ban đầu với nội dung không xác định, vì vậy hãy kiểm tra null nếu thực hiện một thao tác nào đó trong trình thiết lập
Recep

1
Tốt câu trả lời, nhưng contentPlaceholderElementRefkhông ViewContainerRef.
developer033

6
Làm thế nào để bạn gọi setter?
Leandro Cusack

2
@LeandroCusack nó được gọi tự động khi Angular tìm thấy <div #contentPlaceholder></div>. Về mặt kỹ thuật, bạn có thể gọi nó bằng tay như bất kỳ setter nào khác this.content = someElementRefnhưng tôi không hiểu tại sao bạn muốn làm điều đó.
Quốc hội

3
Chỉ là một lưu ý hữu ích cho bất kỳ ai gặp phải điều này ngay bây giờ - bạn cần phải có @ViewChild ('myComponent', {static: false}) trong đó bit key là static: false, cho phép nó nhận các đầu vào khác nhau.
nospamthanks

107

Một cách khác để khắc phục điều này là chạy trình phát hiện thay đổi theo cách thủ công.

Trước tiên, bạn tiêm ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Sau đó, bạn gọi nó sau khi cập nhật biến điều khiển * ng If

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

1
Cảm ơn! Tôi đã sử dụng câu trả lời được chấp nhận nhưng nó vẫn gây ra lỗi vì trẻ em vẫn chưa được xác định khi tôi cố gắng sử dụng chúng sau đó onInit(), vì vậy tôi đã thêm vào detectChangestrước khi gọi bất kỳ chức năng con nào và nó đã sửa nó. (Tôi đã sử dụng cả câu trả lời được chấp nhận và câu trả lời này)
Minyc510

Siêu hữu ích! Cảm ơn!
AppDreamer

55

Góc 8+

Bạn nên thêm { static: false }như một tùy chọn thứ hai cho @ViewChild. Điều này khiến kết quả truy vấn được giải quyết sau khi phát hiện thay đổi chạy, cho phép bạn @ViewChildđược cập nhật sau khi thay đổi giá trị.

Thí dụ:

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

Ví dụ về Stackblitz: https://stackblitz.com/edit/angular-d8ezsn


3
Cảm ơn bạn Sviat Tư. Đã thử tất cả mọi thứ ở trên nhưng chỉ có giải pháp của bạn làm việc.
Peter Drinnan

Điều này cũng làm việc cho tôi (cũng như thủ thuật lừa đảo). Cái này trực quan hơn và dễ dàng hơn cho góc 8.
Alex

2
Làm việc như một bùa mê :)
Sandeep K Nair

1
Đây phải là câu trả lời được chấp nhận cho phiên bản mới nhất.
Krishna Prashatt

Tôi đang sử dụng <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;this.changeDetector.detectChanges();và nó vẫn không hoạt động.
Paul Strupeikis

21

Các câu trả lời ở trên không hoạt động với tôi vì trong dự án của tôi, ng If nằm trên phần tử đầu vào. Tôi cần quyền truy cập vào thuộc tính localEuity để tập trung vào đầu vào khi ng If là đúng. Dường như không có thuộc tính localEuity trên ViewContainerRef. Đây là những gì tôi đã làm (theo tài liệu @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(); });
}

Tôi đã sử dụng setTimeout trước khi lấy nét vì ViewChild mất một giây để được chỉ định. Nếu không nó sẽ không được xác định.


2
Một setTimeout () là 0 làm việc cho tôi. Phần tử của tôi bị ẩn bởi ngNếu của tôi bị ràng buộc chính xác sau một setTimeout, mà không cần phải đặt hàm propertyInput () ở giữa.
Sẽ cạo râu

Bạn có thể phát hiện các thay đổi trong showAsset () và không phải sử dụng thời gian chờ.
WrksOnMyMachine

Làm thế nào đây là một câu trả lời? OP đã được đề cập bằng cách sử dụng setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes

12

Như đã đề cập bởi người khác, giải pháp nhanh nhất và nhanh nhất là sử dụng [ẩn] thay vì * ngNếu, theo cách này, thành phần sẽ được tạo nhưng không hiển thị, bạn có thể truy cập vào nó, mặc dù nó có thể không hiệu quả nhất đường.


1
bạn phải lưu ý rằng việc sử dụng "[hidden]" có thể không hoạt động nếu phần tử không phải là "display: block". sử dụng tốt hơn [style.display] = "condition? '': 'none'"
Félix Brunet

10

Điều này có thể hoạt động nhưng tôi không biết nếu nó thuận tiện cho trường hợp của bạn:

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

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

1
Bạn có thể vui lòng thử ngAfterViewInit()thay vì ngOnInit(). Tôi giả sử rằng viewContainerRefsđã được khởi tạo nhưng chưa chứa các mục. Có vẻ như tôi đã nhớ sai.
Günter Zöchbauer

Xin lỗi, tôi đã sai. AfterViewInitthực sự hoạt động. Tôi đã xóa tất cả các nhận xét của mình để không gây nhầm lẫn cho mọi người. Đây là một Plunker hoạt động: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem

1
Đây thực sự là một câu trả lời tốt. Nó hoạt động và tôi đang sử dụng điều này ngay bây giờ. Cảm ơn!
Konstantin

1
Điều này hoạt động với tôi sau khi nâng cấp từ góc 7 lên 8. Vì một số lý do, việc nâng cấp khiến thành phần không được xác định trong afterViewInit ngay cả khi sử dụng static: false theo cú pháp ViewChild mới khi thành phần được bọc trong ng If. Cũng lưu ý rằng QueryList yêu cầu một loại bây giờ như QueryList này <YourComponentType>;
Alex

Có thể là sự thay đổi liên quan đến consttham số củaViewChild
Günter Zöchbauer

9

Một "mẹo" nhanh (giải pháp dễ dàng) khác là chỉ sử dụng thẻ [hidden] thay vì * ngNếu, điều quan trọng cần biết là trong trường hợp đó, Angular xây dựng đối tượng và vẽ nó dưới lớp: ẩn đây là lý do tại sao ViewChild hoạt động mà không gặp sự cố . Vì vậy, điều quan trọng cần lưu ý là bạn không nên sử dụng ẩn trên các mặt hàng nặng hoặc đắt tiền có thể gây ra vấn đề về hiệu suất

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

Nếu ẩn đó ở bên trong cái khác nếu cần thay đổi nhiều thứ
VIKAS KOHLI

6

Mục tiêu của tôi là tránh mọi phương thức hack mà giả sử một cái gì đó (ví dụ setTimeout) và cuối cùng tôi đã thực hiện giải pháp được chấp nhận với một chút hương vị RxJS trên đầu:

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

Kịch bản của tôi: Tôi muốn thực hiện một hành động trên một @ViewChildphần tử tùy thuộc vào bộ định tuyến queryParams. Do gói *ngIflà sai cho đến khi yêu cầu HTTP trả về dữ liệu, việc khởi tạo @ViewChildphần tử xảy ra với độ trễ.

Cách thức hoạt động: chỉ combineLatest phát ra một giá trị lần đầu tiên khi mỗi Đài quan sát được cung cấp phát ra giá trị đầu tiên kể từ thời điểm combineLatestđược đăng ký. Chủ đề của tôi tabSetInitializedphát ra một giá trị khi @ViewChildphần tử đang được đặt. Do đó, tôi trì hoãn việc thực thi mã subscribecho đến khi các *ngIflượt dương và @ViewChildđược khởi tạo.

Tất nhiên đừng quên hủy đăng ký trên ngOnDestroy, tôi làm điều đó bằng cách sử dụng ngUnsubscribeChủ đề:

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

cảm ơn rất nhiều Tôi đã gặp vấn đề tương tự, với tab Set & ngNếu, phương pháp của bạn đã giúp tôi tiết kiệm rất nhiều thời gian và đau đầu. Chúc mừng m8;)
Thoát khỏi

3

Một phiên bản đơn giản hóa, tôi gặp vấn đề tương tự như vậy khi sử dụng SDK Google Maps.

Giải pháp của tôi là trích xuất divViewChildvào thành phần con của chính nó mà khi được sử dụng trong thành phần cha mẹ có thể được ẩn / hiển thị bằng cách sử dụng một *ngIf.

Trước

HomePageComponent Bản mẫu

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

HomePageComponent Thành phần

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

Sau

MapComponent Bản mẫu

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

MapComponent Thành phần

@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 Bản mẫu

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

HomePageComponent Thành phần

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

1

Trong trường hợp của tôi, tôi chỉ cần tải toàn bộ mô-đun khi div tồn tại trong mẫu, nghĩa là ổ cắm nằm trong một ngif. Bằng cách này, mọi lúc mọi nơi đều phát hiện ra phần tử #geolocalisationOutlet mà nó tạo ra thành phần bên trong nó. Các mô-đun chỉ tải một lần là tốt.

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>

1

Tôi nghĩ rằng việc sử dụng defer từ lodash có rất nhiều ý nghĩa đặc biệt là trong trường hợp của tôi khi tôi @ViewChild()ở trong asyncống


0

Làm việc trên Angular 8 Không cần nhập ChangeDector

ngNếu cho phép bạn không tải phần tử và tránh gây thêm căng thẳng cho ứng dụng của bạn. Đây là cách tôi có nó chạy mà không có ChangeDetector

elem: ElementRef;

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

Sau đó, khi tôi thay đổi giá trị ng If của mình thành trung thực, tôi sẽ sử dụng setTimeout như thế này để chỉ chờ cho chu kỳ thay đổi tiếp theo:

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

Điều này cũng cho phép tôi tránh sử dụng bất kỳ thư viện hoặc nhập khẩu bổ sung.


0

cho Angular 8 - một hỗn hợp kiểm tra null và tin @ViewChild static: falsetặc

để kiểm soát phân trang chờ dữ liệu không đồng bộ

@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

Nó hoạt động với tôi nếu tôi sử dụng ChangeDetectorRef trong 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();
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.