MutationObserver
Low-level utility for observing DOM tree changes using the MutationObserver API. Provides fine-grained control over observation lifecycle.
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:
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:
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:
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:
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
| Parameter | Type | Description |
|---|---|---|
target | MaybeElementSignal<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) => void | Callback function called when DOM mutations occur |
options | MutationObserverInitOptions | Required. 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'>:
| Option | Type | Default | Description |
|---|---|---|---|
childList | MaybeSignal<boolean> | - | Observe child node additions/removals |
attributes | MaybeSignal<boolean> | - | Observe attribute changes |
characterData | MaybeSignal<boolean> | - | Observe text node changes |
subtree | MaybeSignal<boolean> | - | Observe descendants |
attributeOldValue | MaybeSignal<boolean> | - | Include old attribute value in records |
characterDataOldValue | MaybeSignal<boolean> | - | Include old text value in records |
attributeFilter | MaybeSignal<string[]> | - | Filter specific attributes |
manualCleanup | boolean | false | If true, the effect requires manual cleanup. By default, the effect automatically registers itself for cleanup with the current DestroyRef |
debugName | string | - | Debug name for the effect (used in Angular DevTools) |
injector | Injector | - | 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:
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
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;Related
- ResizeObserver — Observe element size changes
- IntersectionObserver — Observe element intersection with viewport