Heimdall Docs

Swaps

A swap controls how Heimdall applies returned HTML to the DOM. This is one of the core concepts of the framework.

Choosing the right swap determines whether you replace content, replace the host element, append to a list, prepend new items, or do no direct DOM update at all.

1. What a Swap Is

When an action returns HTML, Heimdall needs to know where and how that HTML should be applied. The target chooses where. The swap chooses how.

button.Heimdall()
    .Click("Example.Refresh")
    .Target("#result")
    .SwapInner();

2. The Available Swap Modes

Heimdall supports a small set of explicit swap modes. These map directly to the client runtime.

inner — replace the contents of the target element
outer — replace the target element itself
beforeend — append returned HTML inside the target
afterbegin — prepend returned HTML inside the target
none — do not apply the main response directly to the target

3. Inner Swap

Use inner when you want to keep the host element stable and replace only its contents. This is a very common choice for forms, panels, and content regions.

button.Heimdall()
    .Click("Example.Refresh")
    .Target("#result")
    .SwapInner();

4. Outer Swap

Use outer when the returned HTML should replace the target element itself. This is useful when the server returns a fully refreshed host with new state or attributes.

button.Heimdall()
    .Click("Counter.Increment")
    .Target("#counter-host")
    .SwapOuter();

5. Append with BeforeEnd

Use beforeend when you want to append new content to the end of a target. This works well for lists, feeds, and toast containers.

button.Heimdall()
    .Click("Notes.Add")
    .Target("#notes-list")
    .SwapBeforeEnd();

6. Prepend with AfterBegin

Use afterbegin when new content should appear at the beginning of the target. This is useful for newest-first lists, notifications, and toasts.

button.Heimdall()
    .Click("Toast.Create")
    .Target("#toast-manager")
    .SwapAfterBegin();

7. No Direct Swap

Use none when the main response should not be directly inserted into the target. This is especially useful when the response is only performing out-of-band updates.

button.Heimdall()
    .Click("Toast.Publish")
    .SwapNone();

8. Aborting the Main Swap

Sometimes the server needs to stop the main swap for a response, but still let invocations run. Heimdall supports this with an abort directive.

When an abort directive is present, the client skips the main DOM swap for that response. Out-of-band work and invocations can still be processed. This is useful for failure flows where the target should remain unchanged, but the user should still receive feedback such as a toast.

Builder API

fragment.Abort();
fragment.Abort("delete-failed");

Rendered HTML

<abort></abort>

<abort reason="delete-failed"></abort>

Why This Exists

Prevent an incorrect or stale UI update when an action fails
Keep the current target content intact
Still allow invocations to run, such as toasts, banners, or other out-of-band feedback

9. Abort vs None

Abort and none are related, but they solve different problems.

Use None When

  • The action is intentionally designed to do no main-target swap
  • The response only performs out-of-band updates
  • There is no main DOM insertion to suppress

Use Abort When

  • The response would normally participate in a main swap
  • The server detects a failure or invalid condition
  • You want to skip only the main swap while still processing invocations

10. Strongly Typed Markup and Rendered HTML

Heimdall helpers emit plain HTML attributes. The swap mode is part of the browser-side contract.

Strongly Typed Markup

button.Heimdall()
    .Click("Example.Refresh")
    .Target("#result")
    .SwapInner();

Rendered HTML

<button
  heimdall-content-click="Example.Refresh"
  heimdall-content-target="#result"
  heimdall-content-swap="inner">
</button>

11. Choosing the Right Swap

A good rule of thumb is to choose the smallest swap that preserves the structure you want to keep.

Use Inner When

  • The target element should stay in place
  • Only the contents need to change
  • You want a stable host for future targeting

Use Outer When

  • The host element itself has changed
  • New state or attributes belong on the host
  • The server returns a complete replacement boundary

Use BeforeEnd / AfterBegin When

  • You are growing a list or collection
  • You want to append or prepend items
  • The existing list should remain intact

Use None When

  • The response only performs out-of-band updates
  • You do not want a direct main-target update
  • The action triggers other UI changes instead

Use Abort When

  • A normal swap should be cancelled for this response
  • The current UI should remain untouched
  • Invocations should still execute so the user receives feedback

12. Common Pattern: Stable Host

A very common Heimdall pattern is to keep a host element stable with an inner swap. This makes targeting reliable and keeps the DOM boundary consistent.

<div id="form-host">
  <!-- server-rendered form -->
</div>

13. Common Pattern: Replace the Host

When the host itself carries important state or attributes, an outer swap is often the better choice.

<div id="counter-host" data-heimdall-state='{"count":1}'>
  ...
</div>

14. Common Pattern: Abort the Swap but Still Show Feedback

A common failure pattern is to cancel the main swap, keep the current UI visible, and still push a toast or other invocation so the user understands what happened.

[ContentInvocation(ActionID_Delete)]
public static async ValueTask<IHtmlContent> Delete(StoreEntryReference reference, StoreManager _store)
{
    var failureToast = new ToastItem
    {
        Header = "Delete Failed",
        Content = $"A user failed to delete entry with key '{reference.Key}'. It may have already been deleted.",
        Type = ToastType.Error,
    };

    var successToast = new ToastItem
    {
        Header = "Delete Successful",
        Content = $"Successfully deleted entry with key '{reference.Key}'.",
        Type = ToastType.Success,
    };

    var failureToastContent = ToastManager.Create(failureToast);
    var successToastContent = ToastManager.Create(successToast);
    var abortSignal = HeimdallHtml.Abort();

    try
    {
        if (!_store.TryRemove(reference.Key, out var removed) || removed is null)
        {
            return FluentHtml.Fragment(f =>
            {
                f.Add(abortSignal);
                f.Add(failureToastContent.AsInvocation());
            });
        }

        return successToastContent.AsInvocation();
    }
    catch (Exception)
    {
        return FluentHtml.Fragment(f =>
        {
            f.Add(abortSignal);
            f.Add(failureToastContent.AsInvocation());
        });
    }
}

In this pattern, a failed delete does not disturb the current DOM target. The abort directive suppresses the main swap, while the toast invocation still runs and communicates the failure to the user.

Next Steps

Once swaps make sense, the next concept is state. That is where Heimdall starts to feel especially different.