Skip to content

Mutation Observer

Angular wrapper for the native MutationObserver API with RxJS integration, making it easy to observe DOM changes reactively.

Installation

bash
npm install @dasch-ng/mutation-observer

Features

  • Angular Integration: Seamlessly integrates MutationObserver with Angular's lifecycle
  • Directive API: Simple observeMutation directive for declarative DOM observation
  • RxJS Integration: Observable-based API for reactive programming patterns
  • Service API: Injectable service for programmatic DOM mutation observation
  • Type-safe: Full TypeScript support with proper MutationObserver typings
  • Automatic Cleanup: Observers are automatically disconnected when components are destroyed

ObserveMutationDirective

Directive for observing DOM mutations on the host element.

Use case: Declaratively observe DOM changes in templates without manual observer setup.

Example

typescript
import { Component } from '@angular/core';
import { ObserveMutationDirective } from '@dasch-ng/mutation-observer';

@Component({
  selector: 'app-dynamic-content',
  imports: [ObserveMutationDirective],
  template: `
    <div (records)="onMutation($event)">
      <p>Content that may change</p>
      <button (click)="addContent()">Add Content</button>
    </div>
  `,
})
export class DynamicContentComponent {
  onMutation(mutation: MutationRecord) {
    console.log('DOM mutation detected:', mutation);
    console.log('Type:', mutation.type);
    console.log('Target:', mutation.target);
    console.log('Added nodes:', mutation.addedNodes);
    console.log('Removed nodes:', mutation.removedNodes);
  }

  addContent() {
    // Add dynamic content that will trigger mutation observation
  }
}

Output:

  • records: Emits when DOM mutations are detected on the element

View API Documentation →


MutationObserverService

Service for programmatic DOM mutation observation with RxJS integration.

Use case: Observe DOM changes programmatically with full control over observation options.

Example

typescript
import { Component, ElementRef, inject, viewChild, effect } from '@angular/core';
import { MutationObserverService } from '@dasch-ng/mutation-observer';

@Component({
  selector: 'app-content-tracker',
  template: `
    <div #content>
      <p>Dynamic content area</p>
    </div>
  `,
})
export class ContentTrackerComponent {
  private mutationObserver = inject(MutationObserverService);

  contentElement = viewChild.required<ElementRef>('content');

  constructor() {
    effect(() => {
      const element = this.contentElement().nativeElement;

      this.mutationObserver
        .observe(element, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeOldValue: true,
        })
        .subscribe((mutation) => {
          console.log('Mutation detected:', mutation);
        });
    });
  }
}

Methods:

  • observe(element, options?): Start observing an element for mutations (returns Observable)

Parameters:

  • element: HTMLElement to observe
  • options: MutationObserverInit configuration object

View API Documentation →


MutationObserverInit Options

Configure what types of mutations to observe:

OptionTypeDescription
childListbooleanObserve additions/removals of child nodes
attributesbooleanObserve changes to element attributes
characterDatabooleanObserve changes to text content
subtreebooleanObserve descendants in addition to target
attributeOldValuebooleanRecord previous attribute value
characterDataOldValuebooleanRecord previous text content
attributeFilterstring[]Specific attributes to observe

Example Configuration

typescript
const options: MutationObserverInit = {
  childList: true, // Watch for added/removed children
  subtree: true, // Watch entire subtree
  attributes: true, // Watch attribute changes
  attributeFilter: ['class', 'data-state'], // Only these attributes
  attributeOldValue: true, // Keep old attribute values
};

this.mutationObserver.observe(element, options).subscribe((mutation) => {
  // Handle mutation
});

Common Patterns

Observing Dynamic Lists

typescript
import { Component, inject, viewChild, ElementRef, effect } from '@angular/core';
import { MutationObserverService } from '@dasch-ng/mutation-observer';

@Component({
  selector: 'app-todo-list',
  template: `
    <ul #list>
      @for (item of items; track item.id) {
        <li>{{ item.text }}</li>
      }
    </ul>
    <button (click)="addItem()">Add Item</button>
  `,
})
export class TodoListComponent {
  private mutationObserver = inject(MutationObserverService);

  listElement = viewChild.required<ElementRef>('list');
  items = [
    { id: 1, text: 'First item' },
    { id: 2, text: 'Second item' },
  ];

  constructor() {
    effect(() => {
      const element = this.listElement().nativeElement;

      this.mutationObserver
        .observe(element, {
          childList: true,
          subtree: false,
        })
        .subscribe((mutation) => {
          console.log(`Items changed: ${mutation.addedNodes.length} added, ${mutation.removedNodes.length} removed`);
        });
    });
  }

  addItem() {
    const id = this.items.length + 1;
    this.items.push({ id, text: `Item ${id}` });
  }
}

Tracking Attribute Changes

typescript
import { Component, inject, viewChild, ElementRef, effect } from '@angular/core';
import { MutationObserverService } from '@dasch-ng/mutation-observer';

@Component({
  selector: 'app-state-tracker',
  template: `
    <div #container [attr.data-state]="state" [class.active]="isActive">
      <p>State: {{ state }}</p>
    </div>
    <button (click)="toggleState()">Toggle State</button>
  `,
})
export class StateTrackerComponent {
  private mutationObserver = inject(MutationObserverService);

  containerElement = viewChild.required<ElementRef>('container');
  state = 'idle';
  isActive = false;

  constructor() {
    effect(() => {
      const element = this.containerElement().nativeElement;

      this.mutationObserver
        .observe(element, {
          attributes: true,
          attributeFilter: ['data-state', 'class'],
          attributeOldValue: true,
        })
        .subscribe((mutation) => {
          if (mutation.type === 'attributes') {
            console.log(`Attribute "${mutation.attributeName}" changed`);
            console.log(`Old value: ${mutation.oldValue}`);
            console.log(`New value: ${(mutation.target as Element).getAttribute(mutation.attributeName!)}`);
          }
        });
    });
  }

  toggleState() {
    this.state = this.state === 'idle' ? 'active' : 'idle';
    this.isActive = !this.isActive;
  }
}

Content Change Detection

typescript
import { Component } from '@angular/core';
import { ObserveMutationDirective } from '@dasch-ng/mutation-observer';

@Component({
  selector: 'app-editor',
  imports: [ObserveMutationDirective],
  template: `
    <div contenteditable="true" (records)="onContentChange($event)" class="editor">Type here...</div>
    <p>Character count: {{ characterCount }}</p>
  `,
})
export class EditorComponent {
  characterCount = 0;

  onContentChange(mutation: MutationRecord) {
    if (mutation.type === 'characterData' || mutation.type === 'childList') {
      const element = mutation.target as HTMLElement;
      this.characterCount = element.textContent?.length || 0;
    }
  }
}

Lazy Loading Detection

typescript
import { Component, inject, viewChild, ElementRef, effect } from '@angular/core';
import { MutationObserverService } from '@dasch-ng/mutation-observer';

@Component({
  selector: 'app-image-gallery',
  template: `
    <div #gallery class="gallery">
      <!-- Images loaded dynamically -->
    </div>
  `,
})
export class ImageGalleryComponent {
  private mutationObserver = inject(MutationObserverService);

  galleryElement = viewChild.required<ElementRef>('gallery');
  loadedImages = 0;

  constructor() {
    effect(() => {
      const element = this.galleryElement().nativeElement;

      this.mutationObserver
        .observe(element, {
          childList: true,
          subtree: true,
        })
        .subscribe((mutation) => {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeName === 'IMG') {
              this.loadedImages++;
              console.log(`Image loaded. Total: ${this.loadedImages}`);
            }
          });
        });
    });
  }
}

Mutation Record

Each mutation event provides a MutationRecord with the following properties:

PropertyTypeDescription
typestringType of mutation: "attributes", "characterData", or "childList"
targetNodeThe node affected by the mutation
addedNodesNodeListNodes added (for childList mutations)
removedNodesNodeListNodes removed (for childList mutations)
previousSiblingNode | nullPrevious sibling of added/removed nodes
nextSiblingNode | nullNext sibling of added/removed nodes
attributeNamestring | nullName of changed attribute
attributeNamespacestring | nullNamespace of changed attribute
oldValuestring | nullPrevious value (if recordOldValue was true)

API Reference

For complete API documentation with all parameters and return types, see the API Reference.

Source Code

View the source code on GitHub.

Contributing

Found a bug or want to contribute? Check out the contributing guidelines.

Released under the MIT License.