Как использовать Portal и Overlay из CDK?angular-78

Portal и Overlay - мощные инструменты Angular CDK для динамического отображения контента в произвольных местах DOM. Рассмотрим их применение подробно.

1. Основные понятия

Portal - абстракция для динамического контента, который можно "прикрепить" к любой точке приложения. Бывает трех типов:

  • ComponentPortal - для компонентов
  • TemplatePortal - для шаблонов
  • DomPortal - для DOM-узлов

Overlay - сервис для управления позиционированием поверх основного контента (модалки, попапы, меню)

2. Настройка окружения

Сначала импортируем необходимые модули:

import { OverlayModule, PortalModule } from '@angular/cdk/overlay';

@NgModule({
  imports: [
    OverlayModule,
    PortalModule
  ]
})
export class AppModule { }

3. Создание простого Portal

ComponentPortal

import { ComponentPortal } from '@angular/cdk/portal';

// Создаем портал для компонента
const componentPortal = new ComponentPortal(MyDynamicComponent);

// Где-то в коде компонента
@ViewChild('portalOutlet') portalOutlet: PortalOutlet;

attachComponent() {
  this.portalOutlet.attach(componentPortal);
}

TemplatePortal

<ng-template #templatePortal>
  <div>Динамический контент</div>
</ng-template>
import { TemplatePortal } from '@angular/cdk/portal';

@ViewChild('templatePortal') template: TemplateRef<any>;

ngAfterViewInit() {
  this.templatePortal = new TemplatePortal(
    this.template,
    this.viewContainerRef
  );
}

4. Работа с Overlay

Создание базового Overlay

import { Overlay } from '@angular/cdk/overlay';

constructor(private overlay: Overlay) {}

openOverlay() {
  // Создаем Overlay конфигурацию
  const overlayRef = this.overlay.create({
    hasBackdrop: true,
    positionStrategy: this.overlay.position()
      .global()
      .centerHorizontally()
      .centerVertically(),
    scrollStrategy: this.overlay.scrollStrategies.block(),
    width: '400px',
    height: '300px'
  });

  // Прикрепляем компонент
  const portal = new ComponentPortal(MyDialogComponent);
  overlayRef.attach(portal);

  // Закрытие по клику на бэкдроп
  overlayRef.backdropClick().subscribe(() => {
    overlayRef.dispose();
  });
}

Позиционирование Overlay

// Относительно элемента
const positionStrategy = this.overlay.position()
  .flexibleConnectedTo(this.triggerElement.nativeElement)
  .withPositions([
    {
      originX: 'start',
      originY: 'bottom',
      overlayX: 'start',
      overlayY: 'top',
      offsetY: 8
    }
  ]);

// Глобальное позиционирование
const globalPosition = this.overlay.position()
  .global()
  .top('20px')
  .right('20px');

5. Комбинированный пример: выпадающее меню

import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';

@Directive({
  selector: '[appDropdown]'
})
export class DropdownDirective {
  private overlayRef: OverlayRef;

  constructor(
    private overlay: Overlay,
    private viewContainerRef: ViewContainerRef,
    private elementRef: ElementRef
  ) {}

  @Input() set appDropdown(menu: TemplateRef<any>) {
    this.menuTemplate = menu;
  }

  @HostListener('click') onClick() {
    if (this.overlayRef) {
      this.close();
    } else {
      this.open();
    }
  }

  open() {
    const positionStrategy = this.overlay.position()
      .flexibleConnectedTo(this.elementRef)
      .withPositions([{
        originX: 'start',
        originY: 'bottom',
        overlayX: 'start',
        overlayY: 'top',
        offsetY: 8
      }]);

    this.overlayRef = this.overlay.create({
      positionStrategy,
      scrollStrategy: this.overlay.scrollStrategies.reposition(),
      hasBackdrop: true,
      backdropClass: 'cdk-overlay-transparent-backdrop'
    });

    const portal = new TemplatePortal(
      this.menuTemplate,
      this.viewContainerRef
    );

    this.overlayRef.attach(portal);

    this.overlayRef.backdropClick().subscribe(() => {
      this.close();
    });
  }

  close() {
    this.overlayRef.dispose();
    this.overlayRef = null;
  }
}

6. Передача данных в Portal

// Создаем инжектор с данными
const injector = Injector.create({
  providers: [
    { provide: DIALOG_DATA, useValue: { title: 'Hello', content: 'World' } }
  ],
  parent: this.injector
});

// Прикрепляем портал с кастомным инжектором
overlayRef.attach(new ComponentPortal(MyDialogComponent, null, injector));

// В компоненте диалога
constructor(@Inject(DIALOG_DATA) public data: any) {}

7. Анимации и кастомизация

Добавление анимации

overlayRef.overlayElement.classList.add('fade-in');
.fade-in {
  animation: fadeIn 0.3s;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

Кастомизация бэкдропа

overlayRef.updateSize({
  width: '80vw',
  height: '80vh'
});

overlayRef.updatePositionStrategy(
  this.overlay.position().global().centerHorizontally().centerVertically()
);

8. Лучшие практики

  1. Управление памятью - всегда вызывайте dispose() для OverlayRef
  2. Закрытие по ESC - добавьте обработчик клавиатуры
  3. Доступность - управляйте фокусом (cdkTrapFocus)
  4. Адаптивность - учитывайте мобильные устройства
  5. Тестирование - используйте OverlayContainer для тестов

Пример обработки ESC:

import { ESCAPE } from '@angular/cdk/keycodes';

overlayRef.keydownEvents().subscribe(event => {
  if (event.keyCode === ESCAPE) {
    this.close();
  }
});

Резюмируем

Portal и Overlay из Angular CDK предоставляют мощный API для создания динамических, доступных и производительных оверлейных компонентов. Их комбинация позволяет реализовать сложные UI-паттерны, сохраняя контроль над позиционированием, управлением состоянием и производительностью.