angular-cdk
4
总安装量
4
周安装量
#49669
全站排名
安装命令
npx skills add https://github.com/7spade/black-tortoise --skill angular-cdk
Agent 安装分布
antigravity
3
opencode
2
windsurf
2
claude-code
2
codex
2
gemini-cli
2
Skill 文档
Angular CDK (Component Dev Kit) Skill
Rules
Lifecycle & Resource Management
- Must create
FocusTrapinngOnInit()usingfocusTrapFactory.create() - Must call
focusTrap.focusInitialElement()after creating focus trap - Must destroy
FocusTrapinngOnDestroy()usingfocusTrap.destroy() - Must stop monitoring focus in
ngOnDestroy()usingfocusMonitor.stopMonitoring() - Must destroy
keyManagerinngOnDestroy()usingkeyManager.destroy() - Must dispose
overlayRefin cleanup usingoverlayRef.dispose() - Must explicitly release all CDK resources in
ngOnDestroy() - Must clean up:
focusTrap,overlayRef,focusMonitor,keyManager - Shall Not leave CDK resources without cleanup (causes memory leaks)
Accessibility
- Must use
LiveAnnouncerfor screen reader announcements of dynamic content - Must use
FocusKeyManagerfor keyboard navigation in lists
Overlay Positioning
- Must use
flexibleConnectedTo()with at least two fallback position strategies - Must configure explicit
scrollStrategyfor overlay - Must configure explicit
hasBackdropproperty for overlay - Must attach portal to overlay using
overlayRef.attach(portal)
Portal Usage
- Must use
ComponentPortalfor dynamic component rendering - Must use
TemplatePortalfor template content projection - Must detach portal when no longer needed to prevent memory leaks
Drag & Drop
- Must use
cdkDropListcontainer with(cdkDropListDropped)event handler - Must use
cdkDragdirective on draggable items - Must use
trackexpressions in@forloops with drag items - Must use
moveItemInArray()for single-list reordering - Must use
transferArrayItem()for cross-list transfers - Must connect drop lists using
[cdkDropListConnectedTo]for cross-list drag
Virtual Scrolling
- Only use
CdkVirtualScrollViewportfor lists with more than 100 items - Must set explicit
itemSizeproperty - Must configure
minBufferPxandmaxBufferPxfor smooth scrolling - Must set fixed height on viewport container
- Must Not use auto-height virtual scroll viewports
Breakpoint Observer
- Must use
BreakpointObserverfor responsive layout detection - Must use
toSignal()when converting breakpoint observables to signals - Must provide
initialValuewhen usingtoSignal()with breakpoints - Must Not use direct
window.innerWidthor manual resize listeners - Must Not use direct
window.matchMedia()calls
Context
ð¯ Purpose
This skill provides comprehensive guidance on the Angular Component Dev Kit (CDK), a set of behavior primitives for building accessible, high-quality Angular components without Material Design styling.
ð¦ What is Angular CDK?
The Angular CDK is a library of unstyled UI component behaviors:
- Accessibility (A11y): Screen reader support, keyboard navigation, focus management
- Overlay: Positioning, backdrop, and overlay management
- Portal: Dynamic component rendering
- Drag & Drop: Sortable lists and drag-and-drop interactions
- Virtual Scrolling: Efficient rendering of large lists
- Layout: Responsive layout utilities and breakpoint observers
- Table: Data table functionality without styling
- Tree: Hierarchical tree structures
ð¨ When to Use This Skill
Use Angular CDK guidance when:
- Building custom UI components from scratch
- Implementing accessible components (ARIA, focus management)
- Creating overlays, modals, or tooltips
- Building drag-and-drop functionality
- Implementing virtual scrolling for performance
- Working with responsive layouts and breakpoints
- Creating custom data tables or tree views
- Managing focus trapping and keyboard navigation
ð Core CDK Modules
1. Accessibility (A11y)
Focus Management:
import { FocusTrapFactory, FocusMonitor } from '@angular/cdk/a11y';
export class DialogComponent implements OnInit, OnDestroy {
private focusTrap: FocusTrap;
constructor(
private focusTrapFactory: FocusTrapFactory,
private focusMonitor: FocusMonitor,
private elementRef: ElementRef
) {}
ngOnInit() {
// Create focus trap to keep focus within dialog
this.focusTrap = this.focusTrapFactory.create(
this.elementRef.nativeElement
);
this.focusTrap.focusInitialElement();
// Monitor focus changes
this.focusMonitor.monitor(this.elementRef, true).subscribe(origin => {
console.log('Focus origin:', origin);
});
}
ngOnDestroy() {
this.focusTrap.destroy();
this.focusMonitor.stopMonitoring(this.elementRef);
}
}
Live Announcer (Screen Readers):
import { LiveAnnouncer } from '@angular/cdk/a11y';
export class NotificationComponent {
constructor(private liveAnnouncer: LiveAnnouncer) {}
announce(message: string) {
// Announce to screen readers
this.liveAnnouncer.announce(message, 'polite');
}
announceUrgent(message: string) {
this.liveAnnouncer.announce(message, 'assertive');
}
}
List Key Manager (Keyboard Navigation):
import { FocusKeyManager } from '@angular/cdk/a11y';
import { QueryList, ViewChildren, AfterViewInit } from '@angular/core';
export class MenuComponent implements AfterViewInit {
@ViewChildren(MenuItem) menuItems: QueryList<MenuItem>;
private keyManager: FocusKeyManager<MenuItem>;
ngAfterViewInit() {
this.keyManager = new FocusKeyManager(this.menuItems)
.withWrap()
.withVerticalOrientation();
}
onKeydown(event: KeyboardEvent) {
this.keyManager.onKeydown(event);
}
}
2. Overlay
Creating Overlays:
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
export class TooltipDirective {
private overlayRef: OverlayRef | null = null;
constructor(
private overlay: Overlay,
private elementRef: ElementRef
) {}
show() {
// Create overlay configuration
const config = new OverlayConfig({
positionStrategy: this.overlay.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top',
offsetY: 8
}
]),
hasBackdrop: false,
scrollStrategy: this.overlay.scrollStrategies.reposition()
});
// Create overlay
this.overlayRef = this.overlay.create(config);
// Attach component to overlay
const portal = new ComponentPortal(TooltipComponent);
this.overlayRef.attach(portal);
}
hide() {
if (this.overlayRef) {
this.overlayRef.dispose();
this.overlayRef = null;
}
}
}
Global Position Strategy:
const config = new OverlayConfig({
positionStrategy: this.overlay.position()
.global()
.centerHorizontally()
.centerVertically(),
hasBackdrop: true,
backdropClass: 'dark-backdrop'
});
3. Portal
Template Portals:
import { TemplatePortal } from '@angular/cdk/portal';
import { TemplateRef, ViewContainerRef } from '@angular/core';
export class PortalExample {
@ViewChild('templatePortalContent') templateRef: TemplateRef<any>;
selectedPortal: TemplatePortal<any>;
constructor(private viewContainerRef: ViewContainerRef) {}
createTemplatePortal() {
this.selectedPortal = new TemplatePortal(
this.templateRef,
this.viewContainerRef
);
}
}
<ng-template #templatePortalContent>
<p>This content will be rendered in a portal</p>
</ng-template>
<div cdkPortalOutlet [cdkPortalOutlet]="selectedPortal"></div>
Component Portals:
import { ComponentPortal } from '@angular/cdk/portal';
createComponentPortal() {
const portal = new ComponentPortal(
MyComponent,
null, // ViewContainerRef (optional)
this.injector, // Custom injector (optional)
this.componentFactoryResolver // (optional)
);
// Attach to overlay or portal outlet
const ref = this.overlayRef.attach(portal);
ref.instance.data = 'Custom data';
}
4. Drag & Drop
Sortable List:
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
@Component({
template: `
<div cdkDropList (cdkDropListDropped)="drop($event)">
@for (item of items; track item) {
<div cdkDrag>
<div class="drag-handle" cdkDragHandle>â®â®</div>
{{ item }}
</div>
}
</div>
`
})
export class SortableListComponent {
items = ['Item 1', 'Item 2', 'Item 3'];
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
}
}
Drag Between Lists:
import { CdkDragDrop, transferArrayItem } from '@angular/cdk/drag-drop';
@Component({
template: `
<div class="lists-container">
<div cdkDropList #todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList]"
(cdkDropListDropped)="drop($event)">
@for (item of todo; track item) {
<div cdkDrag>{{ item }}</div>
}
</div>
<div cdkDropList #doneList="cdkDropList"
[cdkDropListData]="done"
[cdkDropListConnectedTo]="[todoList]"
(cdkDropListDropped)="drop($event)">
@for (item of done; track item) {
<div cdkDrag>{{ item }}</div>
}
</div>
</div>
`
})
export class TaskBoardComponent {
todo = ['Task 1', 'Task 2'];
done = ['Task 3'];
drop(event: CdkDragDrop<string[]>) {
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
);
}
}
}
5. Virtual Scrolling
Virtual Scroll for Large Lists:
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
@for (item of items; track item) {
<div class="item">{{ item }}</div>
}
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 400px;
width: 100%;
}
.item {
height: 50px;
display: flex;
align-items: center;
}
`]
})
export class VirtualScrollComponent {
items = Array.from({ length: 10000 }, (_, i) => `Item #${i}`);
}
Dynamic Item Sizes:
@Component({
template: `
<cdk-virtual-scroll-viewport
[itemSize]="50"
minBufferPx="200"
maxBufferPx="400">
@for (item of items; track item.id) {
<div class="item" [style.height.px]="item.height">
{{ item.content }}
</div>
}
</cdk-virtual-scroll-viewport>
`
})
export class DynamicVirtualScrollComponent {
items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
content: `Item ${i}`,
height: 40 + (i % 3) * 20 // Variable heights
}));
}
6. Layout & Breakpoints
Breakpoint Observer:
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
export class ResponsiveComponent {
isHandset$ = this.breakpointObserver.observe(Breakpoints.Handset)
.pipe(map(result => result.matches));
constructor(private breakpointObserver: BreakpointObserver) {
// Observe multiple breakpoints
this.breakpointObserver.observe([
Breakpoints.XSmall,
Breakpoints.Small,
Breakpoints.Medium,
Breakpoints.Large,
Breakpoints.XLarge
]).subscribe(result => {
if (result.breakpoints[Breakpoints.XSmall]) {
this.columns = 1;
} else if (result.breakpoints[Breakpoints.Small]) {
this.columns = 2;
} else {
this.columns = 4;
}
});
}
}
Custom Breakpoints:
const customBreakpoints = {
mobile: '(max-width: 599px)',
tablet: '(min-width: 600px) and (max-width: 959px)',
desktop: '(min-width: 960px)'
};
this.breakpointObserver.observe([customBreakpoints.mobile])
.subscribe(result => {
this.isMobile = result.matches;
});
7. Table
CDK Table (Unstyled):
import { DataSource } from '@angular/cdk/collections';
@Component({
template: `
<cdk-table [dataSource]="dataSource">
<ng-container cdkColumnDef="name">
<cdk-header-cell *cdkHeaderCellDef>Name</cdk-header-cell>
<cdk-cell *cdkCellDef="let row">{{ row.name }}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="email">
<cdk-header-cell *cdkHeaderCellDef>Email</cdk-header-cell>
<cdk-cell *cdkCellDef="let row">{{ row.email }}</cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="displayedColumns"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: displayedColumns;"></cdk-row>
</cdk-table>
`
})
export class TableComponent {
displayedColumns = ['name', 'email'];
dataSource = new MyDataSource();
}
8. Tree
Nested Tree:
import { NestedTreeControl } from '@angular/cdk/tree';
import { ArrayDataSource } from '@angular/cdk/collections';
interface FileNode {
name: string;
children?: FileNode[];
}
@Component({
template: `
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
<cdk-nested-tree-node *cdkTreeNodeDef="let node">
{{ node.name }}
</cdk-nested-tree-node>
<cdk-nested-tree-node *cdkTreeNodeDef="let node; when: hasChild">
<button (click)="treeControl.toggle(node)">
{{ treeControl.isExpanded(node) ? 'â¼' : 'â¶' }}
</button>
{{ node.name }}
<div *ngIf="treeControl.isExpanded(node)">
<ng-container cdkTreeNodeOutlet></ng-container>
</div>
</cdk-nested-tree-node>
</cdk-tree>
`
})
export class TreeComponent {
treeControl = new NestedTreeControl<FileNode>(node => node.children);
dataSource = new ArrayDataSource(TREE_DATA);
hasChild = (_: number, node: FileNode) => !!node.children && node.children.length > 0;
}
ð¯ Best Practices
1. Accessibility First
// Always use CDK a11y utilities for custom components
import { FocusTrap, LiveAnnouncer } from '@angular/cdk/a11y';
// Trap focus in modals
// Announce dynamic content changes
// Implement keyboard navigation
2. Overlay Positioning
// Use flexible positioning for better UX
const positionStrategy = this.overlay.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
// Primary position
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
// Fallback positions
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' }
]);
3. Virtual Scrolling Performance
// Configure buffer sizes for smooth scrolling
<cdk-virtual-scroll-viewport
itemSize="50"
minBufferPx="400" // Render extra items above
maxBufferPx="800"> // Render extra items below
4. Drag & Drop Constraints
// Constrain drag movement
<div cdkDrag [cdkDragBoundary]="'.boundary-element'">
Constrained drag
</div>
// Custom drag preview
<div cdkDrag>
<div *cdkDragPreview>Custom preview</div>
<div *cdkDragPlaceholder>Placeholder</div>
Content
</div>
ð Troubleshooting
| Issue | Solution |
|---|---|
| Overlay not positioning correctly | Check scroll strategy and position preferences |
| Virtual scroll not rendering | Ensure itemSize matches actual item height |
| Drag & drop not working | Import DragDropModule and use correct directives |
| Focus trap not working | Ensure element has focusable children |
| Breakpoint observer not triggering | Check for correct breakpoint strings |
| Tree not expanding | Implement hasChild function and tree control |
ð References
ð¡ Common Use Cases
- Custom Dropdown: Use Overlay + Portal
- Accessible Dialog: Use Overlay + Focus Trap + Live Announcer
- Infinite Scroll: Use Virtual Scroll Viewport
- Sortable Table: Use CDK Table + Drag Drop
- Responsive Sidebar: Use Breakpoint Observer + Layout
- File Tree: Use CDK Tree
- Custom Tooltip: Use Overlay + Positioning Strategy
ð Recommended Placement
Project-level skill:
/.github/skills/angular-cdk/SKILL.md
Copilot will load this when working with Angular CDK features.