オプションのコンテンツ投影/注入角度



オプションのコンテンツ投影/注入角度
最近、カスタムコンテンツで置換可能なコンポーネント(カスタムテーブルのヘッダー)の一部を作る必要がありました.何も提供されない場合、実装は「デフォルト」内容を与えることになっていました.単純なテキストは、単純なテキストからスライダー/トグルまでの何かであるかもしれませんでした.
要件は次のように要約できる.
  • それが提供される場合にカスタムコンテンツをレンダリングする
  • デフォルトのコンテンツをレンダリング
  • 使用の間に引き裂かれた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 サンプルリポジトリのフォルダ.「カスタムヘッダー」または「カスタムフッター」を取り除くときdivAppComponent テンプレート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-contentselect 属性(後者は単一の場合のみ省略可能です)ng-content プロジェクトへのリンク).デフォルトのコンテンツはTemplateRef を使うInput プロパティ.
    The SlotRendererComponent 用途ng-content 使用するコンポーネントからプロジェクションされたものをレンダリングするには
    <ng-content *ngIf="isSlotSet$ | async; else defaultSlotContent"></ng-content>
    
    したがって、もともと渡されたカスタムコンテンツは2回投影されます.
  • 最初の外部コンポーネントRenderSlotSlotRendererComponent 例では)
  • 第二にSlotRendererComponent
  • 平坦化された階層はこのように見えます(実際のDOM構造ではありません).
    <!-- 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 .
  • POCの完全なコードを見つけることができますhere .