Mutations
Change attributes, classes, or Heimdall state in place while preserving the target node and its browser-owned state.
Mutations are ordered response directives. They work in content-action and Bifrost SSE responses.
1. Mutation or Swap?
Use a mutation for metadata and state on an existing node. Return replacement HTML when text, children, or structure changed substantially.
Mutation:
- class and attribute changes
- accessibility state
- Heimdall state objects
- preserve focus, input state, listeners, and references
Swap:
- new text or child content
- changed markup structure
- a fresh server-rendered component2. Fluent Response
Mutate resolves the first matching root. MutateAll resolves every matching root. Operations run in the order written.
return FluentHtml.Fragment(fragment => fragment
.Add(OrderPanel.Render(order))
.Heimdall(h => h.Mutate("#order-status", mutation => mutation
.Attr("aria-live", "polite")
.RemoveAttr("aria-busy")
.RemoveClass("loading")
.AddClass("ready")
.State(new { order.Id, order.Status }))));
return FluentHtml.Fragment(fragment => fragment
.Heimdall(heimdall => heimdall.MutateAll(
".order-card",
mutation => mutation.AddClass("stale"))));3. Raw HTML Directive
The fluent response renders an ordered mutation element. A mutation-attr with value sets or replaces an attribute; omitting value removes it. mutation-class uses an explicit add or remove attribute. scope is self, subtree, or select; all applies the directive to every matching root instead of only the first.
<mutation heimdall-content-target="#order-status" scope="self">
<mutation-attr name="aria-live" value="polite"></mutation-attr>
<mutation-attr name="aria-busy"></mutation-attr>
<mutation-class remove="loading"></mutation-class>
<mutation-class add="ready highlighted"></mutation-class>
</mutation>
<mutation
heimdall-content-target=".order-card"
scope="select"
selector=".price"
all>
<mutation-class add="stale"></mutation-class>
</mutation>4. Operations
Attribute, class, and state operations share one ordered builder. Empty is a real attribute value; removal is explicit.
mutation
.Attr("data-status", "complete")
.Set(Html.Attr("aria-busy", "false"))
.RemoveAttr("disabled")
.AddClass("complete", "highlight")
.RemoveClass("pending")
.State(new { Count = 3 })
.State("row", new { Selected = true })
.StateJson("{\"count\":3}")
.RemoveState("draft");5. Safe No-Ops
Removing an absent class, attribute, or state entry succeeds without changing the DOM. Adding an existing class also succeeds without duplicating it. This keeps idempotent server responses simple.
remove missing attribute -> no-op
remove missing class -> no-op
add existing class -> no duplicate
set existing attribute -> replace its value6. Scope
The target selector chooses root elements. The scope chooses which nodes within each root receive the operation.
HeimdallHtml.MutationScope.Self
// resolved root only (default)
HeimdallHtml.MutationScope.Subtree
// root and every descendant
HeimdallHtml.MutationScope.Matching(".price")
// matching descendants inside the root
MutateAll(".card", ...) + Matching(".price")
// every .price under every resolved .card root7. Response Order
Invocations and mutations execute in response order before the normal main-target swap. This lets one directive create a node and a following directive mutate it.
1. invocation creates or replaces #status
2. mutation updates the new #status
3. normal action or SSE target swap runs
<abort> suppresses the main swap; directives still run
<redirect> wins; no mutation or swap effects are applied8. Action and SSE Behavior
The directive format is identical in action and streamed HTML. Each complete response is processed as an ordered unit; dependent directives should remain in one response and in the required order.
await bifrost.PublishAsync(
topic: "orders",
content: FluentHtml.Fragment(fragment => fragment
.Heimdall(h => h.MutateAll(
".order-card",
m => m.AddClass("stale")))),
ttl: TimeSpan.FromSeconds(5),
ct: ct);9. Lifecycle Events
The before event is cancellable. After reports target and operation counts. Error reports a stable code and message. Events bubble from the action source when one exists and identify an action or SSE origin.
document.addEventListener("heimdall:mutation-before", event => {
// Inspect event.detail.directive, targets, operations, origin.
});
document.addEventListener("heimdall:mutation-after", event => {
console.log(event.detail.targetCount,
event.detail.operationCount);
});
document.addEventListener("heimdall:mutation-error", event => {
console.error(event.detail.code, event.detail.message);
});10. Heimdall Attributes
Mutating Heimdall's own trigger or SSE attributes is supported. After mutation, the runtime reconciles the affected behavior naturally. The framework does not block selectors or classes that overlap with its own markup; that targeting decision belongs to the application.
mutation
.Attr("heimdall-content-click", "orders.refresh")
.RemoveAttr("heimdall-content-disable");