Heimdall Docs

Payloads

Payloads define what data Heimdall sends when an interaction runs. A trigger answers when. A payload answers what.

In Heimdall, payload gathering usually stays close to the DOM boundary that already owns the current UI state, but the runtime also supports direct objects, references, self-sourced data, and explicit selectors.

1. What a Payload Means in Heimdall

A payload is the request data gathered when a Heimdall interaction fires. It does not define the trigger, the target, or the swap. It only defines what request data is sent to the action.

Trigger = when the interaction begins
Payload = what data is sent
Action  = what server code runs
Swap    = how returned HTML is applied

2. The Core Pattern

Heimdall payload binding is usually boundary-based. Instead of inventing a separate client-side state store, the runtime gathers data from a nearby form or state boundary already present in the DOM.

button.Heimdall()
    .Click("Counter.Increment")
    .PayloadFromClosestState()
    .SwapOuter();

3. Why Payload Boundaries Matter

Heimdall works best when request data is gathered from the same DOM structure that already represents the current UI truth. That keeps the browser simple and keeps data ownership visible.

The DOM already holds the current interaction boundary.
Payload binding stays near the UI that produced the data.
The browser does not need a separate state container just to construct requests.
The action receives an ordinary DTO shaped around that boundary.

4. Closest Form

Closest form gathers request data from the nearest containing form. This is the natural fit for submit flows, interactive forms, and any interaction where form fields already define the request shape.

Strongly Typed Setup

form.Heimdall()
    .OnSubmit("Contact.Submit")
    .PayloadFromClosestForm()
    .SwapNone();

Rendered HTML Mental Model

<form
  heimdall-content-submit="Contact.Submit"
  heimdall-payload-from="closest-form"
  heimdall-content-swap="none">
  ...
</form>

This is the cleanest payload boundary when the interaction is already form-shaped.

5. Closest State

Closest state gathers request data from the nearest Heimdall state boundary. This is a natural fit for counters, cards, rows, lazy-loading sentinels, and other UI regions where the state already lives on the DOM boundary rather than in form inputs.

button.Heimdall()
    .Click("Counter.Increment")
    .PayloadFromClosestState()
    .SwapOuter();

This works especially well when the element invoking the server lives inside a stateful UI fragment that should remain self-contained.

<div data-heimdall-state='{"count":1}'>
  <button
    heimdall-content-click="Counter.Increment"
    heimdall-payload-from="closest-state"
    heimdall-content-swap="outer">
    Increment
  </button>
</div>

6. Keyed Closest State

Keyed closest state narrows payload binding to a specific named state boundary. This is especially useful when multiple state boundaries exist in the same region or when a component should carry only one particular slice of state forward.

Strongly Typed Setup

sentinel.Heimdall()
    .Visible("Weather.LoadMore")
    .PayloadFromClosestState("weather")
    .Target("#weather-sentinel")
    .SwapOuter();

Rendered HTML Mental Model

<div
  id="weather-sentinel"
  data-heimdall-state-weather='{"page":2}'
  heimdall-content-visible="Weather.LoadMore"
  heimdall-payload-from="closest-state:weather"
  heimdall-content-target="#weather-sentinel"
  heimdall-content-swap="outer">
  Loading more...
</div>

Keyed state works well when the DOM boundary is carrying continuation data, filters, cursors, or other component-local request context.

7. State and Form Payloads Are Complementary

Form payloads and state payloads solve different problems. Forms are a natural request boundary for user-entered fields. State boundaries are a natural request boundary for server-owned UI fragments that carry their own current context.

Form payloads:
- user-entered fields
- validation surfaces
- submit-oriented interactions

State payloads:
- server-rendered UI fragments
- counters, rows, cards, sentinels
- continuation or cursor state

8. Self

Self tells the runtime to build the payload from the element’s own dataset. This is useful when a button, row action, or compact UI command already carries the small amount of data needed for the request.

<button
  data-id="42"
  data-status="active"
  heimdall-content-click="Users.Select"
  heimdall-payload-from="self"
  heimdall-content-swap="outer">
  Select
</button>

This is a good fit for lightweight command-style interactions where a full form or state boundary would be unnecessary.

9. Explicit Form Selector

The runtime can also gather payload from an explicit selector. This is useful when the trigger lives outside the form boundary but should still submit that form’s current data.

<button
  heimdall-content-click="Search.Run"
  heimdall-payload-from="#search-form"
  heimdall-content-target="#search-results"
  heimdall-content-swap="inner">
  Search
</button>

<form id="search-form">
  ...
</form>

This keeps the request source explicit without forcing the trigger to be nested inside the form itself.

10. Inline Object Payload

For small fixed request objects, Heimdall can take the payload directly from inline JSON on the element. This is useful when the request shape is known at render time and does not need a separate boundary.

<button
  heimdall-content-click="Reports.Load"
  heimdall-payload='{"page":2,"sort":"desc"}'
  heimdall-content-swap="inner">
  Load
</button>

This is best reserved for small, stable payloads. Once the object becomes large or dynamic, form, state, or reference-based sources are usually clearer.

11. Referenced Object Payload

Heimdall can also resolve a payload from a global object path. This is useful when some surrounding page script already owns a request object and the interaction should reuse it directly.

<button
  heimdall-content-click="Reports.Load"
  heimdall-payload-ref="App.Reports.Filters"
  heimdall-content-swap="inner">
  Refresh
</button>

This keeps the interaction HTML-first while still allowing reuse of an existing browser-side object when one already exists.

12. Ref-Based Payload Source

The runtime also supports a ref-based payload source through heimdall-payload-from. Conceptually this is similar to payload-ref: the payload comes from an object path rather than from form or DOM state boundaries.

<button
  heimdall-content-click="Reports.Load"
  heimdall-payload-from="ref:App.Reports.Filters"
  heimdall-content-swap="inner">
  Refresh
</button>

Both approaches are supported. The main distinction is whether the payload source is expressed as its own attribute or through payload-from.

13. Payload Source Precedence

The runtime checks payload sources in a specific order. Inline payload is considered first, then payload-ref, then payload-from. In practice that means a direct payload object wins over the other payload-source mechanisms if multiple are present.

1. heimdall-payload
2. heimdall-payload-ref
3. heimdall-payload-from

14. What the Action Receives

Once Heimdall gathers the payload, the action receives it as a normal DTO. Payload binding stays a browser concern. Action parameters stay a server concern.

public sealed class WeatherState
{
    public int Page { get; set; }
}

The binding model stays straightforward: a payload object, optional framework types, and optional DI services.

[ContentInvocation]
public static IHtmlContent LoadMore(WeatherState state)
{
    return RenderNextPage(state.Page);
}

15. Payload Binding Is Not a Client State Store

It is important not to over-read payload binding. Heimdall does not introduce a client-side application state layer here. It simply gathers data from boundaries or objects that already exist at the moment the interaction runs.

DOM boundary or object already exists
-> trigger fires
-> runtime gathers payload
-> server decides next HTML

16. Choosing the Right Payload Source

The right payload source is usually the boundary or object that already owns the interaction’s current data.

Use closest form when the data lives in form fields.
Use closest state when the data lives on a nearby stateful UI fragment.
Use keyed closest state when one specific slice of state should be sent.
Use self when the element’s own dataset already contains the request data.
Use an explicit selector when the trigger lives outside the form it should submit.
Use inline or referenced objects when the payload is already available as a small browser-side object.

17. Payloads Work the Same Across Triggers

Payload binding is independent of whether the interaction starts from click, submit, input, visible, load, or polling. The trigger decides when the request starts. The payload decides what data that request carries.

click   + closest-state
submit  + closest-form
visible + closest-state:key
load    + closest-state
poll    + closest-state
click   + self
click   + ref:object

18. Common Mental Model

A good way to think about payloads in Heimdall is that the browser gathers the smallest meaningful request context from the boundary or object already representing the current interaction state.

Trigger starts interaction
-> payload source is resolved
-> request data is gathered
-> action runs
-> returned HTML becomes the next UI truth

19. Why This Fits the Heimdall Model

Payload sources reinforce Heimdall’s larger architecture. The DOM remains the interaction boundary, the server remains the source of truth, and the browser stays a thin runtime rather than becoming a second application.

DOM = current interaction boundary
Server = next UI decision
Payload = bridge between the two

Next Steps

Once payloads are clear, the next step is understanding how the browser runtime interprets triggers, modifiers, polling, targets, and swaps together, and then how those pieces compose into larger patterns.