Spec-Zone.ru › Angular

Программный рендеринг компонентов

СОВЕТ: Это руководство предполагает, что вы уже прочитали руководство по основам. Если вы только начинаете работать с Angular, сначала прочитайте его.

Помимо использования компонента непосредственно в шаблоне, вы также можете динамически выполнять программный рендеринг компонентов. Это полезно в ситуациях, когда компонент изначально неизвестен (поэтому на него нельзя напрямую сослаться в шаблоне) и его использование зависит от определённых условий.

Есть два основных способа программного рендеринга компонента: в шаблоне с помощью NgComponentOutlet или в коде TypeScript с помощью ViewContainerRef.

ПОЛЕЗНО: для сценариев с ленивой загрузкой (например, если вы хотите отложить загрузку тяжёлого компонента) рассмотрите возможность использования встроенной функции @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];
  });
}

ПРИМЕЧАНИЕ: Гидратация не поддерживает проецирование узлов DOM, созданных с помощью нативных API DOM. Это приводит к ошибке NG0503. Используйте API Angular для создания проецируемого содержимого или добавьте 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;
}

ПРИМЕЧАНИЕ: Свойство componentInstance имеет значение null до рендеринга компонента.

Дополнительные сведения о возможностях директивы см. в справочнике по API NgComponentOutlet.

Использование ViewContainerRef

Контейнер представлений — это узел в дереве компонентов Angular, который может содержать содержимое. Любой компонент или директива может внедрить ViewContainerRef, чтобы получить ссылку на контейнер представлений, соответствующий расположению этого компонента или директивы в DOM.

Вы можете использовать метод createComponent для ViewContainerRef, чтобы динамически создать компонент и выполнить его рендеринг. Когда вы создаёте новый компонент с помощью ViewContainerRef, Angular добавляет его в DOM следующим соседним узлом компонента или директивы, внедрившего ViewContainerRef.

@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);
  }
}

В приведённом выше примере нажатие кнопки «Загрузить содержимое» приводит к следующей структуре 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>

Ленивая загрузка компонентов

ПОЛЕЗНО: если вы хотите выполнять ленивую загрузку некоторых компонентов, рассмотрите возможность использования встроенной функции @defer.

Если ваш сценарий использования не поддерживается функцией @defer, можно использовать NgComponentOutlet или ViewContainerRef вместе со стандартным динамическим импортом JavaScript.

@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 после нажатия кнопки.

Привязка входных и выходных параметров, а также назначение директив хоста при создании

При динамическом создании компонентов ручная настройка входных параметров и подписка на выходные события могут приводить к ошибкам. Часто приходится писать дополнительный код только для настройки привязок после создания экземпляра компонента.

Чтобы упростить эту задачу, createComponent и ViewContainerRef.createComponent поддерживают передачу массива bindings с такими вспомогательными функциями, как inputBinding(), outputBinding() и twoWayBinding(), для предварительной настройки входных и выходных параметров. Также можно указать массив directives для применения любых директив хоста. Это позволяет создавать компоненты программно, задавая привязки в стиле шаблонов одним декларативным вызовом.

Представление хоста с использованием ViewContainerRef.createComponent

ViewContainerRef.createComponent создаёт компонент и автоматически вставляет его представление хоста и элемент хоста в иерархию представлений контейнера в месте расположения контейнера. Используйте этот способ, если динамический компонент должен стать частью логической и визуальной структуры контейнера (например, при добавлении элементов списка или встроенного пользовательского интерфейса).

В отличие от него, автономный API createComponent не присоединяет новый компонент ни к одному существующему представлению или расположению в 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. FocusTrap и ThemeDirective прикрепляются к элементу хоста с помощью directives.

Всплывающее окно, прикреплённое к document.body с помощью createComponent + hostElement

Используйте этот способ для рендеринга вне текущей иерархии представлений (например, оверлеев). Указанный hostElement становится хостом компонента в DOM, поэтому Angular не создаёт новый элемент, соответствующий селектору. Позволяет настраивать привязки напрямую.

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

@Service()
export class PopupService {
  private readonly injector = inject(EnvironmentInjector);
  private readonly appRef = inject(ApplicationRef);

  show(message: string) {
    // Create a host element for the popup
    const host = document.createElement('popup-host');

    // Create the component and bind in one call
    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();
        }),
      ],
    });

    // Registers the component’s view so it participates in change detection cycle.
    this.appRef.attachView(ref.hostView);
    // Inserts the provided host element into the DOM (outside the normal Angular view hierarchy).
    // This is what makes the popup visible on screen, typically used for overlays or modals.
    document.body.appendChild(host);
  }
}

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/components/programmatic-rendering

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API