37. Component Dev Kit (CDK)
🧰 Angular CDK — Component Dev Kit
Заголовок раздела «🧰 Angular CDK — Component Dev Kit»CDK (Component Dev Kit) — это низкоуровневый набор инструментов от Angular команды. В отличие от Angular Material, CDK не навязывает дизайн — он даёт тебе примитивы для поведения: drag-and-drop, оверлеи, виртуальный скролл, управление фокусом и доступность. Строй что хочешь поверх него 🏗️
Установка
Заголовок раздела «Установка»npm install @angular/cdkCDK уже включён в Angular Material, но можно использовать отдельно.
DragDropModule — Перетаскивание
Заголовок раздела «DragDropModule — Перетаскивание»import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
@Component({ standalone: true, imports: [DragDropModule], template: ` <!-- Список с перетаскиванием --> <div cdkDropList [cdkDropListData]="todo" (cdkDropListDropped)="drop($event)"> @for (item of todo; track item) { <div cdkDrag class="drag-item">{{ item }}</div> } </div>
<!-- Перемещение между двумя списками --> <div class="kanban"> <div cdkDropList #todoList="cdkDropList" [cdkDropListData]="todoItems" [cdkDropListConnectedTo]="[doneList]" (cdkDropListDropped)="drop($event)" > @for (item of todoItems; track item.id) { <div cdkDrag [cdkDragData]="item">{{ item.title }}</div> } </div>
<div cdkDropList #doneList="cdkDropList" [cdkDropListData]="doneItems" [cdkDropListConnectedTo]="[todoList]" (cdkDropListDropped)="drop($event)" > @for (item of doneItems; track item.id) { <div cdkDrag [cdkDragData]="item">{{ item.title }}</div> } </div> </div> `})export class KanbanComponent { todoItems = [ { id: 1, title: 'Настроить роутинг' }, { id: 2, title: 'Написать тесты' }, { id: 3, title: 'Задеплоить' }, ]; doneItems: any[] = [];
drop(event: CdkDragDrop<any[]>) { if (event.previousContainer === event.container) { // Перемещение внутри одного списка moveItemInArray( event.container.data, event.previousIndex, event.currentIndex ); } else { // Перемещение между списками transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex ); } }}OverlayModule — Всплывающие слои
Заголовок раздела «OverlayModule — Всплывающие слои»import { Overlay, OverlayModule, OverlayRef } from '@angular/cdk/overlay';import { ComponentPortal, PortalModule } from '@angular/cdk/portal';
@Component({ standalone: true, imports: [OverlayModule, PortalModule], template: ` <button #origin (click)="openTooltip(origin)">Наведи/Кликни</button> `})export class TooltipTriggerComponent { private overlay = inject(Overlay); private overlayRef: OverlayRef | null = null;
openTooltip(origin: HTMLElement) { if (this.overlayRef) { this.close(); return; }
// Позиционируем относительно элемента const positionStrategy = this.overlay .position() .flexibleConnectedTo(origin) .withPositions([ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 8, }, { // Фоллбэк: если снизу нет места — показать сверху originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -8, } ]);
this.overlayRef = this.overlay.create({ positionStrategy, hasBackdrop: true, backdropClass: 'cdk-overlay-transparent-backdrop', scrollStrategy: this.overlay.scrollStrategies.reposition(), });
// Монтируем компонент const portal = new ComponentPortal(TooltipContentComponent); this.overlayRef.attach(portal);
// Закрываем по клику на бэкдроп this.overlayRef.backdropClick().subscribe(() => this.close()); }
close() { this.overlayRef?.dispose(); this.overlayRef = null; }}VirtualScrollViewport — Виртуальный скролл
Заголовок раздела «VirtualScrollViewport — Виртуальный скролл»Незаменим для рендера больших списков (10 000+ элементов):
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({ standalone: true, imports: [ScrollingModule], template: ` <cdk-virtual-scroll-viewport itemSize="50" class="scroll-viewport" style="height: 400px" > @for (item of items; track item.id) { <div *cdkVirtualFor="let item of items; trackBy: trackById" class="list-item" style="height: 50px"> {{ item.name }} </div> } </cdk-virtual-scroll-viewport> `})export class BigListComponent { items = Array.from({ length: 10_000 }, (_, i) => ({ id: i, name: \`Пользователь #\${i + 1}\` }));
trackById = (i: number, item: { id: number }) => item.id;}TextFieldModule — Auto-resize textarea
Заголовок раздела «TextFieldModule — Auto-resize textarea»import { TextFieldModule, CdkTextareaAutosize } from '@angular/cdk/text-field';
@Component({ standalone: true, imports: [TextFieldModule], template: ` <textarea cdkTextareaAutosize cdkAutosizeMinRows="2" cdkAutosizeMaxRows="8" placeholder="Введи текст — textarea растёт автоматически" ></textarea> `})export class AutosizeTextareaComponent { @ViewChild('autosize') autosize!: CdkTextareaAutosize;
// Принудительный ресайз triggerResize() { // Нужен NgZone для корректной работы this.ngZone.onStable.pipe(take(1)).subscribe(() => { this.autosize.resizeToFitContent(true); }); }}A11y — Доступность
Заголовок раздела «A11y — Доступность»FocusTrap — ловушка фокуса (для модалок)
Заголовок раздела «FocusTrap — ловушка фокуса (для модалок)»import { A11yModule, FocusTrap, FocusTrapFactory } from '@angular/cdk/a11y';
@Component({ standalone: true, imports: [A11yModule], template: ` <!-- cdkTrapFocus блокирует фокус внутри элемента --> <div cdkTrapFocus cdkTrapFocusAutoCapture> <button>Первая кнопка (автофокус)</button> <input placeholder="Поле" /> <button>Закрыть</button> </div> `})export class ModalComponent {}LiveAnnouncer — уведомления для скринридеров
Заголовок раздела «LiveAnnouncer — уведомления для скринридеров»import { LiveAnnouncer } from '@angular/cdk/a11y';
@Injectable({ providedIn: 'root' })export class NotificationService { private announcer = inject(LiveAnnouncer);
announce(message: string) { // Скринридер прочитает это сообщение this.announcer.announce(message, 'assertive'); }}
// Использованиеthis.notificationService.announce('3 файла загружено');Portal — динамический рендер
Заголовок раздела «Portal — динамический рендер»import { Portal, ComponentPortal, TemplatePortal, PortalModule, CdkPortalOutlet } from '@angular/cdk/portal';import { ComponentRef, TemplateRef, ViewContainerRef } from '@angular/core';
@Component({ standalone: true, imports: [PortalModule], template: ` <!-- Место для монтирования портала --> <ng-template [cdkPortalOutlet]="currentPortal"></ng-template>
<button (click)="showComponent()">Показать компонент</button> <button (click)="showTemplate(myTpl)">Показать шаблон</button>
<ng-template #myTpl> <p>Это шаблон из портала 🎭</p> </ng-template> `})export class PortalDemoComponent { currentPortal: Portal<any> | null = null; private vcr = inject(ViewContainerRef);
showComponent() { this.currentPortal = new ComponentPortal(SomeOtherComponent); }
showTemplate(tpl: TemplateRef<any>) { this.currentPortal = new TemplatePortal(tpl, this.vcr); }}BreakpointObserver — Адаптивность
Заголовок раздела «BreakpointObserver — Адаптивность»import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@Component({ standalone: true, template: ` @if (isMobile()) { <app-mobile-nav /> } @else { <app-desktop-nav /> } `})export class AppLayoutComponent { private breakpoints = inject(BreakpointObserver);
isMobile = toSignal( this.breakpoints .observe([Breakpoints.Handset, Breakpoints.TabletPortrait]) .pipe(map(state => state.matches)), { initialValue: false } );}
// Кастомные breakpointsthis.breakpoints.observe(['(max-width: 768px)']).subscribe(state => { if (state.matches) console.log('Мобильный вид');});ClipboardModule
Заголовок раздела «ClipboardModule»import { ClipboardModule } from '@angular/cdk/clipboard';
@Component({ standalone: true, imports: [ClipboardModule], template: ` <button [cdkCopyToClipboard]="textToCopy" (cdkCopyToClipboardCopied)="onCopied($event)"> 📋 Скопировать </button> `})export class CopyButtonComponent { textToCopy = 'Скопированный текст';
onCopied(success: boolean) { console.log(success ? 'Скопировано!' : 'Ошибка копирования'); }}