オプションのコンテンツ投影/注入角度
オプションのコンテンツ投影/注入角度
最近、カスタムコンテンツで置換可能なコンポーネント(カスタムテーブルのヘッダー)の一部を作る必要がありました.何も提供されない場合、実装は「デフォルト」内容を与えることになっていました.単純なテキストは、単純なテキストからスライダー/トグルまでの何かであるかもしれませんでした.
要件は次のように要約できる.
ng-content
or ng-template
問題を解決する.私は、どちらかのオプションを実装しているPOCを作成しました.要件とは対照的に、作成されたPOC 複数の内容(ヘッダーとフッターのような)を置き換えることができます.(もし必要があれば)解決策を将来拡張することができます.次のセクションでは、私が思い付く可能性のある選択肢について説明します.NG内容
これは通常、最初のオプションとして実装し、使用する簡単です.カスタムコンテンツを使用して子供たちとして提供されて
ng-content
. を使ってselect
属性の複数のコンテンツも同様に投影できます.<ng-content select="[slot='header']"></ng-content>
<ng-content select="[slot='footer']"></ng-content>
これは、最初の要件をカバーしています.第2は、使用を実現するのがより難しいですng-content
ひとりぼっち.カスタムまたはデフォルトのコンテンツをレンダリングするかどうかは、何かがng-content
かどうか.私はどのようなビルドイン機能を見つけることができません/コンポーネントまたはテンプレートからその情報を取得し、カスタムソリューションが必要です.つのオプションは、投影される内容に置かれる指令を作成することです
appSlot
下記の例で)<app-render-slot>
<div appSlot slot="header">Custom Header</div>
<div appSlot slot="footer">Custom Footer</div>
</app-render-slot>
コンポーネントは、@ContentChildren
クエリ.プレースホルダに何かが見つかった場合、カスタムコンテンツが使用されます.@Component({
selector: 'app-render-slot',
templateUrl: './component.html',
styleUrls: ['./component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RenderSlotComponent {
@ContentChildren(SlotDirective, { read: ElementRef }) set slotDirectives(
value: QueryList<ElementRef>
) {
this.nativeSlots.next(Array.from(value));
}
private nativeSlots: BehaviorSubject<Array<ElementRef>>;
readonly slotNames$: Observable<SlotNames>;
constructor() {
this.nativeSlots = new BehaviorSubject<Array<ElementRef>>([]);
this.slotNames$ = this.setSlotsByName(this.nativeSlots.asObservable());
}
isSlotSet(slotName: SlotName): Observable<boolean> {
return this.slotNames$.pipe(
map((slotNames) => slotNames.includes(slotName))
);
}
private setSlotsByName(
slots$: Observable<Array<ElementRef>>
): Observable<SlotNames> {
return slots$.pipe(
map((slots) =>
slots.map((slot) => slot.nativeElement.getAttribute('slot'))
)
);
}
}
例については、スロット(「ヘッダー」または「フッター」)の「名前」は、投射された内容のカスタム「スロット」属性のために設定されたものに基づいて抽出される.The ElementRef
探してみるとSlotDirective
そして、@ContentChildren
クエリ.実装の他の部分は、ちょうどElementRef
sのスロット名.…の助けを借りて
isSlotSet
メソッドは、カスタムコンテンツをレンダリングできます(スロットが見つかった場合)またはデフォルトのコンテンツに戻ります.この例のために、コンポーネントのテンプレートは
ng-content
プレースホルダ<ng-content
select="[slot='header']"
*ngIf="isSlotSet('header') | async; else defaultHeader"
></ng-content>
<ng-content
select="[slot='footer']"
*ngIf="isSlotSet('footer') | async; else defaultFooter"
></ng-content>
<ng-template #defaultHeader> Default Header </ng-template>
<ng-template #defaultFooter> Default Footer </ng-template>
ここで説明する代替は、ng-content/render-slot
サンプルリポジトリのフォルダ.「カスタムヘッダー」または「カスタムフッター」を取り除くときdiv
にAppComponent
テンプレートapp-render-slot
デフォルトのフォールバックはレンダリングされます.を使用します.
ヘッドアップ:このソリューションは動作しませんので、興味のない場合には先に進んでください.
上記のアプローチは、オプションの内容投射を伴う各々のコンポーネントがレンダリングされた内容を見つけて/決定するためにメカニズムを実行しなければならないという欠点を有する.
私のアイデアは、“ヘルパー”コンポーネントを作成することによって解決策を改善することでした
SlotRendererComponent
, コンポーネントを使用して渡される内容をレンダリングする責任があります.<app-slot-renderer [defaultSlotContent]="defaultHeader"
><ng-content select="[slot='header']"></ng-content
></app-slot-renderer>
<app-slot-renderer [defaultSlotContent]="defaultFooter"
><ng-content select="[slot='footer']"></ng-content
></app-slot-renderer>
<ng-template #defaultHeader> <div>Default Header</div> </ng-template>
<ng-template #defaultFooter> <div>Default Footer</div> </ng-template>
カスタムコンテンツはng-content
とselect
属性(後者は単一の場合のみ省略可能です)ng-content
プロジェクトへのリンク).デフォルトのコンテンツはTemplateRef
を使うInput
プロパティ.The
SlotRendererComponent
用途ng-content
使用するコンポーネントからプロジェクションされたものをレンダリングするには<ng-content *ngIf="isSlotSet$ | async; else defaultSlotContent"></ng-content>
したがって、もともと渡されたカスタムコンテンツは2回投影されます.RenderSlotSlotRendererComponent
例では)SlotRendererComponent
<!-- From SlotRendererComponent -->
<ng-content *ngIf="isSlotSet$ | async; else defaultSlotContent">
<!-- From RenderSlotSlotRendererComponent -->
<ng-content select="[slot='header']">
<!-- Projected custom content -->
<div appSlot slot="header">Custom Header</div>
</ng-content>
</ng-content>
<!-- Same for the footer -->
第1のアプローチと同じメカニズムによって、習慣またはデフォルト内容は、そばにレンダリングされますSlotRendererComponent
.このソリューションが動作していない理由は
@ContentChildren
ネストされていないng-content
セッティング{ descendants: true }
また、私のために働きませんでした.私はissue 問題を記述するAngularDart
リポジトリのような関係があるかもしれません.NGテンプレート
テンプレートのプロパティ
つのオプション
ng-template
ベースの解決策は、カスタムコンテンツをTemplateRef
s.<app-template-render-props
[templates]="{ 'header': header, 'footer': footer }"
></app-template-render-props>
<ng-template #header><div>Custom Header</div></ng-template>
<ng-template #footer><div>Custom Footer</div></ng-template>
提供TemplateRef
各スロットに対して*ngTemplateOutlet
. と同じng-content
コンポーネントは、何も定義されていない場合はデフォルトの内容に戻るRenderTemplateComponent
例ではヘルパー).<app-render-template
[template]="{ customTemplate: templates.header, defaultTemplate: defaultHeader }"
></app-render-template>
<app-render-template
[template]="{ customTemplate: templates.footer, defaultTemplate: defaultHeader }"
></app-render-template>
<ng-template #defaultHeader> <div>Default Header</div> </ng-template>
<ng-template #defaultFooter> <div>Default Footer</div> </ng-template>
指令で
専用のものを定義する
ng-template
各カスタムコンテンツのラッパーは、使用するコンポーネントのテンプレートを使用し、整理するのに不便です.これは、TemplateRef
スロット名と同様に:@Directive({
selector: '[appTemplateSlot]'
})
export class TemplateSlotDirective {
@Input() appTemplateSlot: SlotName | null = null;
constructor(public template: TemplateRef<unknown>) {}
}
ディレクティブは、入力プロパティとしてスロット名(“ヘッダー”または“フッター”)を受け取り、TemplateRef
人前でtemplate
プロパティunknown
種類TemplateRef
それが既知である/利用可能な場合の関連文脈によって、取り替えられることができた.レンダリングコンポーネントをすぐにクエリをすることができます
TemplateSlotDirective
使用@ContentChildren
を格納し、template
対応するスロットに@Component({
selector: 'app-render-props-directive',
templateUrl: './component.html',
styleUrls: ['./component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RenderPropsDirectiveComponent {
@ContentChildren(TemplateSlotDirective) set templateSlots(
templateSlots: QueryList<TemplateSlotDirective>
) {
this.templateDirectives.next(
templateSlots.length > 0 ? Array.from(templateSlots) : []
);
}
private templateDirectives: ReplaySubject<Array<TemplateSlotDirective>>;
templates$: Observable<Partial<Templates>>;
constructor() {
this.templateDirectives = new ReplaySubject(1);
this.templates$ = this.setupTemplates(
this.templateDirectives.asObservable()
);
}
private setupTemplates(
templateDirectives$: Observable<Array<TemplateSlotDirective>>
): Observable<Partial<Templates>> {
return templateDirectives$.pipe(
map((templateDirectives) =>
templateDirectives.reduce(
(partialTemplateDirectives, templateDirective) =>
templateDirective.appTemplateSlot
? {
...partialTemplateDirectives,
[templateDirective.appTemplateSlot]:
templateDirective.template
}
: partialTemplateDirectives,
{}
)
),
shareReplay({ bufferSize: 1, refCount: true })
);
}
}
通常、レンダリングコンポーネントは、各スロットのカスタムまたはフォールバックコンテンツをレンダリングします.<app-render-template
[template]="{ customTemplate: (templates$ | async)?.header, defaultTemplate: defaultHeader }"
></app-render-template>
<app-render-template
[template]="{ customTemplate: (templates$ | async)?.footer, defaultTemplate: defaultHeader }"
></app-render-template>
<ng-template #defaultHeader> <div>Default Header</div> </ng-template>
<ng-template #defaultFooter> <div>Default Footer</div> </ng-template>
下記の通りng-template
ラッパーは現在、置くことに置き換えられますTemplateSlotDirective
カスタムコンテンツのセレクター<app-render-props-directive>
<div *appTemplateSlot="'header'">Custom Header</div>
<div *appTemplateSlot="'footer'">Custom Footer</div>
</app-render-props-directive>
結論
両方とも
ng-content
だけでなくng-template
カスタムコンテンツを表示するか、デフォルトをレンダリングするために戻る必要があります.私は好む
ng-template
以下のベースの解決方法ng-content
を使用するコンポーネント(特にテンプレート内).ng-content
入れ子を問うことによる問題のためのベース・ソリューションng-content
使用@ContentChildren
. Reference
この問題について(オプションのコンテンツ投影/注入角度), 我々は、より多くの情報をここで見つけました https://dev.to/remshams/optional-content-projection-injection-in-angular-2jnnテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol