Dom
Finding and creating elements Blazor did not render - querySelector, getElementById, createElement and node traversal - with a bridge back to every ElementReference extension in the library.
@inject Bit.Butil.Dom domMDN reference
ElementReference, which means an
element your own markup rendered. That boundary exists because reaching into your own components
by selector is how Blazor's diffing and your code end up disagreeing about what is on the page -
Blazor will re-render over whatever you changed, at a moment you do not control. This is the way
out for what the boundary does not cover: a third-party widget, something a script put there, an
element you are building to hand to a library.
True when there is a document to query, which is any real browser. During prerender/SSR it returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.Dom dom
var supported = await dom.IsSupported();An invalid selector answers null rather than throwing: a selector is usually built from something a user or a configuration file supplied, so a bad one is an input error rather than an exceptional condition. QueryAll is a snapshot, not a live list - elements added afterwards are not in it.
var widget = await dom.Query(".third-party-widget");
var all = await dom.QueryAll("[data-role]");
var byId = await dom.ById("some-id");
var head = await dom.Head(); // where a stylesheet or a script goesA little markup to search: bold, italic, and a span with an id.
- one
- two
- three
A created element is not on the page until something appends it. Moving an element that is already somewhere moves it - a node is in one place at a time, so there is no need to remove it first. Appending into one of your own rendered elements is possible but risky: Blazor owns that element's children and its next diff may remove what you put there, so use a container your markup leaves empty.
var box = await dom.Create("div");
await box!.SetAttribute("class", "note");
await box.SetText("built from C#");
var body = await dom.Body();
await body!.Append(box);
// SVG needs its namespace, or it renders as nothing:
var circle = await dom.Create("circle", "http://www.w3.org/2000/svg");Element-wise, not node-wise: the sibling and child accessors skip text and comment nodes, which is why raw node traversal so rarely does what you meant - the whitespace between two elements in your markup is a node too.
var item = await dom.Query("ul[data-role] li");
var list = await item!.GetParent();
var second = await item.GetNextSibling();
var section = await item.Closest("[data-role]");
var isListItem = await item.Matches("li");This is what keeps the surface small: a found or created element becomes an ElementReference, and every element extension in Butil - classes, ARIA, styles, scrolling, layout, events - works on it. Two conditions: the element has to be in the document, and the lookup does not pierce shadow roots. It leans on the convention Blazor's own reference lookup uses, which is covered by a test, so a future change shows up here rather than in your app.
var found = await dom.Query("#some-widget");
var reference = await found!.AsElementReference();
// and now the whole element surface applies:
await reference!.Value.AddClass("highlighted");
await reference.Value.SetStyleProperty("outline", "2px solid currentColor");
var rect = await reference.Value.GetBoundingClientRect();SetText is safe with any input - markup in it becomes visible characters rather than elements. SetHtml is an injection point: anything from a user, a URL or a server response can carry script. Not a script tag, which will not run, but an onerror on an img, which will. Use SetText for anything you did not write yourself.
{
private DomHandle? handle; // from dom.QuerySelector / CreateElement
private async Task Write()
{
await handle!.SetText("<b>not bold</b>"); // shows the characters
await handle.SetHtml("<b>bold</b>"); // parses - only for markup you wrote
}
}Remove takes it off the page. A handle to
an element that has been removed keeps working - it is simply no longer connected. As a safety
net the Dom service releases every handle when its scope is torn down, which
likewise leaves the page alone.
API reference
ValueTask<bool> IsSupported()ValueTask<DomHandle?> Query(string selector), ValueTask<DomHandle[]> QueryAll(string selector)ValueTask<DomHandle?> ById(string elementId)ValueTask<DomHandle?> Body(), Head(), DocumentElement()ValueTask<DomHandle?> Create(string tagName, string? namespaceUri = null)ValueTask<ElementReference?> AsElementReference()ValueTask<DomHandle?> Query(string), ValueTask<DomHandle[]> QueryAll(string), ValueTask<DomHandle?> Closest(string), ValueTask<bool> Matches(string)ValueTask<DomHandle?> GetParent(), GetFirstChild(), GetLastChild(), GetNextSibling(), GetPreviousSibling(); ValueTask<DomHandle[]> GetChildren()ValueTask<bool> Append(DomHandle), Prepend(DomHandle), InsertBefore(DomHandle), AppendTo(ElementReference), Remove()ValueTask<string> GetText(), GetHtml(); ValueTask<bool> SetText(string), SetHtml(string)ValueTask<string?> GetAttribute(string); ValueTask<bool> SetAttribute(string, string), RemoveAttribute(string)ValueTask<bool> IsConnected()ValueTask DisposeAsync()