326

问题

@ViewChild显示模板中的相应元素后最优雅的获取方式是什么?

下面是一个例子。Plunker 也可用

组件.模板.html:

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

组件.component.ts:

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,但它会导致太多无用的调用。

答案(Plunker)

4

17 回答 17

485

为 ViewChild 使用 setter:

 private contentPlaceholder: ElementRef;

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

*ngIf一旦变为,就会使用元素引用调用 setter true

请注意,对于 Angular 8,您必须确保设置{ static: false },这是其他 Angular 版本中的默认设置:

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

注意:如果 contentPlaceholder 是一个组件,您可以将 ElementRef 更改为您的组件类:

  private contentPlaceholder: MyCustomComponent;

  @ViewChild('contentPlaceholder') set content(content: MyCustomComponent) {
     if(content) { // initially setter gets called with undefined
          this.contentPlaceholder = content;
     }
  }
于 2016-12-12T07:04:52.737 回答
140

克服这个问题的另一种方法是手动运行变化检测器。

您首先注入ChangeDetectorRef

constructor(private changeDetector : ChangeDetectorRef) {}

然后在更新控制 *ngIf 的变量后调用它

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }
于 2017-09-04T20:46:22.520 回答
88

角 8+

您应该将其添加{ static: false }@ViewChild. 这会导致在更改检测运行解析查询结果,从而允许您@ViewChild在值更改后进行更新。

例子:

export class AppComponent {
    @ViewChild('contentPlaceholder', { static: false }) contentPlaceholder: ElementRef;

    display = false;

    constructor(private changeDetectorRef: ChangeDetectorRef) {
    }

    show() {
        this.display = true;

        // Required to access this.contentPlaceholder below,
        // otherwise contentPlaceholder will be undefined
        this.changeDetectorRef.detectChanges();

        console.log(this.contentPlaceholder);
    }
}

Stackblitz 示例:https ://stackblitz.com/edit/angular-d8ezsn

于 2019-08-15T10:45:34.317 回答
21

上面的答案对我不起作用,因为在我的项目中,ngIf 位于输入元素上。当 ngIf 为真时,我需要访问 nativeElement 属性以便专注于输入。ViewContainerRef 上似乎没有 nativeElement 属性。这是我所做的(遵循@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 需要一秒钟才能被分配。否则它将是未定义的。

于 2017-04-25T22:53:31.587 回答
15

正如其他人提到的,最快最快的解决方案是使用 [hidden] 而不是 *ngIf。采用这种方法,组件将被创建但不可见,因此您可以访问它。这可能不是最有效的方法。

于 2017-01-29T09:55:57.200 回答
13

这可能有效,但我不知道它是否适合您的情况:

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

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}
于 2016-09-07T10:21:21.403 回答
10

另一个快速的“技巧”(简单的解决方案)就是使用 [hidden] 标签而不是 *ngIf,重要的是要知道在这种情况下 Angular 构建对象并将其绘制在 class:hidden 这就是 ViewChild 工作没有问题的原因. 所以重要的是要记住,你不应该在可能导致性能问题的沉重或昂贵的物品上使用隐藏

  <div class="addTable" [hidden]="CONDITION">
于 2019-01-12T17:19:01.313 回答
7

我的目标是避免任何假设某些东西的 hacky 方法(例如 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根据 router 对元素触发一个动作queryParams。由于*ngIf在 HTTP 请求返回数据之前包装为假,因此@ViewChild元素的初始化会延迟。

它是如何工作的: combineLatest仅当每个提供的 Observable 发出自combineLatest订阅时刻以来的第一个值时,才第一次发出一个值。设置元素时,我的主题tabSetInitialized会发出一个值。@ViewChild因此,我延迟了代码的执行,subscribe直到*ngIf变为正并且@ViewChild被初始化。

当然不要忘记取消订阅 ngOnDestroy,我使用ngUnsubscribeSubject 来取消订阅:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
于 2018-05-23T18:59:23.130 回答
3

一个简化版本,我在使用 Google Maps JS SDK 时遇到了类似的问题。

div我的解决方案是将and提取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;
 }
于 2018-11-18T13:11:23.350 回答
3

如果我在 Angular 9 中使用 ChangeDetectorRef 它对我有用

@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();
}
于 2020-05-12T04:36:54.853 回答
1

对于Angular 8 - 空值检查和@ViewChild static: false骇客的混合

用于等待异步数据的分页控制

@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);
  });
}
于 2019-12-20T13:55:05.680 回答
1

我认为使用lodash 的 defer很有意义,尤其是在我@ViewChild()async管道内的情况下

于 2019-06-12T16:10:29.620 回答
1

In my case I needed to load a whole module only when the div existed in the template, meaning the outlet was inside an ngif. This way everytime angular detected the element #geolocalisationOutlet it created the component inside of it. The module only loads once as well.

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>
于 2019-03-12T13:27:58.243 回答
0

我自己也有同样的问题,使用 Angular 10。

如果我尝试使用[hidden]or *ngIf,那么@ViewChild变量总是undefined

<p-calendar #calendar *ngIf="bShowCalendar" >
</p-calendar>

我通过不将其从网页中删除来修复它。
我用 an[ngClass]使控件具有opacity:0,并将其完全移开。

<style>
  .notVisible {
    opacity: 0;
    left: -1000px;
    position: absolute !important;
  }
</style>

<p-calendar #calendar [ngClass]="{'notVisible': bShowCalendar }" >
</p-calendar>

是的,我知道,它既愚蠢又丑陋,但它解决了问题。

我还必须使控件static。我不明白为什么.. 但是,如果没有这个改变,它又拒绝工作:

export class DatePickerCellRenderer {
    @ViewChild('calendar', {static: true }) calendar: Calendar;
于 2021-01-28T14:11:34.327 回答
0

使用 Angular 8 无需导入 ChangeDector

ngIf 允许您不加载元素并避免给您的应用程序增加更多压力。这是我在没有 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();
  });

这也让我避免使用任何额外的库或导入。

于 2019-09-06T01:54:48.230 回答
-1

只需确保将静态选项设置为 false

  @ViewChild('contentPlaceholder', {static: false}) contentPlaceholder: ElementRef;
于 2020-07-21T11:25:48.943 回答
-1

如果 setter 似乎不起作用(根本没有被调用),请@ViewChild改用 try @ContentChild

于 2021-08-09T22:18:52.783 回答