loading

Watch child-list changes

ObserveMutations

Capture the element with the ref directive and call ObserveMutations with the IJSRuntime and a handler. With no options it defaults to watching added and removed nodes across the whole subtree - the most common case. Start observing, then add and remove items to see childList records arrive.

Razor
@inject IJSRuntime js

<div @ref="container">...</div>

@code {
    private ElementReference container;
    private ButilSubscription? subscription;

    private async Task Start()
    {
        // defaults to ChildList = true, Subtree = true
        subscription = await container.ObserveMutations(js, records =>
        {
            foreach (var record in records)
            {
                Console.WriteLine($"{record.Type}: +{record.AddedCount} / -{record.RemovedCount}");
            }
        });
    }
}
Live sample
Observed container - change count: 0
child-list output
Results will appear here when you interact with the samples.

Watch attributes

MutationObserverOptions.Attributes / AttributeOldValue / AttributeFilter

Set Attributes to true to receive a record for every attribute change on the target. AttributeOldValue includes the previous value in each record, and AttributeFilter narrows the watch to specific attribute names so unrelated changes stay silent.

C#
await container.ObserveMutations(js, records =>
{
    foreach (var record in records)
    {
        Console.WriteLine($"{record.AttributeName}: {record.OldValue} -> changed");
    }
}, new MutationObserverOptions
{
    Attributes = true,
    AttributeOldValue = true,
    AttributeFilter = ["data-state"],
});
Live sample
attributes output
Results will appear here when you interact with the samples.

Watch text changes

MutationObserverOptions.CharacterData / CharacterDataOldValue

CharacterData watches edits to text nodes; pair it with Subtree so text anywhere inside the container is covered, and CharacterDataOldValue to receive the previous text. Incrementing the counter below makes Blazor update a text node inside the observed container.

C#
await container.ObserveMutations(js, records =>
{
    foreach (var record in records)
    {
        Console.WriteLine($"text changed, was: {record.OldValue}");
    }
}, new MutationObserverOptions
{
    CharacterData = true,
    CharacterDataOldValue = true,
    Subtree = true,
});
Live sample
text changes output
Results will appear here when you interact with the samples.

Reading records

MutationRecord

DOM nodes cannot cross the interop boundary, so each MutationRecord is a flattened summary: the mutation type, the target's tag name and id, the attribute name and old value where applicable, and how many nodes were added or removed.

C#
await container.ObserveMutations(js, records =>
{
    foreach (var record in records)
    {
        var type = record.Type;          // "childList" | "attributes" | "characterData"
        var tag = record.TargetTagName;  // e.g. "DIV"
        var id = record.TargetId;        // element id, when present
        var attr = record.AttributeName; // attributes mutations only
        var old = record.OldValue;       // when *OldValue options are enabled
        var added = record.AddedCount;   // childList mutations only
        var removed = record.RemovedCount;
    }
});
Live sample
records output
Results will appear here when you interact with the samples.

Stopping

ButilSubscription

ObserveMutations returns a ButilSubscription; disposing it disconnects the observer and releases the interop reference. Dispose every observer you start - at the latest in your component's DisposeAsync, as this page does.

Razor
@implements IAsyncDisposable

private ButilSubscription? subscription;

public async ValueTask DisposeAsync()
{
    if (subscription is not null)
    {
        await subscription.DisposeAsync();
    }
}
Live sample
stopping output
Results will appear here when you interact with the samples.
Note:
Blazor renders count as mutations too The observer sees every DOM change inside the watched subtree - including the ones Blazor itself makes when it re-renders. That is exactly what powers the demos above, but when watching a region your own components render, expect records for Blazor's diff updates as well as for external changes.

API reference

Member
Signature
Description
ObserveMutations
Task<ButilSubscription> ObserveMutations(IJSRuntime js, Action<MutationRecord[]> handler, MutationObserverOptions? options = null)
Extension method on ElementReference. Starts observing and returns a disposable subscription. Null options default to ChildList and Subtree.
MutationObserverOptions.ChildList
bool ChildList { get; set; }
Watch for added or removed children.
MutationObserverOptions.Attributes
bool Attributes { get; set; }
Watch for attribute changes on the target.
MutationObserverOptions.CharacterData
bool CharacterData { get; set; }
Watch for character-data changes within the target.
MutationObserverOptions.Subtree
bool Subtree { get; set; }
Apply the chosen options to the entire subtree, not just the immediate target.
MutationObserverOptions.AttributeOldValue
bool AttributeOldValue { get; set; }
Include the previous attribute value in each record.
MutationObserverOptions.CharacterDataOldValue
bool CharacterDataOldValue { get; set; }
Include the previous character-data value in each record.
MutationObserverOptions.AttributeFilter
string[]? AttributeFilter { get; set; }
Optional whitelist of attribute names to watch. Null means all.
MutationRecord.Type
string Type { get; set; }
One of "attributes", "characterData" or "childList".
MutationRecord.TargetTagName
string TargetTagName { get; set; }
Tag name of the target node, or empty for non-element targets.
MutationRecord.TargetId
string? TargetId { get; set; }
Id of the target node when present.
MutationRecord.AttributeName
string? AttributeName { get; set; }
Attribute name (attributes mutations only).
MutationRecord.AttributeNamespace
string? AttributeNamespace { get; set; }
Attribute namespace (attributes mutations only).
MutationRecord.OldValue
string? OldValue { get; set; }
Previous value when the matching *OldValue option was enabled.
MutationRecord.AddedCount
int AddedCount { get; set; }
Number of nodes added (childList mutations only).
MutationRecord.RemovedCount
int RemovedCount { get; set; }
Number of nodes removed (childList mutations only).
ButilSubscription.Id
Guid Id { get; }
The internal listener id of the subscription.
ButilSubscription.DisposeAsync
ValueTask DisposeAsync()
Disconnects the observer and releases the interop reference. Idempotent.
An unhandled error has occurred. Reload 🗙