詳細ガイド
コンポーネント

コンポーネントのプログラマティックレンダリング

TIP: このガイドは、すでに基本ガイドを読んでいることを前提としています。Angularを初めて使う場合は、まずそちらをお読みください。

テンプレートでコンポーネントを直接使用する以外に、コンポーネントをプログラム的に動的にレンダリングできます。 これは、コンポーネントが最初は不明で(したがってテンプレートで直接参照できない)、何らかの条件に依存する 状況に役立ちます。

プログラム的にコンポーネントをレンダリングする主な方法は2つあります: テンプレートでNgComponentOutletを使用する方法と、 TypeScriptコードでViewContainerRefを使用する方法です。

HELPFUL: 遅延読み込みのユースケース(たとえば、重いコンポーネントの読み込みを遅らせたい場合)については、代わりに組み込みの @defer機能の使用を検討してください。@defer機能を使用すると、@deferブロック内のコンポーネント、ディレクティブ、パイプのコードが 自動的に別のJavaScriptチャンクに抽出され、設定されたトリガーに基づいて必要な場合にのみ 読み込まれます。

NgComponentOutletの使用

NgComponentOutletは、テンプレートで指定されたコンポーネントを動的にレンダリングする 構造ディレクティブです。

@Component({/*...*/})
export class AdminBio { /* ... */ }

@Component({/*...*/})
export class StandardBio { /* ... */ }

@Component({
  ...,
  template: `
    <p>Profile for {{user.name}}</p>
    <ng-container *ngComponentOutlet="getBioComponent()" /> `
})
export class CustomDialog {
  user = input.required<User>();

  getBioComponent() {
    return this.user().isAdmin ? AdminBio : StandardBio;
  }
}

動的にレンダリングされたコンポーネントへのインプットの渡し方

ngComponentOutletInputsプロパティを使用して、動的にレンダリングされたコンポーネントにインプットを渡すことができます。このプロパティは、キーがインプット名で値がインプット値であるオブジェクトを受け入れます。

@Component({
  selector: 'user-greeting',
  template: `
    <div>
      <p>User: {{ username() }}</p>
      <p>Role: {{ role() }}</p>
    </div>
  `,
})
export class UserGreeting {
  username = input.required<string>();
  role = input('guest');
}

@Component({
  selector: 'profile-view',
  imports: [NgComponentOutlet],
  template: `<ng-container *ngComponentOutlet="greetingComponent; inputs: greetingInputs()" />`,
})
export class ProfileView {
  greetingComponent = UserGreeting;
  greetingInputs = signal({username: 'ngAwesome', role: 'admin'});
}

インプットはgreetingInputsシグナルが変更されるたびに更新され、動的コンポーネントを親の状態と同期させます。

コンテンツプロジェクションの提供

ngComponentOutletContentを使用して、動的にレンダリングされたコンポーネントにプロジェクトされたコンテンツを渡すことができます。これは、動的コンポーネントが<ng-content>を使用してコンテンツを表示する場合に便利です。

@Component({
  selector: 'card-wrapper',
  template: `
    <div class="card">
      <ng-content />
    </div>
  `,
})
export class CardWrapper {}

@Component({
  imports: [NgComponentOutlet],
  template: `
    <ng-container *ngComponentOutlet="cardComponent; content: cardContent()" />

    <ng-template #contentTemplate>
      <h3>Dynamic Content</h3>
      <p>This content is projected into the card.</p>
    </ng-template>
  `,
})
export class DynamicCard {
  private vcr = inject(ViewContainerRef);
  cardComponent = CardWrapper;

  private contentTemplate = viewChild<TemplateRef<unknown>>('contentTemplate');

  cardContent = computed(() => {
    const template = this.contentTemplate();
    if (!template) return [];
    // Returns an array of projection slots. Each element represents one <ng-content> slot.
    // CardWrapper has one <ng-content>, so we return an array with one element.
    return [this.vcr.createEmbeddedView(template).rootNodes];
  });
}

NOTE: Hydrationは、ネイティブDOM APIで作成されたDOMノードのプロジェクションをサポートしていません。これによりNG0503エラーが発生します。Angular APIを使用してプロジェクトされたコンテンツを作成するか、コンポーネントにngSkipHydrationを追加してください。

インジェクターの提供

ngComponentOutletInjectorを使用して、動的に作成されたコンポーネントにカスタムインジェクターを提供できます。これは、コンポーネント固有のサービスや設定を提供する場合に便利です。

export const THEME_DATA = new InjectionToken<string>('THEME_DATA', {
  factory: () => 'light',
});

@Component({
  selector: 'themed-panel',
  template: `<div [class]="theme">...</div>`,
})
export class ThemedPanel {
  theme = inject(THEME_DATA);
}

@Component({
  selector: 'dynamic-panel',
  imports: [NgComponentOutlet],
  template: `<ng-container *ngComponentOutlet="panelComponent; injector: customInjector" />`,
})
export class DynamicPanel {
  panelComponent = ThemedPanel;

  customInjector = Injector.create({
    providers: [{provide: THEME_DATA, useValue: 'dark'}],
  });
}

コンポーネントインスタンスへのアクセス

ディレクティブのexportAs機能を使用して、動的に作成されたコンポーネントのインスタンスにアクセスできます。

@Component({
  selector: 'counter',
  template: `<p>Count: {{ count() }}</p>`,
})
export class Counter {
  count = signal(0);
  increment() {
    this.count.update((c) => c + 1);
  }
}

@Component({
  imports: [NgComponentOutlet],
  template: `
    <ng-container [ngComponentOutlet]="counterComponent" #outlet="ngComponentOutlet" />

    <button (click)="outlet.componentInstance?.increment()">Increment</button>
  `,
})
export class CounterHost {
  counterComponent = Counter;
}

NOTE: componentInstanceプロパティは、コンポーネントがレンダリングされる前はnullです。

ディレクティブの機能の詳細については、NgComponentOutlet APIリファレンスを 参照してください。

ViewContainerRefの使用

ビューコンテナは、Angularのコンポーネントツリー内のノードで、コンテンツを含むことができます。任意のコンポーネント またはディレクティブはViewContainerRefを注入して、DOM内のそのコンポーネントまたはディレクティブの位置に対応する ビューコンテナへの参照を取得できます。

ViewContainerRefcreateComponentメソッドを使用して、コンポーネントを動的に作成およびレンダリングできます。 ViewContainerRefで新しいコンポーネントを作成すると、Angularはそれを、ViewContainerRefを注入したコンポーネント またはディレクティブの次の兄弟としてDOMに追加します。

@Component({
  selector: 'leaf-content',
  template: `This is the leaf content`,
})
export class LeafContent {}

@Component({
  selector: 'outer-container',
  template: `
    <p>This is the start of the outer container</p>
    <inner-item />
    <p>This is the end of the outer container</p>
  `,
})
export class OuterContainer {}

@Component({
  selector: 'inner-item',
  template: `<button (click)="loadContent()">Load content</button>`,
})
export class InnerItem {
  private viewContainer = inject(ViewContainerRef);

  loadContent() {
    this.viewContainer.createComponent(LeafContent);
  }
}

上記の例では、「Load content」ボタンをクリックすると、次のDOM構造になります

<outer-container>
  <p>This is the start of the outer container</p>
  <inner-item>
    <button>Load content</button>
  </inner-item>
  <leaf-content>This is the leaf content</leaf-content>
  <p>This is the end of the outer container</p>
</outer-container>

コンポーネントの遅延読み込み

HELPFUL: コンポーネントを遅延読み込みしたい場合は、代わりに組み込みの@defer機能の使用を検討してください。

@defer機能でカバーされていないユースケースの場合は、標準のJavaScript 動的インポートと 一緒にNgComponentOutletまたはViewContainerRefを使用できます。

@Component({
  ...,
  template: `
    <section>
      <h2>Basic settings</h2>
      <basic-settings />
    </section>
    <section>
      <h2>Advanced settings</h2>
      @if(!advancedSettings) {
        <button (click)="loadAdvanced()">
          Load advanced settings
        </button>
      }
      <ng-container *ngComponentOutlet="advancedSettings" />
    </section>
  `
})
export class AdminSettings {
  advancedSettings: {new(): AdvancedSettings} | undefined;

  async loadAdvanced() {
    const { AdvancedSettings } = await import('path/to/advanced_settings.js');
    this.advancedSettings = AdvancedSettings;
  }
}

上記の例では、ボタンクリックを受け取るとAdvancedSettingsを読み込んで表示します。

作成時のインプット、アウトプットのバインディングとホストディレクティブの設定

コンポーネントを動的に作成する場合、手動でインプットを設定し、アウトプットを購読するのはエラーが発生しやすくなります。コンポーネントがインスタンス化された後にバインディングを接続するためだけに、余分なコードを記述する必要があることがよくあります。

これを簡素化するために、createComponentViewContainerRef.createComponentの両方が、inputBinding()outputBinding()twoWayBinding()などのヘルパーを使用したbindings配列を渡して、インプットとアウトプットを事前に設定できるようにサポートしています。また、ホストディレクティブを適用するためのdirectives配列を指定できます。これにより、単一の宣言的な呼び出しで、テンプレートのようなバインディングでコンポーネントをプログラム的に作成できます。

ViewContainerRef.createComponentを使用したホストビュー

ViewContainerRef.createComponentはコンポーネントを作成し、そのホストビューとホスト要素をコンテナのビュー階層内のコンテナの位置に自動的に挿入します。動的コンポーネントがコンテナの論理的および視覚的な構造の一部になる場合(たとえば、リストアイテムやインラインUIの追加)に使用します。

対照的に、スタンドアロンのcreateComponent APIは、新しいコンポーネントを既存のビューやDOM位置にアタッチしません。ComponentRefを返し、 コンポーネントのホスト要素を配置する場所を明示的に制御できます。

import {Component, input, model, output} from '@angular/core';

@Component({
  selector: 'app-warning',
  template: `
    @if (isExpanded()) {
      <section>
        <p>Warning: Action needed!</p>
        <button (click)="close.emit(true)">Close</button>
      </section>
    }
  `,
})
export class AppWarning {
  readonly canClose = input.required<boolean>();
  readonly isExpanded = model<boolean>();
  readonly close = output<boolean>();
}
import {
  Component,
  ViewContainerRef,
  signal,
  inputBinding,
  outputBinding,
  twoWayBinding,
  inject,
} from '@angular/core';
import {FocusTrap} from '@angular/cdk/a11y';
import {ThemeDirective} from '../theme.directive';

@Component({
  template: `<ng-container #container />`,
})
export class Host {
  private vcr = inject(ViewContainerRef);
  readonly canClose = signal(true);
  readonly isExpanded = signal(true);

  showWarning() {
    const compRef = this.vcr.createComponent(AppWarning, {
      bindings: [
        inputBinding('canClose', this.canClose),
        twoWayBinding('isExpanded', this.isExpanded),
        outputBinding<boolean>('close', (confirmed) => {
          console.log('Closed with result:', confirmed);
        }),
      ],
      directives: [
        FocusTrap,
        {type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')]},
      ],
    });
  }
}

上記の例では、動的なAppWarningは、canCloseインプットがリアクティブシグナルにバインドされ、isExpanded状態で双方向バインディングが行われ、closeのアウトプットリスナーが設定されて作成されます。FocusTrapThemeDirectiveは、directivesを介してホスト要素にアタッチされます。

createComponent + hostElementdocument.bodyにアタッチされたポップアップ

現在のビュー階層の外側にレンダリングする場合(オーバーレイなど)に使用します。提供されたhostElementがDOM内のコンポーネントのホストになるため、Angularはセレクターに一致する新しい要素を作成しません。bindingsを直接設定できます。

import {
  ApplicationRef,
  createComponent,
  EnvironmentInjector,
  inject,
  Injectable,
  inputBinding,
  outputBinding,
} from '@angular/core';
import {Popup} from './popup';

@Injectable({providedIn: 'root'})
export class PopupService {
  private readonly injector = inject(EnvironmentInjector);
  private readonly appRef = inject(ApplicationRef);

  show(message: string) {
    // ポップアップ用のホスト要素を作成
    const host = document.createElement('popup-host');

    // コンポーネントを作成し、1回の呼び出しでバインド
    const ref = createComponent(Popup, {
      environmentInjector: this.injector,
      hostElement: host,
      bindings: [
        inputBinding('message', () => message),
        outputBinding('closed', () => {
          document.body.removeChild(host);
          this.appRef.detachView(ref.hostView);
          ref.destroy();
        }),
      ],
    });

    // 変更検知サイクルに参加するようにコンポーネントのビューを登録します。
    this.appRef.attachView(ref.hostView);
    // 提供されたホスト要素をDOM(通常のAngularビュー階層の外側)に挿入します。これにより、ポップアップが画面に表示されます。通常、オーバーレイやモーダルに使用されます。
    document.body.appendChild(host);
  }
}