The Angular CDK Collections module provides utilities for managing data collections, selection state, and data sources for components like tables, lists, and trees.
A model for managing selection state in collections with support for single and multiple selection modes.
/**
* Model for managing selection state
* @template T Type of items that can be selected
*/
class SelectionModel<T> {
/**
* Create a selection model
* @param multiple - Whether multiple selection is allowed
* @param initiallySelectedValues - Initially selected values
* @param emitChanges - Whether to emit change events
* @param compareWith - Function to compare items for equality
*/
constructor(
multiple?: boolean,
initiallySelectedValues?: T[],
emitChanges?: boolean,
compareWith?: (o1: T, o2: T) => boolean
);
/**
* Selected values as a sorted array
*/
readonly selected: T[];
/**
* Observable that emits when selection changes
*/
readonly changed: Subject<SelectionChange<T>>;
/**
* Select one or more values
* @param values - Values to select
*/
select(...values: T[]): void;
/**
* Deselect one or more values
* @param values - Values to deselect
*/
deselect(...values: T[]): void;
/**
* Toggle the selection state of a value
* @param value - Value to toggle
*/
toggle(value: T): void;
/**
* Clear all selections
*/
clear(): void;
/**
* Check if a value is selected
* @param value - Value to check
* @returns True if the value is selected
*/
isSelected(value: T): boolean;
/**
* Check if the selection is empty
* @returns True if no items are selected
*/
isEmpty(): boolean;
/**
* Check if the selection has values
* @returns True if any items are selected
*/
hasValue(): boolean;
/**
* Sort the selected values using a comparison function
* @param predicate - Comparison function
*/
sort(predicate?: (a: T, b: T) => number): void;
/**
* Check if multiple selection is enabled
* @returns True if multiple selection is enabled
*/
isMultipleSelection(): boolean;
}
/**
* Event emitted when selection changes
* @template T Type of selected items
*/
interface SelectionChange<T> {
/** The selection model that emitted the change */
source: SelectionModel<T>;
/** Items that were added to the selection */
added: T[];
/** Items that were removed from the selection */
removed: T[];
}Usage Example:
import { SelectionModel } from '@angular/cdk/collections';
@Component({
selector: 'app-selectable-list'
})
export class SelectableListComponent {
items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
selection = new SelectionModel<string>(true, []); // Multiple selection enabled
constructor() {
this.selection.changed.subscribe(change => {
console.log('Selection changed:', change);
});
}
isSelected(item: string): boolean {
return this.selection.isSelected(item);
}
toggle(item: string): void {
this.selection.toggle(item);
}
selectAll(): void {
this.selection.select(...this.items);
}
clearSelection(): void {
this.selection.clear();
}
get selectedItems(): string[] {
return this.selection.selected;
}
}Abstract base class for data sources that provide data to data-displaying components.
/**
* Base class for data sources
* @template T Type of data items
*/
abstract class DataSource<T> {
/**
* Connect to the data source
* @param collectionViewer - Viewer that will display the data
* @returns Observable that emits data arrays
*/
abstract connect(collectionViewer: CollectionViewer): Observable<T[]>;
/**
* Disconnect from the data source
* @param collectionViewer - Viewer that was displaying the data
*/
abstract disconnect(collectionViewer: CollectionViewer): void;
}
/**
* Interface for collection viewers that display data
*/
interface CollectionViewer {
/**
* Observable that emits when the view range changes
*/
viewChange: Observable<ListRange>;
}
/**
* Range of items visible in a list
*/
interface ListRange {
/** Start index of visible range */
start: number;
/** End index of visible range */
end: number;
}Simple data source implementation backed by an array.
/**
* Data source backed by an array
* @template T Type of data items
*/
class ArrayDataSource<T> extends DataSource<T> {
/**
* Create an array data source
* @param data - Initial data array or observable
*/
constructor(data: T[] | Observable<T[]>);
/**
* The data array backing this data source
*/
data: T[];
/**
* Connect to the data source
* @param collectionViewer - Collection viewer
* @returns Observable that emits the data array
*/
connect(collectionViewer: CollectionViewer): Observable<T[]>;
/**
* Disconnect from the data source
* @param collectionViewer - Collection viewer
*/
disconnect(collectionViewer: CollectionViewer): void;
}Usage Example:
import { ArrayDataSource } from '@angular/cdk/collections';
@Component({
selector: 'app-data-list'
})
export class DataListComponent {
items = ['Apple', 'Banana', 'Cherry', 'Date'];
dataSource = new ArrayDataSource(this.items);
addItem(item: string): void {
this.items.push(item);
this.dataSource.data = [...this.items]; // Trigger update
}
removeItem(index: number): void {
this.items.splice(index, 1);
this.dataSource.data = [...this.items]; // Trigger update
}
}Service for managing unique selections across multiple components.
/**
* Service for dispatching unique selection events
*/
class UniqueSelectionDispatcher implements OnDestroy {
/**
* Listen for selection events with a given ID and name
* @param id - Unique identifier for the selection group
* @param name - Name of the selection within the group
* @returns Observable that emits when other selections change
*/
listen(id: string, name: string): Observable<UniqueSelectionDispatcherListener>;
/**
* Notify that a selection has been made
* @param id - Unique identifier for the selection group
* @param name - Name of the selection within the group
*/
notify(id: string, name: string): void;
/**
* Clean up resources
*/
ngOnDestroy(): void;
}
/**
* Event emitted by unique selection dispatcher
*/
interface UniqueSelectionDispatcherListener {
/** ID of the selection group */
id: string;
/** Name of the selection that changed */
name: string;
}Strategies for efficiently rendering and managing repeated views in collections.
/**
* Interface for view repeating strategies
* @template T Type of data items
* @template R Type of repeated views
* @template C Type of context
*/
interface ViewRepeater<T, R, C> {
/**
* Apply changes to the view repeater
* @param changes - Collection of changes to apply
*/
applyChanges(changes: IterableChanges<T>): void;
/**
* Detach a view at the given index
* @param index - Index of view to detach
* @returns Detached view
*/
detach(index: number): R;
/**
* Move a view from one index to another
* @param oldIndex - Current index
* @param newIndex - Target index
*/
move(oldIndex: number, newIndex: number): void;
}
/**
* View repeater strategy that recycles views
* @template T Type of data items
* @template R Type of repeated views
* @template C Type of context
*/
class RecycleViewRepeaterStrategy<T, R, C> implements ViewRepeater<T, R, C> {
/**
* Apply changes to recycle views
* @param changes - Collection changes
*/
applyChanges(changes: IterableChanges<T>): void;
/**
* Detach view at index
* @param index - Index to detach
*/
detach(index: number): R;
/**
* Move view between indices
* @param oldIndex - Current index
* @param newIndex - Target index
*/
move(oldIndex: number, newIndex: number): void;
}
/**
* View repeater strategy that disposes views
* @template T Type of data items
* @template R Type of repeated views
* @template C Type of context
*/
class DisposeViewRepeaterStrategy<T, R, C> implements ViewRepeater<T, R, C> {
/**
* Apply changes by disposing old views
* @param changes - Collection changes
*/
applyChanges(changes: IterableChanges<T>): void;
/**
* Detach and dispose view at index
* @param index - Index to detach
*/
detach(index: number): R;
/**
* Move view between indices
* @param oldIndex - Current index
* @param newIndex - Target index
*/
move(oldIndex: number, newIndex: number): void;
}Utilities for working with tree data in flat or nested formats.
/**
* Base tree control for managing tree data and expansion state
* @template T Type of tree nodes
* @template K Type of tree node keys
*/
abstract class BaseTreeControl<T, K = T> {
/**
* Data nodes in the tree
*/
dataNodes: T[];
/**
* Selection model for tracking expanded nodes
*/
expansionModel: SelectionModel<K>;
/**
* Get the level of a data node
*/
getLevel: (dataNode: T) => number;
/**
* Check if a data node is expandable
*/
isExpandable: (dataNode: T) => boolean;
/**
* Expand a tree node
* @param dataNode - Node to expand
*/
expand(dataNode: T): void;
/**
* Collapse a tree node
* @param dataNode - Node to collapse
*/
collapse(dataNode: T): void;
/**
* Toggle the expansion state of a node
* @param dataNode - Node to toggle
*/
toggle(dataNode: T): void;
/**
* Expand all expandable nodes
*/
expandAll(): void;
/**
* Collapse all nodes
*/
collapseAll(): void;
/**
* Check if a node is expanded
* @param dataNode - Node to check
* @returns True if expanded
*/
isExpanded(dataNode: T): boolean;
/**
* Get descendants of a node
* @param dataNode - Parent node
* @returns Array of descendant nodes
*/
getDescendants(dataNode: T): T[];
/**
* Expand all descendants of a node
* @param dataNode - Parent node
*/
expandDescendants(dataNode: T): void;
/**
* Collapse all descendants of a node
* @param dataNode - Parent node
*/
collapseDescendants(dataNode: T): void;
}
/**
* Tree control for flat tree data
* @template T Type of tree nodes
* @template K Type of tree node keys
*/
class FlatTreeControl<T, K = T> extends BaseTreeControl<T, K> {
/**
* Create a flat tree control
* @param getLevel - Function to get node level
* @param isExpandable - Function to check if node is expandable
* @param options - Optional configuration
*/
constructor(
getLevel: (dataNode: T) => number,
isExpandable: (dataNode: T) => boolean,
options?: FlatTreeControlOptions<T, K>
);
}
/**
* Tree control for nested tree data
* @template T Type of tree nodes
* @template K Type of tree node keys
*/
class NestedTreeControl<T, K = T> extends BaseTreeControl<T, K> {
/**
* Create a nested tree control
* @param getChildren - Function to get child nodes
* @param options - Optional configuration
*/
constructor(
getChildren: (dataNode: T) => Observable<T[]> | T[] | undefined | null,
options?: NestedTreeControlOptions<T, K>
);
}
/**
* Options for flat tree control
*/
interface FlatTreeControlOptions<T, K> {
trackBy?: (dataNode: T) => K;
}
/**
* Options for nested tree control
*/
interface NestedTreeControlOptions<T, K> {
trackBy?: (dataNode: T) => K;
}import { SelectionModel } from '@angular/cdk/collections';
interface ListItem {
id: number;
name: string;
}
@Component({
selector: 'app-multi-select-list',
template: `
<div class="list-container">
<div class="list-header">
<label>
<input
type="checkbox"
[checked]="isAllSelected()"
[indeterminate]="isIndeterminate()"
(change)="toggleAll()">
Select All
</label>
</div>
<div *ngFor="let item of items" class="list-item">
<label>
<input
type="checkbox"
[checked]="selection.isSelected(item)"
(change)="selection.toggle(item)">
{{ item.name }}
</label>
</div>
<div class="list-footer">
Selected: {{ selection.selected.length }} / {{ items.length }}
</div>
</div>
`
})
export class MultiSelectListComponent {
items: ListItem[] = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' }
];
selection = new SelectionModel<ListItem>(true, []);
isAllSelected(): boolean {
return this.selection.selected.length === this.items.length;
}
isIndeterminate(): boolean {
return this.selection.selected.length > 0 && !this.isAllSelected();
}
toggleAll(): void {
if (this.isAllSelected()) {
this.selection.clear();
} else {
this.selection.select(...this.items);
}
}
}import { DataSource, CollectionViewer } from '@angular/cdk/collections';
import { BehaviorSubject, Observable } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
class UserDataSource extends DataSource<User> {
private dataSubject = new BehaviorSubject<User[]>([]);
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSubject.asObservable();
constructor(private userService: UserService) {
super();
}
connect(collectionViewer: CollectionViewer): Observable<User[]> {
return this.dataSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.dataSubject.complete();
this.loadingSubject.complete();
}
loadUsers(filter = '', sortDirection = 'asc', pageIndex = 0, pageSize = 10): void {
this.loadingSubject.next(true);
this.userService.getUsers(filter, sortDirection, pageIndex, pageSize)
.subscribe(users => {
this.dataSubject.next(users);
this.loadingSubject.next(false);
});
}
}
@Component({
selector: 'app-user-table',
template: `
<div *ngIf="dataSource.loading$ | async">Loading...</div>
<div *ngFor="let user of dataSource.connect() | async">
{{ user.name }} - {{ user.email }}
</div>
`
})
export class UserTableComponent implements OnInit {
dataSource: UserDataSource;
constructor(private userService: UserService) {
this.dataSource = new UserDataSource(userService);
}
ngOnInit(): void {
this.dataSource.loadUsers();
}
}