MutationObserver

Low-level utility for observing DOM tree changes using the MutationObserver API. Provides fine-grained control over observation lifecycle.

Loading demo...

Usage

WARNING

For type safety, this utility accepts only Element rather than Node, even though the MutationObserver API works with any node. Accessing text or comment nodes typically requires direct DOM manipulation, which we aim to avoid — hence, only element is allowed.

Single element observation

Observe a single element by passing it as the first parameter:

angular-ts
import { Component, inject, ElementRef } from '@angular/core';
import { mutationObserver } from '@signality/core';

@Component({ 
  template: `<div>Content that may change</div>`,
})
export class Mutation {
  readonly el = inject(ElementRef);
  readonly mo = mutationObserver(this.el, console.log, { childList: true }); 
}

Multiple elements observation

Observe multiple elements by passing an array:

angular-ts
import { Component, viewChild, ElementRef } from '@angular/core';
import { mutationObserver } from '@signality/core';

@Component({
  template: `
    <div #child1>Child 1</div>
    <div #child2>Child 2</div>
  `,
})
export class MultipleMutation {
  readonly child1 = viewChild<ElementRef>('child1');
  readonly child2 = viewChild<ElementRef>('child2');
  
  readonly mo = mutationObserver(
    [this.child1, this.child2], 
    mutations => {
      for (const mutation of mutations) {
        console.log('Mutation:', mutation.type);
      }
    },
    { childList: true }
  );
}

With reactive options

The options can be reactive signals:

angular-ts
import { Component, viewChild, ElementRef, signal } from '@angular/core';
import { mutationObserver } from '@signality/core';

@Component({
  template: `<div #box>Content</div>`,
})
export class ReactiveOptionsMutation {
  readonly box = viewChild<ElementRef>('box');
  readonly watchChildren = input(true); 
  
  readonly mo = mutationObserver(
    this.box,
    mutations => {
      console.log('Mutations:', mutations);
    },
    { childList: this.watchChildren } 
  );
}

Manual cleanup

Observers are automatically disconnected after the view is destroyed (see Automatic cleanup). However, the returned MutationObserverRef can be destroyed to stop observation manually:

angular-ts
import { Component, viewChild, ElementRef } from '@angular/core';
import { mutationObserver } from '@signality/core';

@Component({
  template: `<div #box>Content</div>`,
})
export class ManualCleanup {
  readonly box = viewChild<ElementRef>('box');
  readonly mo = mutationObserver(this.box, console.log, { childList: true });

  manualCleanup() {
    // Stop observing
    this.mo.destroy();
  }
}

Parameters

ParameterTypeDescription
targetMaybeElementSignal<Element> | MaybeElementSignal<Element>[]Element(s) to observe. Can be a single element or array. Can be:
- A plain Element or ElementRef<Element>
- A Signal<Element> or Signal<ElementRef<Element>>
- undefined (observation is skipped)
- An array of any of the above
callback(mutations: readonly MutationRecord[], observer: MutationObserver) => voidCallback function called when DOM mutations occur
optionsMutationObserverInitOptionsRequired. Configuration for the observer (see Options below). At least one of childList, attributes, or characterData must be specified.

Options

The MutationObserverInitOptions extends Omit<CreateEffectOptions, 'allowSignalWrites'>:

OptionTypeDefaultDescription
childListMaybeSignal<boolean>-Observe child node additions/removals
attributesMaybeSignal<boolean>-Observe attribute changes
characterDataMaybeSignal<boolean>-Observe text node changes
subtreeMaybeSignal<boolean>-Observe descendants
attributeOldValueMaybeSignal<boolean>-Include old attribute value in records
characterDataOldValueMaybeSignal<boolean>-Include old text value in records
attributeFilterMaybeSignal<string[]>-Filter specific attributes
manualCleanupbooleanfalseIf true, the effect requires manual cleanup. By default, the effect automatically registers itself for cleanup with the current DestroyRef
debugNamestring-Debug name for the effect (used in Angular DevTools)
injectorInjector-Optional injector for DI context

Return Value

Returns a MutationObserverRef with a destroy() method to stop observing the element(s).

Examples

Conditional observation

Observe elements conditionally:

angular-ts
import { Component, viewChild, ElementRef, computed } from '@angular/core';
import { mutationObserver } from '@signality/core';

@Component({
  template: `
    <div #box>Box</div>
    <button (click)="enabled.set(!enabled())">
      {{ enabled() ? 'Disable' : 'Enable' }} Observation
    </button>
  `,
})
export class ConditionalMutation {
  readonly box = viewChild<ElementRef>('box');
  readonly enabled = signal(true);
  
  readonly observer = mutationObserver(
    computed(() => (this.enabled() ? this.box() : undefined)), 
    mutations => {
      console.log('Mutations:', mutations);
    },
    { childList: true }
  );
}

SSR Compatibility

On the server, mutationObserver returns a no-op MutationObserverRef that safely handles destroy() calls without creating actual observers.

Type Definitions

typescript
interface MutationObserverInitOptions extends Omit<CreateEffectOptions, 'allowSignalWrites'> {
  readonly childList?: MaybeSignal<boolean>;
  readonly attributes?: MaybeSignal<boolean>;
  readonly characterData?: MaybeSignal<boolean>;
  readonly subtree?: MaybeSignal<boolean>;
  readonly attributeOldValue?: MaybeSignal<boolean>;
  readonly characterDataOldValue?: MaybeSignal<boolean>;
  readonly attributeFilter?: MaybeSignal<string[]>;
}

interface MutationObserverRef {
  readonly destroy: () => void;
}

function mutationObserver(
  target: MaybeElementSignal<Element> | MaybeElementSignal<Element>[],
  callback: (mutations: readonly MutationRecord[], observer: MutationObserver) => void,
  options: MutationObserverInitOptions
): MutationObserverRef;
Edit this page on GitHub Last updated: Mar 19, 2026, 23:28:23