* ngIfの@ViewChild


215

質問

@ViewChildテンプレート内の対応する要素が表示された後に取得する最もエレガントな方法は何ですか?

以下に例を示します。またPlunker利用できます。

テンプレート:

<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()メソッドを呼び出すと、それが表示されます。ただし、Angular 2の変更検出が完了する前に、を参照することはできませんviewContainerRef。私は通常、setTimeout(()=>{},1)上記のようにすべての必要なアクションをラップします。もっと正しい方法はありますか?

にオプションがあることは知っていますがngAfterViewChecked、これにより無駄な呼び出しが多くなりすぎます。

ANSWER(プランカー)


3
* ngIfの代わりに[hidden]属性を使用してみましたか?私にとっても同じような状況でうまくいきました。
Shardul 2017年

回答:


335

ViewChildのセッターを使用します。

 private contentPlaceholder: ElementRef;

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

一度セッターは、要素の参照と呼ばれる*ngIfなり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
良い答えですが、そうでcontentPlaceholderはありElementRefませんViewContainerRef
developer033

6
どのようにしてセッターを呼び出しますか?
Leandro Cusack

2
@LeandroCusackは、Angularが検出し<div #contentPlaceholder></div>たときに自動的に呼び出されます。技術的には他のセッターと同じように手動で呼び出すことができますがthis.content = someElementRef、なぜそれを行いたいのかわかりません。
議会

3
今これに遭遇した人のためのちょうど役立つメモ-あなたは@ViewChild( 'myComponent'、{static:false})を持っている必要があります。
nospamthanks

107

これを克服する別の方法は、変更検出器を手動で実行することです。

最初に注入しChangeDetectorRefます:

constructor(private changeDetector : ChangeDetectorRef) {}

次に、* ngIfを制御する変数を更新した後に呼び出します

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

1
ありがとう!私は受け入れられた答えを使用していましたが、の後onInit()detectChanges子を使用しようとしたときに子がまだ定義されていないため、それでもエラーが発生していました。そのため、子関数を呼び出す前にを追加して修正しました。(私は受け入れられた回答とこの回答の両方を使用しました)
Minyc510

超役立つ!ありがとう!
AppDreamer

55

Angular 8+

{ static: false }2番目のオプションとして追加する必要があります@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
スビアトスラブありがとう。上記のすべてを試してみましたが、ソリューションのみが機能しました。
Peter Drinnan

これも私にとっては機能しました(viewchildrenのトリックと同様)。この1は、角度8.ため、より直感的で簡単です
アレックス・

2
魅力のように働きました:)
Sandeep K Nair

1
これは、最新バージョンの承認済みの回答です。
クリシュナプラシャット

を使用して<mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepperおり@ViewChild('stepper', {static: true}) private stepper: MatStepper;this.changeDetector.detectChanges();、それはまだ動作しません。
Paul Strupeikis

21

私のプロジェクトでは、ngIfがinput要素にあるため、上記の回答はうまくいきませんでした。ngIfがtrueのときに入力に集中するために、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(); });
}

ViewChildの割り当てには1秒かかるので、フォーカスする前にsetTimeoutを使用しました。それ以外の場合は未定義になります。


2
0のsetTimeout()がうまくいきました。ngIfによって非表示にされた私の要素は、setTimeoutの後に正しくバインドされました。中央にset assetInput()関数は必要ありません。
シェーバーは2017

showAsset()で変更を検出でき、タイムアウトを使用する必要はありません。
WrksOnMyMachine

これはどうですか?OPはすでにsetTimeoutI usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
ファンメンデス

12

他の人が言及したように、最速かつ最速の解決策は、* ngIfの代わりに[hidden]を使用することです。これにより、コンポーネントは作成されますが表示されません。これにより、コンポーネントにアクセスできますが、最も効率的ではない場合があります仕方。


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実際に動作します。人を混乱させないために、すべてのコメントを削除しました。これが動作するPlunkerです:plnkr.co/edit/myu7qXonmpA2hxxU3SLB
p=

1
これは実際には良い答えです。それは動作し、私は現在これを使用しています。ありがとう!
コンスタンティン

1
これは、angular 7から8へのアップグレード後に機能しました。何らかの理由で、コンポーネントがngIfでラップされたとき、新しいViewChild構文ごとにstatic:falseを使用しても、アップグレードによりafterViewInitでコンポーネントが未定義になりました。また、QueryListには、このQueryList <YourComponentType>のようなタイプが必要であることに注意してください。
アレックス

関連した変更であるかもしれないconstのパラメータViewChild
ギュンターZöchbauer

9

別の簡単な「トリック」(簡単な解決策)は、* ngIfの代わりに[hidden]タグを使用することです。その場合、Angularがオブジェクトを構築し、class:hiddenの下にペイントすることを知っておくことが重要です。これが、ViewChildが問題なく機能する理由です。 。 したがって、パフォーマンスの問題を引き起こす可能性のある重いアイテムや高価なアイテムでは、hiddenを使用しないでください。

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

その隠されたものが別の中にある場合、多くのことを変更する必要があります
VIKAS KOHLI

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*ngIfHTTPリクエストがデータを返すまでラッピングがfalse @ViewChildであるため、要素の初期化は遅延して発生します。

仕組み: combineLatest提供された各Observableが、モーメントcombineLatestがサブスクライブされてから最初の値を発行する場合にのみ、値を初めて発行します。要素が設定さtabSetInitializedれると、私のサブジェクトが値を出力します@ViewChild。これにより、が正になり、が初期化さsubscribeれるまで、コードの実行を遅らせます。*ngIf@ViewChild

もちろん、ngOnDestroyでサブスクライブを解除することを忘れないでくださいngUnsubscribe

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

おかげで、同じ問題が発生しました。tabSetとngIfを使用すると、このメソッドを使用すると多くの時間と頭痛を節約できました。Cheers m8;)
Exitl0l

3

簡略化されたバージョンで、Google Maps JS SDKを使用すると、これと同様の問題が発生しました。

私の解決策を抽出するためだったdivViewChild親コンポーネントで使用されるそれ自身の子コンポーネントにHIDすることができた/使用して表示*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を検出するたびに、その内部にコンポーネントを作成しました。モジュールも1回だけロードされます。

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値を真実に変更するとき、次の変更サイクルのみを待機するために、このようなsetTimeoutを使用します。

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

これにより、追加のライブラリやインポートの使用を回避することもできました。


0

アンギュラ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();
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.