概要
Angularの<ng-content>
便利ですよね。親コンポーネントから子コンポーネントにhtmlを渡すのによく使います。
今回はそんな<ng-content>
を同じコンポーネント内で複数設置したとき、適切にレンダリングされなかったお話です。
どんなコードで起こり、具体的にどんな問題なのか、そしてそれをどうやって解消したか紹介します。
前提
この記事はAngular 9.1.0を前提としています。今後のバージョンアップ次第で動作が変わる可能性があります。
どんなコードを書いていたか
下記のようなコードです。
<span *ngIf="flg">
<ng-content></ng-content>
</span>
<ng-container *ngIf="!flg">
<ng-content></ng-content>
</ng-container>
何かしらの変数(ここではflg)によって表示するhtmlが異なる、ということをしたかったわけです。今回の場合、flgがtrueの場合は<span>
タグ内に親コンポーネントから渡されたhtmlを表示したい、flgがfalseの場合は親コンポーネントから渡されたhtmlをそのまま表示したいというケースです。そのため、<ng-content>
が2箇所で必要でした。
しかしこれを実際に動かしてみるとflgがtrueの場合は親コンポーネントから渡ってきたhtmlが適切にレンダリングされますが、flgがfalseの場合はレンダリングされないという現象に陥りました。
なぜ適切にレンダリングされないのか
下記のIssueに書いてありましたが、どうやら<ng-content>
はコンポーネントテンプレート内で必ず1つにしなければならないように設計されているようです。
Strange behaviour with multiple and *ngIf · Issue #22972 · angular/angular
ここで言う必ず1つというのは 同じ属性が付与された<ng-content>
が必ず1つである必要があるということです。<ng-content>
はselect属性を設定可能ですが、複数の<ng-content>
で別々のselect属性が設定されていれば異なる<ng-content>
と判別されます。逆にどれもselect属性が同じであったり、どれも全く設定されていない場合は同じ<ng-content>
になるため、上記のような適切にレンダリングされないということが起こります。
じゃあどうすればいいのか
解消策として、同じ属性の<ng-content>
を複数書かないようにすればうまくいきます。具体的には<ng-template>
を使って<ng-content>
の部分を一つに括りだし、それを参照するようにすれば解消できます。
具体的なコードは以下のようになります。
<ng-template #content>
<ng-content></ng-content>
</ng-template>
<span *ngIf="flg">
<ng-container *ngTemplateOutlet="content"></ng-container>
</span>
<ng-container *ngIf="!flg">
<ng-container *ngTemplateOutlet="content"></ng-container>
</ng-container>
このようにすれば同じ属性の<ng-content>
が複数個存在することがなくなるので、flgがtrueの場合もfalseの場合も適切にレンダリングされます。
最後に
ということで同じコンポーネント内の同じ属性の<ng-content>
の複数設置回避方法を紹介しました。ただ先程のIssueを見ていると、この件についてもう少し柔軟にできるよう設計を変更するようなPRが2018/05時点ですでにいくつかあるようです。ただしIvy(Angular9でデフォルトとなったレンダリングエンジン)が利用可能になってからそれを目指していると記載があります。
2020/05現在、Ivyはデフォルトのレンダリングエンジンとなりましたし、<ng-content>
周りの設計が変わりもう少し柔軟に使えるようになる日も近いかもしれません。