The Angular CDK Observers module provides utilities for detecting content changes in DOM elements using the MutationObserver API with Angular integration and filtering capabilities.
Service for observing DOM content changes with support for debouncing and filtering.
/**
* Service for observing DOM content changes
*/
class ContentObserver implements OnDestroy {
/**
* Observe content changes on an element
* @param element - Element to observe
* @returns Observable that emits mutation records
*/
observe(element: Element): Observable<MutationRecord[]>;
/**
* Observe content changes on an element with options
* @param element - Element to observe
* @param options - MutationObserver options
* @returns Observable that emits mutation records
*/
observe(element: Element, options: MutationObserverInit): Observable<MutationRecord[]>;
/**
* Stop observing an element
* @param element - Element to stop observing
*/
unobserve(element: Element): void;
/**
* Destroy the content observer and clean up all observations
*/
ngOnDestroy(): void;
}
/**
* Factory for creating MutationObserver instances
*/
class MutationObserverFactory {
/**
* Create a new MutationObserver instance
* @param callback - Callback function to handle mutations
* @returns MutationObserver instance or null if not supported
*/
create(callback: MutationCallback): MutationObserver | null;
}Usage Example:
import { Component, ElementRef, ViewChild, OnDestroy } from '@angular/core';
import { ContentObserver } from '@angular/cdk/observers';
import { Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Component({
template: `
<div class="container">
<div #observedElement class="observed-content">
<p>This content is being observed for changes.</p>
<div id="dynamic-content">
<!-- Dynamic content will be added here -->
</div>
</div>
<div class="controls">
<button (click)="addContent()">Add Content</button>
<button (click)="removeContent()">Remove Content</button>
<button (click)="modifyContent()">Modify Content</button>
<button (click)="addAttribute()">Add Attribute</button>
</div>
<div class="mutation-log">
<h3>Mutation Log:</h3>
<ul>
<li *ngFor="let mutation of mutations; trackBy: trackMutation">
{{ mutation.type }}: {{ getMutationDescription(mutation) }}
</li>
</ul>
</div>
</div>
`,
styles: [`
.observed-content {
border: 2px solid #2196f3;
padding: 16px;
margin-bottom: 16px;
min-height: 100px;
}
.controls button {
margin-right: 8px;
margin-bottom: 8px;
padding: 8px 16px;
}
.mutation-log {
background: #f5f5f5;
padding: 16px;
max-height: 200px;
overflow-y: auto;
}
.mutation-log ul {
margin: 0;
padding-left: 20px;
}
`]
})
export class ContentObserverExample implements OnDestroy {
@ViewChild('observedElement') observedElement!: ElementRef;
mutations: Array<MutationRecord & { timestamp: number }> = [];
private subscription?: Subscription;
private contentCounter = 0;
constructor(private contentObserver: ContentObserver) {}
ngAfterViewInit() {
// Observe all types of mutations with debouncing
this.subscription = this.contentObserver
.observe(this.observedElement.nativeElement, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
})
.pipe(debounceTime(100)) // Debounce rapid changes
.subscribe(mutations => {
mutations.forEach(mutation => {
this.mutations.unshift({
...mutation,
timestamp: Date.now()
});
});
// Keep only last 20 mutations
this.mutations = this.mutations.slice(0, 20);
});
}
addContent() {
const dynamicContent = this.observedElement.nativeElement.querySelector('#dynamic-content');
const newElement = document.createElement('p');
newElement.textContent = `Dynamic content ${++this.contentCounter}`;
newElement.className = 'dynamic-item';
dynamicContent.appendChild(newElement);
}
removeContent() {
const dynamicContent = this.observedElement.nativeElement.querySelector('#dynamic-content');
const lastChild = dynamicContent.lastElementChild;
if (lastChild && lastChild.classList.contains('dynamic-item')) {
lastChild.remove();
}
}
modifyContent() {
const dynamicItems = this.observedElement.nativeElement.querySelectorAll('.dynamic-item');
if (dynamicItems.length > 0) {
const randomItem = dynamicItems[Math.floor(Math.random() * dynamicItems.length)];
randomItem.textContent = `Modified at ${new Date().toLocaleTimeString()}`;
}
}
addAttribute() {
const dynamicItems = this.observedElement.nativeElement.querySelectorAll('.dynamic-item');
if (dynamicItems.length > 0) {
const randomItem = dynamicItems[Math.floor(Math.random() * dynamicItems.length)];
randomItem.setAttribute('data-modified', Date.now().toString());
}
}
getMutationDescription(mutation: MutationRecord): string {
switch (mutation.type) {
case 'childList':
const added = mutation.addedNodes.length;
const removed = mutation.removedNodes.length;
return `${added} added, ${removed} removed`;
case 'attributes':
return `${mutation.attributeName} changed`;
case 'characterData':
return `text changed from "${mutation.oldValue}" to "${mutation.target.textContent}"`;
default:
return 'unknown change';
}
}
trackMutation(index: number, mutation: any): number {
return mutation.timestamp;
}
ngOnDestroy() {
this.subscription?.unsubscribe();
}
}Directive for observing content changes directly in templates.
/**
* Directive for observing content changes on elements
*/
class CdkObserveContent implements AfterContentInit, OnDestroy {
/**
* Event emitted when content changes are detected
*/
readonly cdkObserveContent: EventEmitter<MutationRecord[]>;
/**
* Whether content observation is disabled
*/
disabled: boolean;
/**
* Debounce time in milliseconds for change detection
*/
debounce: number;
}Usage Example:
import { Component } from '@angular/core';
@Component({
template: `
<div
cdkObserveContent
(cdkObserveContent)="onContentChange($event)"
[debounce]="debounceTime"
[disabled]="observationDisabled"
class="observed-container">
<h3>Observed Container</h3>
<div class="content-area">
<p *ngFor="let item of items; trackBy: trackItem">
{{ item.text }}
</p>
</div>
<div class="controls">
<button (click)="addItem()">Add Item</button>
<button (click)="removeItem()">Remove Item</button>
<button (click)="toggleObservation()">
{{ observationDisabled ? 'Enable' : 'Disable' }} Observation
</button>
</div>
</div>
<div class="settings">
<label>
Debounce Time (ms):
<input type="number" [(ngModel)]="debounceTime" min="0" max="1000">
</label>
</div>
<div class="change-log">
<h4>Content Changes ({{ changeCount }}):</h4>
<ul>
<li *ngFor="let change of recentChanges">
{{ change.type }} at {{ change.timestamp | date:'HH:mm:ss.SSS' }}
</li>
</ul>
</div>
`,
styles: [`
.observed-container {
border: 2px dashed #ff9800;
padding: 16px;
margin-bottom: 16px;
}
.content-area {
background: #f9f9f9;
padding: 8px;
margin: 8px 0;
min-height: 50px;
}
.controls button {
margin-right: 8px;
}
.settings {
margin-bottom: 16px;
}
.settings label {
display: flex;
align-items: center;
gap: 8px;
}
.change-log {
background: #f0f0f0;
padding: 12px;
max-height: 150px;
overflow-y: auto;
}
`]
})
export class ObserveContentExample {
items = [
{ id: 1, text: 'Initial item 1' },
{ id: 2, text: 'Initial item 2' }
];
observationDisabled = false;
debounceTime = 300;
changeCount = 0;
recentChanges: Array<{ type: string; timestamp: Date }> = [];
private nextId = 3;
onContentChange(mutations: MutationRecord[]) {
this.changeCount++;
mutations.forEach(mutation => {
this.recentChanges.unshift({
type: this.getMutationType(mutation),
timestamp: new Date()
});
});
// Keep only recent changes
this.recentChanges = this.recentChanges.slice(0, 10);
}
addItem() {
this.items.push({
id: this.nextId++,
text: `New item ${this.nextId - 1}`
});
}
removeItem() {
if (this.items.length > 0) {
this.items.pop();
}
}
toggleObservation() {
this.observationDisabled = !this.observationDisabled;
}
private getMutationType(mutation: MutationRecord): string {
switch (mutation.type) {
case 'childList':
return `Child nodes changed (${mutation.addedNodes.length} added, ${mutation.removedNodes.length} removed)`;
case 'attributes':
return `Attribute "${mutation.attributeName}" changed`;
case 'characterData':
return 'Text content changed';
default:
return 'Unknown change';
}
}
trackItem(index: number, item: any): number {
return item.id;
}
}/**
* Angular module that includes all CDK observer directives and services
*/
class ObserversModule {}