问题

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

下面是一个例子。也可提供活塞。

Component.template.html:

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

Component.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有一个选项,但它会导致太多无用的调用。

答案(设置)


当前回答

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

其他回答

为ViewChild使用setter:

 private contentPlaceholder: ElementRef;

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

一旦*ngIf变成true, setter就会被元素引用调用。

注意,对于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;
     }
  }

如果setter看起来不工作(根本没有被调用)@ViewChild尝试@ContentChild代替。

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

在Angular 8上工作,不需要导入ChangeDector

ngIf允许你不加载元素,避免给你的应用程序增加更多的压力。以下是我如何在没有ChangeDetector的情况下运行它

elem: ElementRef;

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

然后当我将我的ngIf值更改为true时,我将像这样使用setTimeout让它只等待下一个更改周期:

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

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

我的目标是避免任何假定的方法(例如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;
    });
  }

我的场景:我想根据路由器queryParams在@ViewChild元素上触发一个动作。由于在HTTP请求返回数据之前包装*ngIf为false, @ViewChild元素的初始化发生延迟。

它是如何工作的:只有当每个提供的可观察对象在订阅了combineLatest后发出第一个值时,combineLatest才第一次发出一个值。当@ViewChild元素被设置时,我的Subject tabSetInitialized会发出一个值。因此,我延迟了订阅下代码的执行,直到*ngIf变为正数,@ViewChild被初始化。

当然,不要忘记取消订阅ngOnDestroy,我使用ngUnsubscribe主题:

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