* ngIf中的@ViewChild


214

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

下面是一个例子。还提供柱塞

模板:

<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()method时,它变得可见。但是,在Angular 2更改检测完成之前,我无法引用viewContainerRef。我通常将所有必需的动作包装成setTimeout(()=>{},1)上述所示。有没有更正确的方法?

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

答案(柱塞)


3
您是否尝试使用[hidden]属性而不是* ngIf?在类似情况下,它对我也有用。
Shardul

Answers:


335

将设置器用于ViewChild:

 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 },这是其他Agnular版本中的默认设置:

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

27
请注意,此设置程序最初是使用未定义的内容调用的,因此,如果在设置程序中执行了某些操作,请检查是否为null
Recep

1
回答得好,不过contentPlaceholderElementRef不是ViewContainerRef
developer033

6
您如何称呼二传手?
Leandro Cusack,

2
@LeandroCusack,当Angular找到时会自动调用<div #contentPlaceholder></div>。从技术上讲,您可以像其他二传手一样手动调用它,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
谢谢斯维亚托斯拉夫。尝试了以上所有内容,但只有您的解决方案有效。
彼得·德林南

这也对我有用(视童戏法也是如此)。这对于角度8来说更直观,更容易
Alex

2
像魅力一样工作:)
Sandeep K Nair19

1
这应该是最新版本的可接受答案。
克里希纳·普拉沙特

我使用的<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为true时专注于输入。在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需要花费一秒钟的时间。否则它将是不确定的。


2
setTimeout()为0对我有用。我的ngIf隐藏的元素在setTimeout之后正确绑定,不需要中间的set assetInput()函数。
Will Shaver

您可以在showAsset()中检测到Changes,而不必使用超时。
WrksOnMyMachine

这个答案如何?OP已经提到使用setTimeoutI usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes

12

就像其他人提到的那样,最快,最快的解决方案是使用[hidden]而不是* ngIf,这样可以创建组件但不可见,尽管您可能不是最有效的组件,但您可以在那里访问它方式。


1
您必须注意,如果元素不是“ display:block”,则使用“ [hidden]”可能不起作用。更好地利用[的style.display] = “条件'': '无'?”
费利克斯·布吕

10

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

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

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

1
您能ngAfterViewInit()代替尝试吗ngOnInit()?我以为viewContainerRefs已经初始化但还不包含任何项目。似乎我记得这个错误。
君特Zöchbauer

对不起我错了。AfterViewInit实际有效。我删除了所有评论,以免引起人们的混淆。这是一个工作正常的柱塞:plnkr.co/edit/myu7qXonmpA2hxxU3SLB?
sinedsem

1
这实际上是一个很好的答案。它有效,我现在正在使用它。谢谢!
康斯坦丁

1
从7号角升级到8号后,这对我有用。由于某种原因,即使使用static:组件包裹在ngIf中时,使用新的ViewChild语法,升级也导致组件在afterViewInit中未定义。还要注意,QueryList现在需要一个类似QueryList <YourComponentType>的类型。
亚历克斯(Alex)

可能是相关的变化const的参数ViewChild
冈特Zöchbauer

9

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

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

如果那个隐藏的东西在另一个里面,那么就需要改变很多东西
VIKAS KOHLI 19/12/19

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仅当combineLatest订阅的那一刻起提供的每个Observable都发出第一个值时,才第一次发出值。设置元素tabSetInitialized时,我的主题会发出一个值@ViewChild。因此,我将代码的执行延迟subscribe到正*ngIf转并@ViewChild初始化之前。

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

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

非常感谢我在使用tabSet和ngIf时遇到了同样的问题,您的方法为我节省了很多时间和头痛。干杯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内。这样,每次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>


0

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

这也使我避免使用任何其他库或导入。


0

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

0

如果我在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();
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.