【问题标题】:@ViewChild in *ngIf*ngIf 中的@ViewChild
【发布时间】:2017-01-14 23:20:11
【问题描述】:

问题

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

下面是一个例子。还有Plunker 可用。

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(()=&gt;{},1) 中,如上所示。有没有更正确的方法?

我知道ngAfterViewChecked 有一个选项,但它会导致太多无用的调用。

ANSWER (Plunker)

【问题讨论】:

  • 您是否尝试使用 [hidden] 属性而不是 *ngIf?对于类似的情况,它对我有用。

标签: angular angular2-changedetection viewchild


【解决方案1】:

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

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

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

【讨论】:

  • 你可以试试ngAfterViewInit()而不是ngOnInit()。我假设viewContainerRefs 已经初始化但还没有包含项目。好像我记错了。
  • 对不起,我错了。 AfterViewInit 确实有效。为了不混淆人们,我已经删除了所有的 cmets。这是一个工作 Plunker:plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
  • 这实际上是一个很好的答案。它有效,我现在正在使用它。谢谢!
  • 这在从 Angular 7 升级到 8 后对我有用。由于某种原因,升级导致组件在 afterViewInit 中未定义,即使使用 static: false 根据新的 ViewChild 语法包装组件时在 ngIf 中。另请注意,QueryList 现在需要一个类型,例如 QueryList;
  • 可能是ViewChildconst参数的变化
【解决方案2】:

为 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最初是用未定义的内容调用的,所以如果在setter中做某事,请检查null
  • 很好的答案,但contentPlaceholderElementRef 不是ViewContainerRef
  • 二传手怎么称呼?
  • @LeandroCusack 当 Angular 找到 &lt;div #contentPlaceholder&gt;&lt;/div&gt; 时会自动调用它。从技术上讲,您可以像其他任何 setter this.content = someElementRef 一样手动调用它,但我不明白您为什么要这样做。
  • 对于现在遇到此问题的任何人来说只是一个有用的说明 - 您需要有 @ViewChild('myComponent', {static: false}) ,其中关键位是 static: false,它允许它接受不同的输入。
【解决方案3】:

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

【讨论】:

  • 您必须注意,如果元素不是“display: block”,则使用“[hidden]”可能不起作用。更好地使用 [style.display]="condition ? '' : 'none'"
【解决方案4】:

上面的答案对我不起作用,因为在我的项目中,ngIf 位于输入元素上。当 ngIf 为真时,我需要访问 nativeElement 属性以便专注于输入。 ViewContainerRef 上似乎没有 nativeElement 属性。这是我所做的(遵循@ViewChild documentation):

<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 需要一秒钟才能被分配。否则它将是未定义的。

【讨论】:

  • 0 的 setTimeout() 对我有用。我的 ngIf 隐藏的元素在 setTimeout 后被正确绑定,中间不需要 set assetInput() 函数。
  • 您可以检测 showAsset() 中的变化,而不必使用超时。
  • 这个答案怎么样? OP 已经提到使用setTimeout? I usually wrap all required actions into setTimeout(()=&gt;{},1) as shown above. Is there a more correct way?
【解决方案5】:

解决此问题的另一种方法是手动运行更改检测器。

你先注入ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

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

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

【讨论】:

  • 谢谢!我正在使用接受的答案,但它仍然导致错误,因为当我在onInit() 之后的某个时间尝试使用它们时,孩子们仍然未定义,所以我在调用任何子函数之前添加了detectChanges 并修复了它。 (我同时使用了接受的答案和这个答案)
  • 超级有帮助!谢谢!
  • 我还必须运行 CDR,当我需要 ViewChild 时,它没有及时更新。如果您在更新 *ngIf 属性时在同一函数中依赖子项,则可能会发生这种情况。在这种情况下,可能尚未检测到更改,并且 ViewChild 属性可能仍未定义。
  • 任何想法为什么我在尝试调用detectChanges()时可能会收到此错误:错误类型错误:无法读取未定义的属性'detectChanges'
【解决方案6】:

我的目标是避免任何假设某些东西的 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;
    });
  }

我的场景:我想根据路由器queryParams@ViewChild 元素执行操作。由于在 HTTP 请求返回数据之前包装 *ngIf 为 false,因此 @ViewChild 元素的初始化会延迟。

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

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

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

【讨论】:

  • 非常感谢我遇到了同样的问题,使用 tabSet 和 ngIf,您的方法为我节省了很多时间和头痛。干杯 m8 ;)
  • 有同样的问题,你拯救了我的一天。谢谢你:)
【解决方案7】:

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

我的解决方案是将divViewChild提取到它自己的子组件中,当在父组件中使用时,可以使用*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;
 }

【讨论】:

    【解决方案8】:

    另一个快速的“技巧”(简单的解决方案) 只是使用 [hidden] 标签而不是 *ngIf,重要的是要知道在这种情况下 Angular 构建对象并将其绘制在类下:隐藏这就是为什么 ViewChild 可以正常工作的原因。 因此请务必牢记,您不应在可能导致性能问题的沉重或昂贵物品上使用隐藏功能

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

    【讨论】:

    • 如果隐藏在另一个里面,如果需要改变很多东西
    【解决方案9】:

    在我的例子中,只有当模板中存在 div 时,我才需要加载整个模块,这意味着出口位于 ngif 中。这样,每次 angular 检测到元素 #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>
    

    【讨论】:

      【解决方案10】:

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

      【讨论】:

        【解决方案11】:

        Angular 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

        【讨论】:

        • 谢谢斯维亚托斯拉夫。尝试了上述所有方法,但只有您的解决方案有效。
        • 这也对我有用(就像 viewchildren 技巧一样)。这对于 angular 8 来说更直观、更容易。
        • 这应该是最新版本的公认答案。
        • 答案的文本缺少这样一个事实,即您必须致电 detectChanges 这似乎不是您应该做的事情,我宁愿有一个 setter 而不必注入额外的东西我的组件。更不用说上面的两个 cmets 说它不起作用......所以我不同意这应该是公认的答案,这是一个替代方案。
        • 可能是 Angular 8+ 的最佳解决方案,但确实需要 this.changeDetectorRef.detectChanges();
        【解决方案12】:

        使用 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();
          });
        

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

        【讨论】:

          【解决方案13】:

          对于 Angular 8 - 空检查和 @ViewChild static: falsehackery 的混合体

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

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

          【讨论】:

            【解决方案14】:

            如果我在 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();
            }
            

            【讨论】:

              【解决方案15】:

              确保 static 选项设置为 false

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

              【讨论】:

                【解决方案16】:

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

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

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

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

                <style>
                  .notVisible {
                    opacity: 0;
                    left: -1000px;
                    position: absolute !important;
                  }
                </style>
                
                <p-calendar #calendar [ngClass]="{'notVisible': bShowCalendar }" >
                </p-calendar>
                

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

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

                export class DatePickerCellRenderer {
                    @ViewChild('calendar', {static: true }) calendar: Calendar;
                

                【讨论】:

                  【解决方案17】:

                  如果 setter 似乎无法与 @ViewChild 一起工作(根本没有被调用),请尝试使用 @ContentChild

                  【讨论】:

                  • @ContentChild 完全用于其他用途。
                  猜你喜欢
                  • 2018-02-20
                  • 1970-01-01
                  • 1970-01-01
                  • 2020-05-04
                  • 1970-01-01
                  • 1970-01-01
                  • 2020-12-18
                  • 2020-11-25
                  • 2021-03-11
                  相关资源
                  最近更新 更多