Observers
How to use the observers and async browser APIs of the bit Butil?
Usage
These reactive and async browser APIs are spread across a few classes and element extension methods.
Inject the ones you need (the element observers take the injected IJSRuntime) and use them like this:
@inject Bit.Butil.Performance performance
@inject Bit.Butil.StorageManager storageManager
@inject Bit.Butil.NetworkInformation networkInformation
@inject Bit.Butil.BroadcastChannel broadcastChannel
@inject Bit.Butil.IndexedDb indexedDb
@inject IJSRuntime js
@code {
var sub = await element.ObserveIntersection(js, entries => { /* ... */ });
await using var db = await indexedDb.Open("my-db", 1, [new IndexedDbStoreSchema { Name = "items", KeyPath = "id" }]);
}Element observers
The ObserveIntersection, ObserveResize and ObserveMutations extension methods wire the
matching browser observers onto an ElementReference. Each returns a ButilSubscription you
dispose to stop observing. The samples below all watch this shared target element:
ObserveIntersection:
Fires when the target enters or leaves the viewport, reporting the intersection ratio and visibility (MDN).
ObserveResize:
Reports size changes when the target is resized, surfacing the new content-rect dimensions (MDN).
ObserveMutations:
Logs DOM changes on the target element, such as attribute mutations (MDN).
target
ObserveIntersection:
Fires when the target enters or leaves the viewport, reporting the intersection ratio and visibility (MDN).
@inject IJSRuntime js
<div @ref="target">target</div>
<BitButton OnClick="StartIntersection">Observe</BitButton>
<BitButton OnClick="StopIntersection">Stop</BitButton>
<div>@intersectionResult</div>
@code {
private ElementReference target;
private ButilSubscription? intersectionSub;
private string? intersectionResult;
private async Task StartIntersection()
{
await StopIntersection();
intersectionSub = await target.ObserveIntersection(js, entries =>
{
if (entries.Length > 0)
{
intersectionResult = $"ratio={entries[0].IntersectionRatio:F2}, visible={entries[0].IsIntersecting}";
InvokeAsync(StateHasChanged);
}
});
}
private async Task StopIntersection()
{
if (intersectionSub is null) return;
await intersectionSub.DisposeAsync();
intersectionSub = null;
}
}ObserveResize:
Reports size changes when the target is resized, surfacing the new content-rect dimensions (MDN).
@inject IJSRuntime js
<div @ref="target">target</div>
<BitButton OnClick="StartResize">Observe</BitButton>
<BitButton OnClick="GrowTarget">Grow target</BitButton>
<BitButton OnClick="StopResize">Stop</BitButton>
<div>@resizeResult</div>
@code {
private int targetWidth = 120;
private ElementReference target;
private ButilSubscription? resizeSub;
private string? resizeResult;
private async Task StartResize()
{
await StopResize();
resizeSub = await target.ObserveResize(js, entries =>
{
if (entries.Length > 0)
{
var rect = entries[0].ContentRect;
resizeResult = $"{rect?.Width:F0}x{rect?.Height:F0}";
InvokeAsync(StateHasChanged);
}
});
}
private async Task GrowTarget()
{
targetWidth += 40;
await target.SetAttribute("style", $"width:{targetWidth}px;height:48px");
}
private async Task StopResize()
{
if (resizeSub is null) return;
await resizeSub.DisposeAsync();
resizeSub = null;
}
}ObserveMutations:
Logs DOM changes on the target element, such as attribute mutations (MDN).
@inject IJSRuntime js
<div @ref="target">target</div>
<BitButton OnClick="StartMutation">Observe</BitButton>
<BitButton OnClick="MutateTarget">Mutate attribute</BitButton>
<div>@mutationResult</div>
@code {
private ElementReference target;
private ButilSubscription? mutationSub;
private string? mutationResult;
private async Task StartMutation()
{
await StopMutation();
mutationSub = await target.ObserveMutations(js, records =>
{
if (records.Length > 0)
{
mutationResult = $"{records[0].Type} on {records[0].TargetId ?? "target"}";
InvokeAsync(StateHasChanged);
}
}, new MutationObserverOptions { Attributes = true });
}
private async Task MutateTarget()
{
await target.SetAttribute("data-butil", Guid.NewGuid().ToString("N")[..8]);
}
private async Task StopMutation()
{
if (mutationSub is null) return;
await mutationSub.DisposeAsync();
mutationSub = null;
}
}Performance
The Performance class wraps the
Performance
timing and marker API.
Mark / Measure / GetEntries:
Adds named marks to the timeline, measures the elapsed time between them, and reads back the recorded entries (MDN).
SubscribeObserver:
Subscribes a PerformanceObserver to one or more entry types and reports each batch as it arrives (MDN).
Mark / Measure / GetEntries:
Adds named marks to the timeline, measures the elapsed time between them, and reads back the recorded entries (MDN).
@inject Bit.Butil.Performance performance
<BitButton OnClick="PerfMarkMeasure">Mark + Measure</BitButton>
<div>@perfMeasureResult</div>
@code {
private string? perfMeasureResult;
private async Task PerfMarkMeasure()
{
await performance.Mark("butil-demo-a");
await Task.Delay(50);
await performance.Mark("butil-demo-b");
await performance.Measure("butil-demo-measure", "butil-demo-a", "butil-demo-b");
var entries = await performance.GetEntries("butil-demo-measure", "measure");
perfMeasureResult = entries.Length > 0 ? entries[0].ToString() : "(no entry)";
}
}SubscribeObserver:
Subscribes a PerformanceObserver to one or more entry types and reports each batch as it arrives (MDN).
@inject Bit.Butil.Performance performance
<BitButton OnClick="PerfObserver">SubscribeObserver</BitButton>
<div>@perfObserverResult</div>
@code {
private ButilSubscription? perfObserverSub;
private string? perfObserverResult;
private async Task PerfObserver()
{
await StopPerfObserver();
perfObserverSub = await performance.SubscribeObserver(["mark"], entries =>
{
if (entries.Length > 0)
{
perfObserverResult = entries[0].ToString();
InvokeAsync(StateHasChanged);
}
}, buffered: false);
await performance.Mark("butil-demo-observed");
}
private async Task StopPerfObserver()
{
if (perfObserverSub is null) return;
await perfObserverSub.DisposeAsync();
perfObserverSub = null;
}
}StorageManager & NetworkInformation
The StorageManager class wraps
navigator.storage
and the NetworkInformation class wraps the
Network Information API.
Estimate:
Reports an estimate of the storage quota and current usage for the origin (MDN).
GetStatus:
Returns the online flag, effective connection type, downlink and round-trip-time estimates (MDN).
Estimate:
Reports an estimate of the storage quota and current usage for the origin (MDN).
@inject Bit.Butil.StorageManager storageManager
<BitButton OnClick="StorageEstimate">Estimate</BitButton>
<div>@storageResult</div>
@code {
private string? storageResult;
private async Task StorageEstimate()
{
var est = await storageManager.Estimate();
storageResult = $"quota={est.Quota}, usage={est.Usage}";
}
}GetStatus:
Returns the online flag, effective connection type, downlink and round-trip-time estimates (MDN).
@inject Bit.Butil.NetworkInformation networkInformation
<BitButton OnClick="NetworkStatus">GetStatus</BitButton>
<div>@networkResult</div>
@code {
private string? networkResult;
private async Task NetworkStatus()
{
var status = await networkInformation.GetStatus();
networkResult = $"online={status.Online}, type={status.EffectiveType}, downlink={status.Downlink}, rtt={status.Rtt}";
}
}BroadcastChannel
The BroadcastChannel class wraps the
BroadcastChannel
API for cross-tab pub/sub on the same origin.
Subscribe / Post:
Subscribes to a named channel and posts messages to every other listener on that channel in the same origin (the sender does not receive its own message). Open a second tab to see messages arrive (MDN).
Subscribe / Post:
Subscribes to a named channel and posts messages to every other listener on that channel in the same origin (the sender does not receive its own message). Open a second tab to see messages arrive (MDN).
@inject Bit.Butil.BroadcastChannel broadcastChannel
<BitButton OnClick="BroadcastSubscribe">Subscribe</BitButton>
<BitButton OnClick="BroadcastPost">Post</BitButton>
<div>@broadcastResult</div>
@code {
private ButilSubscription? broadcastSub;
private string? broadcastResult;
private async Task BroadcastSubscribe()
{
await StopBroadcast();
broadcastSub = await broadcastChannel.Subscribe("butil-demo-channel", msg =>
{
broadcastResult = $"received -> {msg}";
InvokeAsync(StateHasChanged);
});
}
private async Task BroadcastPost()
{
await broadcastChannel.Post("butil-demo-channel", new { text = "hello from this tab", at = DateTime.Now });
}
private async Task StopBroadcast()
{
if (broadcastSub is null) return;
await broadcastSub.DisposeAsync();
broadcastSub = null;
}
}IndexedDB
The IndexedDb class is a lightweight wrapper over
IndexedDB.
Open / Put / Get:
Opens (and upgrades if needed) a database, stores a record and reads it back by key (MDN).
Open / Put / Get:
Opens (and upgrades if needed) a database, stores a record and reads it back by key (MDN).
@inject Bit.Butil.IndexedDb indexedDb
<BitButton OnClick="IndexedDbRoundTrip">Round-trip</BitButton>
<div>@indexedDbResult</div>
@code {
private string? indexedDbResult;
private class DemoIdbItem
{
public string Id { get; set; } = "";
public string Value { get; set; } = "";
}
private async Task IndexedDbRoundTrip()
{
await using var db = await indexedDb.Open("butil-demo-db", 1,
[
new IndexedDbStoreSchema { Name = "items", KeyPath = "id" }
]);
var item = new DemoIdbItem { Id = "demo-1", Value = "stored at " + DateTime.Now.ToString("HH:mm:ss") };
await db.Put("items", item);
var read = await db.Get<DemoIdbItem>("items", "demo-1");
indexedDbResult = read?.Value ?? "(null)";
}
}