Skip to documentation
Heimdall Docs
Heimdall Docs

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.

Text
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 component

2. Fluent Response

Mutate resolves the first matching root. MutateAll resolves every matching root. Operations run in the order written.

C#
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.

HTML
<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.

Text
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.

Text
remove missing attribute -> no-op
remove missing class     -> no-op
add existing class       -> no duplicate
set existing attribute   -> replace its value

6. Scope

The target selector chooses root elements. The scope chooses which nodes within each root receive the operation.

C#
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 root

7. 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.

Text
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 applied

8. 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.

C#
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.

JavaScript
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.

Text
mutation
    .Attr("heimdall-content-click", "orders.refresh")
    .RemoveAttr("heimdall-content-disable");